ics-query 0.1.0a0__tar.gz → 0.1.1a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/PKG-INFO +91 -12
  2. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/README.md +88 -9
  3. ics_query-0.1.1a0/ics-query +1 -0
  4. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/ics_query/_version.py +2 -2
  5. ics_query-0.1.1a0/ics_query/cli.py +260 -0
  6. ics_query-0.1.1a0/ics_query/parse.py +50 -0
  7. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/ics_query/tests/conftest.py +24 -12
  8. ics_query-0.1.1a0/ics_query/tests/runs/at 2019-03-04 multiple-calendars.ics -.run +20 -0
  9. ics_query-0.1.1a0/ics_query/tests/runs/at 2019-03-04 one-event-twice.ics -.run +18 -0
  10. ics_query-0.1.1a0/ics_query/tests/runs/at 2019-03-07 multiple-calendars.ics -.run +11 -0
  11. ics_query-0.1.1a0/ics_query/tests/runs/calendars/multiple-calendars.ics +71 -0
  12. ics_query-0.1.1a0/ics_query/tests/runs/calendars/one-event-twice.ics +68 -0
  13. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/ics_query/tests/test_command_line.py +9 -0
  14. ics_query-0.1.1a0/ics_query/tests/test_parse_date.py +63 -0
  15. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/pyproject.toml +1 -1
  16. ics_query-0.1.0a0/ics_query/cli.py +0 -92
  17. ics_query-0.1.0a0/ics_query/parse.py +0 -13
  18. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/.github/FUNDING.yml +0 -0
  19. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  20. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/.github/dependabot.yml +0 -0
  21. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/.github/workflows/tests.yml +0 -0
  22. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/.gitignore +0 -0
  23. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/LICENSE +0 -0
  24. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/ics_query/__init__.py +0 -0
  25. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/ics_query/__main__.py +0 -0
  26. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/ics_query/tests/__init__.py +0 -0
  27. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/ics_query/tests/runs/at 2019-03-04 one-event.ics -.run +0 -0
  28. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/ics_query/tests/runs/calendars/one-event.ics +0 -0
  29. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/ics_query/version.py +0 -0
  30. {ics_query-0.1.0a0 → ics_query-0.1.1a0}/tox.ini +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ics-query
3
- Version: 0.1.0a0
4
- Summary: Find out what happens in ICS calendar files - query and filter RFC 5545 compatible `.ics` files for events, journals, TODOs and more.
3
+ Version: 0.1.1a0
4
+ Summary: Find out what happens in ICS calendar files - query and filter RFC 5545 compatible .ics files for events, journals, TODOs and more.
5
5
  Project-URL: Homepage, https://github.com/niccokunzmann/ics-query/
6
6
  Project-URL: Repository, https://github.com/niccokunzmann/ics-query/
7
- Project-URL: source_archive, https://github.com/niccokunzmann/ics-query/archive/cf4aefe6fb80c98ee554f4030ce9b2e7f16db162.zip
7
+ Project-URL: source_archive, https://github.com/niccokunzmann/ics-query/archive/fb12ef2822d23f693aa5f9faaa0cb97fa9f67516.zip
8
8
  Project-URL: Issues, https://github.com/niccokunzmann/ics-query/issues
9
9
  Project-URL: Documentation, https://github.com/niccokunzmann/ics-query/
10
10
  Project-URL: Changelog, https://github.com/niccokunzmann/ics-query/#changelog
@@ -716,7 +716,79 @@ You can install this package from the [PyPI](https://pypi.org/project/ics-query/
716
716
  pip install ics-query
717
717
  ```
718
718
 
719
- ## `ics-query at` - occurrences at certain times
719
+ ## Usage
720
+
721
+ See how to use `ics-query`.
722
+
723
+ ### Examples
724
+
725
+ You can easily get a calendar from the web and see what is on.
726
+ In this example, we show which German National Holidays happen in August 2024:
727
+
728
+ ```shell
729
+ $ wget -qO- 'https://www.calendarlabs.com/ical-calendar/ics/46/Germany_Holidays.ics' | ./ics-query at 2024-08 - -
730
+ BEGIN:VEVENT
731
+ SUMMARY:Assumption Day (BY\, SL)
732
+ DTSTART;VALUE=DATE:20240815
733
+ DTEND;VALUE=DATE:20240815
734
+ DTSTAMP:20231013T092513Z
735
+ UID:65290cf9326601697189113@calendarlabs.com
736
+ SEQUENCE:0
737
+ DESCRIPTION:Visit https://calendarlabs.com/holidays/us/the-assumption-of-m
738
+ ary.php to know more about Assumption Day (BY\, SL). \n\n Like us on Faceb
739
+ ook: http://fb.com/calendarlabs to get updates
740
+ LOCATION:Germany
741
+ STATUS:CONFIRMED
742
+ TRANSP:TRANSPARENT
743
+ END:VEVENT
744
+ ```
745
+
746
+ In the following example, we query a calendar file and print the result.
747
+
748
+ ```shell
749
+ $ ics-query at 2019-03-04 one-event.ics -
750
+ BEGIN:VEVENT
751
+ SUMMARY:test1
752
+ DTSTART;TZID=Europe/Berlin:20190304T080000
753
+ DTEND;TZID=Europe/Berlin:20190304T083000
754
+ DTSTAMP:20190303T111937
755
+ UID:UYDQSG9TH4DE0WM3QFL2J
756
+ CREATED:20190303T111937
757
+ LAST-MODIFIED:20190303T111937
758
+ END:VEVENT
759
+ ```
760
+
761
+ We can concatenate calendars and pipe them into `ics-query`.
762
+ In the example below, we get all events that happen right now in two calendars.
763
+
764
+ ```shell
765
+ $ cat calendar1.ics calendar2.ics | ics-query at `date +%Y%m%d%H%M%S` - -
766
+ BEGIN:VEVENT
767
+ ...
768
+ ```
769
+
770
+ ### Events at Certain Times
771
+
772
+ You can query which events happen at certain times:
773
+
774
+ ```shell
775
+ ics-query at <date-time> calendar.ics -
776
+ ```
777
+
778
+ `<date-time>` can be built up: It can be a year, a month, a day, an hour, a minute or a second.
779
+
780
+ Please see the command documentation for more help:
781
+
782
+ ```shell
783
+ ics-query --help
784
+ ics-query at --help
785
+ ```
786
+
787
+ ## Vision
788
+
789
+ This section shows where we would like to get to.
790
+
791
+ ### `ics-query at` - occurrences at certain times
720
792
 
721
793
  You can get all **events** that happen at a certain **day**.
722
794
 
@@ -736,32 +808,32 @@ You can get all **TODO**s that happen at in certain **month**.
736
808
  ics-query --components VTODO at 2029-12-24 calendar.ics
737
809
  ```
738
810
 
739
- ## `ics-query at` - time ranges
811
+ ### `ics-query at` - time ranges
740
812
 
741
813
 
742
- ## `ics-query --output=count` - count occurrences
814
+ ### `ics-query --output=count` - count occurrences
743
815
 
744
816
 
745
- ## `ics-query --output=ics` - use ics as output (default)
817
+ ### `ics-query --output=ics` - use ics as output (default)
746
818
 
747
819
 
748
- ## `ics-query --select-index` - reduce output size
820
+ ### `ics-query --select-index` - reduce output size
749
821
 
750
822
  Examples: `0,2,4` `0-10`
751
823
 
752
- ## `ics-query all` - the whole calendar
824
+ ### `ics-query all` - the whole calendar
753
825
 
754
- ## `ics-query between` - time ranges
826
+ ### `ics-query between` - time ranges
755
827
 
756
828
  ```shell
757
829
  ics-query between dt dt
758
830
  ics-query between dt duration
759
831
  ```
760
832
 
761
- ## `ics-query --select-component` - filter for components
833
+ ### `ics-query --select-component` - filter for components
762
834
 
763
835
 
764
- ## `ics-query --select-uid` - filter by uid
836
+ ### `ics-query --select-uid` - filter by uid
765
837
 
766
838
 
767
839
  ## How to edit an event
@@ -859,12 +931,19 @@ To release new versions,
859
931
 
860
932
  ```shell
861
933
  git tag v0.1.0a
934
+ git push origin v0.1.0a
862
935
  ```
863
936
 
864
937
  5. Notify the issues about their release
865
938
 
866
939
  ## Changelog
867
940
 
941
+ - v0.1.1a
942
+
943
+ - Add `--version`
944
+ - Add `ics-query at <date>`
945
+ - Add support for multiple calendars in one input
946
+
868
947
  - v0.1.0a
869
948
 
870
949
  - Update Python version compatibility
@@ -11,7 +11,79 @@ You can install this package from the [PyPI](https://pypi.org/project/ics-query/
11
11
  pip install ics-query
12
12
  ```
13
13
 
14
- ## `ics-query at` - occurrences at certain times
14
+ ## Usage
15
+
16
+ See how to use `ics-query`.
17
+
18
+ ### Examples
19
+
20
+ You can easily get a calendar from the web and see what is on.
21
+ In this example, we show which German National Holidays happen in August 2024:
22
+
23
+ ```shell
24
+ $ wget -qO- 'https://www.calendarlabs.com/ical-calendar/ics/46/Germany_Holidays.ics' | ./ics-query at 2024-08 - -
25
+ BEGIN:VEVENT
26
+ SUMMARY:Assumption Day (BY\, SL)
27
+ DTSTART;VALUE=DATE:20240815
28
+ DTEND;VALUE=DATE:20240815
29
+ DTSTAMP:20231013T092513Z
30
+ UID:65290cf9326601697189113@calendarlabs.com
31
+ SEQUENCE:0
32
+ DESCRIPTION:Visit https://calendarlabs.com/holidays/us/the-assumption-of-m
33
+ ary.php to know more about Assumption Day (BY\, SL). \n\n Like us on Faceb
34
+ ook: http://fb.com/calendarlabs to get updates
35
+ LOCATION:Germany
36
+ STATUS:CONFIRMED
37
+ TRANSP:TRANSPARENT
38
+ END:VEVENT
39
+ ```
40
+
41
+ In the following example, we query a calendar file and print the result.
42
+
43
+ ```shell
44
+ $ ics-query at 2019-03-04 one-event.ics -
45
+ BEGIN:VEVENT
46
+ SUMMARY:test1
47
+ DTSTART;TZID=Europe/Berlin:20190304T080000
48
+ DTEND;TZID=Europe/Berlin:20190304T083000
49
+ DTSTAMP:20190303T111937
50
+ UID:UYDQSG9TH4DE0WM3QFL2J
51
+ CREATED:20190303T111937
52
+ LAST-MODIFIED:20190303T111937
53
+ END:VEVENT
54
+ ```
55
+
56
+ We can concatenate calendars and pipe them into `ics-query`.
57
+ In the example below, we get all events that happen right now in two calendars.
58
+
59
+ ```shell
60
+ $ cat calendar1.ics calendar2.ics | ics-query at `date +%Y%m%d%H%M%S` - -
61
+ BEGIN:VEVENT
62
+ ...
63
+ ```
64
+
65
+ ### Events at Certain Times
66
+
67
+ You can query which events happen at certain times:
68
+
69
+ ```shell
70
+ ics-query at <date-time> calendar.ics -
71
+ ```
72
+
73
+ `<date-time>` can be built up: It can be a year, a month, a day, an hour, a minute or a second.
74
+
75
+ Please see the command documentation for more help:
76
+
77
+ ```shell
78
+ ics-query --help
79
+ ics-query at --help
80
+ ```
81
+
82
+ ## Vision
83
+
84
+ This section shows where we would like to get to.
85
+
86
+ ### `ics-query at` - occurrences at certain times
15
87
 
16
88
  You can get all **events** that happen at a certain **day**.
17
89
 
@@ -31,32 +103,32 @@ You can get all **TODO**s that happen at in certain **month**.
31
103
  ics-query --components VTODO at 2029-12-24 calendar.ics
32
104
  ```
33
105
 
34
- ## `ics-query at` - time ranges
106
+ ### `ics-query at` - time ranges
35
107
 
36
108
 
37
- ## `ics-query --output=count` - count occurrences
109
+ ### `ics-query --output=count` - count occurrences
38
110
 
39
111
 
40
- ## `ics-query --output=ics` - use ics as output (default)
112
+ ### `ics-query --output=ics` - use ics as output (default)
41
113
 
42
114
 
43
- ## `ics-query --select-index` - reduce output size
115
+ ### `ics-query --select-index` - reduce output size
44
116
 
45
117
  Examples: `0,2,4` `0-10`
46
118
 
47
- ## `ics-query all` - the whole calendar
119
+ ### `ics-query all` - the whole calendar
48
120
 
49
- ## `ics-query between` - time ranges
121
+ ### `ics-query between` - time ranges
50
122
 
51
123
  ```shell
52
124
  ics-query between dt dt
53
125
  ics-query between dt duration
54
126
  ```
55
127
 
56
- ## `ics-query --select-component` - filter for components
128
+ ### `ics-query --select-component` - filter for components
57
129
 
58
130
 
59
- ## `ics-query --select-uid` - filter by uid
131
+ ### `ics-query --select-uid` - filter by uid
60
132
 
61
133
 
62
134
  ## How to edit an event
@@ -154,12 +226,19 @@ To release new versions,
154
226
 
155
227
  ```shell
156
228
  git tag v0.1.0a
229
+ git push origin v0.1.0a
157
230
  ```
158
231
 
159
232
  5. Notify the issues about their release
160
233
 
161
234
  ## Changelog
162
235
 
236
+ - v0.1.1a
237
+
238
+ - Add `--version`
239
+ - Add `ics-query at <date>`
240
+ - Add support for multiple calendars in one input
241
+
163
242
  - v0.1.0a
164
243
 
165
244
  - Update Python version compatibility
@@ -0,0 +1 @@
1
+ .venv/bin/ics-query
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.1.0a0'
16
- __version_tuple__ = version_tuple = (0, 1, 0)
15
+ __version__ = version = '0.1.1a0'
16
+ __version_tuple__ = version_tuple = (0, 1, 1)
@@ -0,0 +1,260 @@
1
+ """The command line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import os # noqa: TCH003
7
+ import sys
8
+ import typing as t
9
+
10
+ import click
11
+ import recurring_ical_events
12
+ from icalendar.cal import Calendar, Component
13
+ from recurring_ical_events import CalendarQuery
14
+
15
+ from . import parse
16
+ from .version import __version__
17
+
18
+ if t.TYPE_CHECKING:
19
+ from io import FileIO
20
+
21
+ from icalendar.cal import Component
22
+
23
+ from .parse import DateArgument
24
+
25
+ print = functools.partial(print, file=sys.stderr) # noqa: A001
26
+
27
+
28
+ class ComponentsResult:
29
+ """Output interface for components."""
30
+
31
+ def __init__(self, output: FileIO):
32
+ """Create a new result."""
33
+ self._file = output
34
+
35
+ def add_component(self, component: Component):
36
+ """Return a component."""
37
+ self._file.write(component.to_ical())
38
+
39
+
40
+ class ComponentsResultArgument(click.File):
41
+ """Argument for the result."""
42
+
43
+ def convert(
44
+ self,
45
+ value: str | os.PathLike[str] | t.IO[t.Any],
46
+ param: click.Parameter | None,
47
+ ctx: click.Context | None,
48
+ ) -> ComponentsResult:
49
+ """Return a ComponentsResult."""
50
+ file = super().convert(value, param, ctx)
51
+ return ComponentsResult(file)
52
+
53
+
54
+ class JoinedCalendars:
55
+ def __init__(self, calendars: list[Calendar]):
56
+ """Join multiple calendars."""
57
+ self.queries = [recurring_ical_events.of(calendar) for calendar in calendars]
58
+
59
+ def at(self, dt: tuple[int]) -> t.Generator[Component]:
60
+ """Return the components."""
61
+ for query in self.queries:
62
+ yield from query.at(dt)
63
+
64
+
65
+ class CalendarQueryInputArgument(click.File):
66
+ """Argument for the result."""
67
+
68
+ def convert(
69
+ self,
70
+ value: str | os.PathLike[str] | t.IO[t.Any],
71
+ param: click.Parameter | None,
72
+ ctx: click.Context | None,
73
+ ) -> recurring_ical_events.CalendarQuery:
74
+ """Return a CalendarQuery."""
75
+ file = super().convert(value, param, ctx)
76
+ calendars = Calendar.from_ical(file.read(), multiple=True)
77
+ return JoinedCalendars(calendars)
78
+
79
+
80
+ arg_calendar = click.argument("calendar", type=CalendarQueryInputArgument("rb"))
81
+ arg_output = click.argument("output", type=ComponentsResultArgument("wb"))
82
+
83
+
84
+ @click.group()
85
+ @click.version_option(__version__)
86
+ def main():
87
+ """Find out what happens in ICS calendar files.
88
+
89
+ ics-query can query and filter RFC 5545 compatible .ics files.
90
+ Components are events, journal entries and TODOs.
91
+
92
+ \b
93
+ Common Parameters
94
+ -----------------
95
+
96
+ Common parameters are described below.
97
+
98
+ CALENDAR
99
+
100
+ The CALENDAR is a readable file with one or more ICS calendars in it.
101
+ If CALENDAR is "-", then the standard input is used.
102
+
103
+ OUTPUT
104
+
105
+ This is the OUTPUT file for the result.
106
+ It is usually a path to a file that can be written to.
107
+ If OUTPUT is "-", then the standard output is used.
108
+
109
+ \b
110
+ Notes on Calculation
111
+ --------------------
112
+
113
+ An event can be very long. If you request smaller time spans or a time as
114
+ exact as a second, the event will still occur within this time span if it
115
+ happens during that time.
116
+
117
+ Generally, an event occurs within a time span if this applies:
118
+
119
+ event.DTSTART <= span.DTEND and span.DTSTART < event.DTEND
120
+
121
+ The START is INCLUSIVE, then END is EXCLUSIVE.
122
+
123
+ \b
124
+ Notes on Timezones
125
+ ------------------
126
+ """ # noqa: D301
127
+
128
+
129
+ pass_datetime = click.make_pass_decorator(parse.to_time)
130
+
131
+
132
+ @main.command()
133
+ @click.argument("date", type=parse.to_time)
134
+ @arg_calendar
135
+ @arg_output
136
+ def at(calendar: CalendarQuery, output: ComponentsResult, date: DateArgument):
137
+ """Occurrences at a certain dates.
138
+
139
+ YEAR
140
+
141
+ All occurrences in this year.
142
+
143
+ \b
144
+ Formats:
145
+ \b
146
+ YYYY
147
+ \b
148
+ Examples:
149
+ \b
150
+ ics-query at 2024 # all occurrences in year 2024
151
+ ics-query at `date +%Y` # all occurrences in this year
152
+
153
+ MONTH
154
+
155
+ All occurrences in this month.
156
+
157
+ \b
158
+ Formats:
159
+
160
+ YYYY-MM
161
+ YYYY-M
162
+ YYYYMM
163
+ \b
164
+ Examples:
165
+ \b
166
+ ics-query at 2019-10 # October 2019
167
+ ics-query at 1990-01 # January 1990
168
+ ics-query at 1990-1 # January 1990
169
+ ics-query at 199001 # January 1990
170
+ ics-query at `date +%Y%m` # this month
171
+
172
+ DAY
173
+
174
+ All occurrences in one day.
175
+
176
+ \b
177
+ Formats:
178
+
179
+ YYYY-MM-DD
180
+ YYYY-M-D
181
+ YYYYMMDD
182
+ \b
183
+ Examples:
184
+ \b
185
+ ics-query at 1990-01-01 # 1st January 1990
186
+ ics-query at 1990-1-1 # 1st January 1990
187
+ ics-query at 19900101 # 1st January 1990
188
+ ics-query at `date +%Y%m%d` # today
189
+
190
+ HOUR
191
+
192
+ All occurrences within one hour.
193
+
194
+ \b
195
+ Formats:
196
+ \b
197
+ YYYY-MM-DD HH
198
+ YYYY-MM-DDTHH
199
+ YYYY-M-DTH
200
+ YYYYMMDDTHH
201
+ YYYYMMDDHH
202
+ \b
203
+ Examples:
204
+ \b
205
+ ics-query at 1990-01-01 00 # 1st January 1990, 12am - 1am
206
+ ics-query at 1990-01-01T00 # 1st January 1990, 12am - 1am
207
+ ics-query at 1990-1-1T17 # 1st January 1990, 17:00 - 18:00
208
+ ics-query at 19900101T23 # 1st January 1990, 23:00 - midnight
209
+ ics-query at 1990010123 # 1st January 1990, 23:00 - midnight
210
+ ics-query at `date +%Y%m%d%H` # this hour
211
+
212
+ MINUTE
213
+
214
+ All occurrences within one minute.
215
+
216
+ \b
217
+ Formats:
218
+ \b
219
+ YYYY-MM-DD HH:MM
220
+ YYYY-MM-DDTHH:MM
221
+ YYYY-M-DTH:M
222
+ YYYYMMDDTHHMM
223
+ YYYYMMDDHHMM
224
+ \b
225
+ Examples:
226
+ \b
227
+ ics-query at 1990-01-01 10:10 # 1st January 1990, 10:10am - 10:11am
228
+ ics-query at 1990-01-01T10:10 # 1st January 1990, 10:10am - 10:11am
229
+ ics-query at 1990-1-1T7:2 # 1st January 1990, 07:02 - 07:03
230
+ ics-query at 19900101T2359 # 1st January 1990, 23:59 - midnight
231
+ ics-query at 199001012359 # 1st January 1990, 23:59 - midnight
232
+ ics-query at `date +%Y%m%d%H%M` # this minute
233
+
234
+ SECOND
235
+
236
+ All occurrences at a precise time.
237
+
238
+ \b
239
+ Formats:
240
+ \b
241
+ YYYY-MM-DD HH:MM:SS
242
+ YYYY-MM-DDTHH:MM:SS
243
+ YYYY-M-DTH:M:S
244
+ YYYYMMDDTHHMMSS
245
+ YYYYMMDDHHMMSS
246
+ \b
247
+ Examples:
248
+ \b
249
+ ics-query at 1990-01-01 10:10:00 # 1st January 1990, 10:10am
250
+ ics-query at 1990-01-01T10:10:00 # 1st January 1990, 10:10am
251
+ ics-query at 1990-1-1T7:2:30 # 1st January 1990, 07:02:30
252
+ ics-query at 19901231T235959 # 31st December 1990, 23:59:59
253
+ ics-query at 19900101235959 # 1st January 1990, 23:59:59
254
+ ics-query at `date +%Y%m%d%H%M%S` # now
255
+ """ # noqa: D301
256
+ for event in calendar.at(date):
257
+ output.add_component(event)
258
+
259
+
260
+ __all__ = ["main"]
@@ -0,0 +1,50 @@
1
+ """Functions for parsing the content."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ DateArgument = tuple[int]
8
+
9
+
10
+ class InvalidTimeFormat(ValueError):
11
+ """The value provided does not yield a precise time."""
12
+
13
+
14
+ REGEX_TIME = re.compile(
15
+ r"^(?P<year>\d\d\d\d)"
16
+ r"(?P<month>-\d?\d|\d\d)?"
17
+ r"(?P<day>-\d?\d|\d\d)?"
18
+ r"(?P<hour>[ T]\d?\d|\d\d)?"
19
+ r"(?P<minute>:\d?\d|\d\d)?"
20
+ r"(?P<second>:\d?\d|\d\d)?"
21
+ r"$"
22
+ )
23
+
24
+
25
+ def to_time(dt: str) -> DateArgument:
26
+ """Parse the time and date."""
27
+ parsed_dt = REGEX_TIME.match(dt)
28
+ if parsed_dt is None:
29
+ raise InvalidTimeFormat(dt)
30
+
31
+ def group(group_name: str) -> tuple[int]:
32
+ """Return a group's value."""
33
+ result = parsed_dt.group(group_name)
34
+ while result and result[0] not in "0123456789":
35
+ result = result[1:]
36
+ if result is None:
37
+ return ()
38
+ return (int(result),)
39
+
40
+ return (
41
+ group("year")
42
+ + group("month")
43
+ + group("day")
44
+ + group("hour")
45
+ + group("minute")
46
+ + group("second")
47
+ )
48
+
49
+
50
+ __all__ = ["to_time", "DateArgument"]
@@ -5,12 +5,13 @@ from __future__ import annotations
5
5
  import subprocess
6
6
  from copy import deepcopy
7
7
  from pathlib import Path
8
- from typing import NamedTuple
8
+ from typing import Callable, NamedTuple
9
9
 
10
10
  import pytest
11
11
 
12
12
  HERE = Path(__file__).parent
13
13
  IO_DIRECTORY = HERE / "runs"
14
+ CALENDARS_DIRECTORY = IO_DIRECTORY / "calendars"
14
15
 
15
16
 
16
17
  class TestRun(NamedTuple):
@@ -34,12 +35,26 @@ class TestRun(NamedTuple):
34
35
  )
35
36
 
36
37
 
38
+ def run_ics_query(*command, cwd=CALENDARS_DIRECTORY) -> TestRun:
39
+ """Run ics-qeury with a command."""
40
+ cmd = ["ics-query", *command]
41
+ print(" ".join(cmd))
42
+ completed_process = subprocess.run( # noqa: S603, RUF100
43
+ cmd, # noqa: S603, RUF100
44
+ capture_output=True,
45
+ timeout=3,
46
+ check=False,
47
+ cwd=cwd,
48
+ )
49
+ return TestRun.from_completed_process(completed_process)
50
+
51
+
37
52
  class IOTestCase(NamedTuple):
38
53
  """An example test case."""
39
54
 
40
55
  name: str
41
56
  command: list[str]
42
- cwd: Path
57
+ location: Path
43
58
  expected_output: str
44
59
 
45
60
  @classmethod
@@ -50,16 +65,7 @@ class IOTestCase(NamedTuple):
50
65
 
51
66
  def run(self) -> TestRun:
52
67
  """Run this test case and return the result."""
53
- command = ["ics-query", *self.command]
54
- print(" ".join(command))
55
- completed_process = subprocess.run( # noqa: S603, RUF100
56
- command, # noqa: S603, RUF100
57
- capture_output=True,
58
- timeout=3,
59
- check=False,
60
- cwd=self.cwd / "calendars",
61
- )
62
- return TestRun.from_completed_process(completed_process)
68
+ return run_ics_query(*self.command)
63
69
 
64
70
 
65
71
  io_test_cases = [
@@ -75,4 +81,10 @@ def io_testcase(request) -> IOTestCase:
75
81
  return deepcopy(request.param)
76
82
 
77
83
 
84
+ @pytest.fixture
85
+ def run() -> Callable[..., TestRun]:
86
+ """Return a runner function."""
87
+ return run_ics_query
88
+
89
+
78
90
  __all__ = ["IOTestCase", "TestRun"]
@@ -0,0 +1,20 @@
1
+ BEGIN:VEVENT
2
+ SUMMARY:test4
3
+ DTSTART;TZID=Europe/Berlin:20190304T000000
4
+ DTEND;TZID=Europe/Berlin:20190304T010000
5
+ DTSTAMP:20190303T111937
6
+ UID:UYDQSG9TH4DE0WM3QFL2J
7
+ CLASS:PUBLIC
8
+ CREATED:20190303T111937
9
+ LAST-MODIFIED:20190303T111937
10
+ STATUS:CONFIRMED
11
+ END:VEVENT
12
+ BEGIN:VEVENT
13
+ SUMMARY:test1
14
+ DTSTART;TZID=Europe/Berlin:20190304T080000
15
+ DTEND;TZID=Europe/Berlin:20190304T083000
16
+ DTSTAMP:20190303T111937
17
+ UID:UYDQSG9TH4DE0WM3QFL2J
18
+ CREATED:20190303T111937
19
+ LAST-MODIFIED:20190303T111937
20
+ END:VEVENT
@@ -0,0 +1,18 @@
1
+ BEGIN:VEVENT
2
+ SUMMARY:test1
3
+ DTSTART;TZID=Europe/Berlin:20190304T080000
4
+ DTEND;TZID=Europe/Berlin:20190304T083000
5
+ DTSTAMP:20190303T111937
6
+ UID:UYDQSG9TH4DE0WM3QFL2J
7
+ CREATED:20190303T111937
8
+ LAST-MODIFIED:20190303T111937
9
+ END:VEVENT
10
+ BEGIN:VEVENT
11
+ SUMMARY:test1
12
+ DTSTART;TZID=Europe/Berlin:20190304T080000
13
+ DTEND;TZID=Europe/Berlin:20190304T083000
14
+ DTSTAMP:20190303T111937
15
+ UID:UYDQSG9TH4DE0WM3QFL2J
16
+ CREATED:20190303T111937
17
+ LAST-MODIFIED:20190303T111937
18
+ END:VEVENT
@@ -0,0 +1,11 @@
1
+ BEGIN:VEVENT
2
+ SUMMARY:test4
3
+ DTSTART;TZID=Europe/Berlin:20190307T000000
4
+ DTEND;TZID=Europe/Berlin:20190307T010000
5
+ DTSTAMP:20190303T111937
6
+ UID:UYDQSG9TH4DE0WM3QFL2J
7
+ CLASS:PUBLIC
8
+ CREATED:20190303T111937
9
+ LAST-MODIFIED:20190303T111937
10
+ STATUS:CONFIRMED
11
+ END:VEVENT
@@ -0,0 +1,71 @@
1
+ BEGIN:VCALENDAR
2
+ VERSION:2.0
3
+ PRODID:-//SabreDAV//SabreDAV//EN
4
+ CALSCALE:GREGORIAN
5
+ X-WR-CALNAME:test
6
+ X-APPLE-CALENDAR-COLOR:#e78074
7
+ BEGIN:VTIMEZONE
8
+ TZID:Europe/Berlin
9
+ X-LIC-LOCATION:Europe/Berlin
10
+ BEGIN:DAYLIGHT
11
+ TZOFFSETFROM:+0100
12
+ TZOFFSETTO:+0200
13
+ TZNAME:CEST
14
+ DTSTART:19700329T020000
15
+ RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
16
+ END:DAYLIGHT
17
+ BEGIN:STANDARD
18
+ TZOFFSETFROM:+0200
19
+ TZOFFSETTO:+0100
20
+ TZNAME:CET
21
+ DTSTART:19701025T030000
22
+ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
23
+ END:STANDARD
24
+ END:VTIMEZONE
25
+ BEGIN:VEVENT
26
+ CREATED:20190303T111937
27
+ DTSTAMP:20190303T111937
28
+ LAST-MODIFIED:20190303T111937
29
+ UID:UYDQSG9TH4DE0WM3QFL2J
30
+ SUMMARY:test4
31
+ CLASS:PUBLIC
32
+ STATUS:CONFIRMED
33
+ RRULE:FREQ=DAILY;COUNT=3;INTERVAL=3
34
+ DTSTART;TZID=Europe/Berlin:20190304T000000
35
+ DTEND;TZID=Europe/Berlin:20190304T010000
36
+ END:VEVENT
37
+ END:VCALENDAR
38
+ BEGIN:VCALENDAR
39
+ VERSION:2.0
40
+ PRODID:-//SabreDAV//SabreDAV//EN
41
+ CALSCALE:GREGORIAN
42
+ X-WR-CALNAME:test
43
+ X-APPLE-CALENDAR-COLOR:#e78074
44
+ BEGIN:VTIMEZONE
45
+ TZID:Europe/Berlin
46
+ X-LIC-LOCATION:Europe/Berlin
47
+ BEGIN:DAYLIGHT
48
+ TZOFFSETFROM:+0100
49
+ TZOFFSETTO:+0200
50
+ TZNAME:CEST
51
+ DTSTART:19700329T020000
52
+ RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
53
+ END:DAYLIGHT
54
+ BEGIN:STANDARD
55
+ TZOFFSETFROM:+0200
56
+ TZOFFSETTO:+0100
57
+ TZNAME:CET
58
+ DTSTART:19701025T030000
59
+ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
60
+ END:STANDARD
61
+ END:VTIMEZONE
62
+ BEGIN:VEVENT
63
+ CREATED:20190303T111937
64
+ DTSTAMP:20190303T111937
65
+ LAST-MODIFIED:20190303T111937
66
+ UID:UYDQSG9TH4DE0WM3QFL2J
67
+ SUMMARY:test1
68
+ DTSTART;TZID=Europe/Berlin:20190304T080000
69
+ DTEND;TZID=Europe/Berlin:20190304T083000
70
+ END:VEVENT
71
+ END:VCALENDAR
@@ -0,0 +1,68 @@
1
+ BEGIN:VCALENDAR
2
+ VERSION:2.0
3
+ PRODID:-//SabreDAV//SabreDAV//EN
4
+ CALSCALE:GREGORIAN
5
+ X-WR-CALNAME:test
6
+ X-APPLE-CALENDAR-COLOR:#e78074
7
+ BEGIN:VTIMEZONE
8
+ TZID:Europe/Berlin
9
+ X-LIC-LOCATION:Europe/Berlin
10
+ BEGIN:DAYLIGHT
11
+ TZOFFSETFROM:+0100
12
+ TZOFFSETTO:+0200
13
+ TZNAME:CEST
14
+ DTSTART:19700329T020000
15
+ RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
16
+ END:DAYLIGHT
17
+ BEGIN:STANDARD
18
+ TZOFFSETFROM:+0200
19
+ TZOFFSETTO:+0100
20
+ TZNAME:CET
21
+ DTSTART:19701025T030000
22
+ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
23
+ END:STANDARD
24
+ END:VTIMEZONE
25
+ BEGIN:VEVENT
26
+ CREATED:20190303T111937
27
+ DTSTAMP:20190303T111937
28
+ LAST-MODIFIED:20190303T111937
29
+ UID:UYDQSG9TH4DE0WM3QFL2J
30
+ SUMMARY:test1
31
+ DTSTART;TZID=Europe/Berlin:20190304T080000
32
+ DTEND;TZID=Europe/Berlin:20190304T083000
33
+ END:VEVENT
34
+ END:VCALENDAR
35
+ BEGIN:VCALENDAR
36
+ VERSION:2.0
37
+ PRODID:-//SabreDAV//SabreDAV//EN
38
+ CALSCALE:GREGORIAN
39
+ X-WR-CALNAME:test
40
+ X-APPLE-CALENDAR-COLOR:#e78074
41
+ BEGIN:VTIMEZONE
42
+ TZID:Europe/Berlin
43
+ X-LIC-LOCATION:Europe/Berlin
44
+ BEGIN:DAYLIGHT
45
+ TZOFFSETFROM:+0100
46
+ TZOFFSETTO:+0200
47
+ TZNAME:CEST
48
+ DTSTART:19700329T020000
49
+ RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
50
+ END:DAYLIGHT
51
+ BEGIN:STANDARD
52
+ TZOFFSETFROM:+0200
53
+ TZOFFSETTO:+0100
54
+ TZNAME:CET
55
+ DTSTART:19701025T030000
56
+ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
57
+ END:STANDARD
58
+ END:VTIMEZONE
59
+ BEGIN:VEVENT
60
+ CREATED:20190303T111937
61
+ DTSTAMP:20190303T111937
62
+ LAST-MODIFIED:20190303T111937
63
+ UID:UYDQSG9TH4DE0WM3QFL2J
64
+ SUMMARY:test1
65
+ DTSTART;TZID=Europe/Berlin:20190304T080000
66
+ DTEND;TZID=Europe/Berlin:20190304T083000
67
+ END:VEVENT
68
+ END:VCALENDAR
@@ -1,5 +1,7 @@
1
1
  """Test the commmand line."""
2
2
 
3
+ from ics_query.version import version
4
+
3
5
  from .conftest import IOTestCase
4
6
 
5
7
 
@@ -8,3 +10,10 @@ def test_check_program_output(io_testcase: IOTestCase):
8
10
  result = io_testcase.run()
9
11
  print(result.error)
10
12
  assert result.output == io_testcase.expected_output
13
+
14
+
15
+ def test_version(run):
16
+ """Check the version is displayed."""
17
+ result = run("--version")
18
+ assert result.exit_code == 0
19
+ assert version in result.output
@@ -0,0 +1,63 @@
1
+ """This tests parsing input times and dates."""
2
+
3
+ import pytest
4
+
5
+ from ics_query.parse import InvalidTimeFormat, to_time
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ ("string_argument", "expected_result"),
10
+ [
11
+ # year
12
+ ("2019", (2019,)),
13
+ ("1991", (1991,)),
14
+ # month
15
+ ("2000-11", (2000, 11)),
16
+ ("2001-01", (2001, 1)),
17
+ ("1990-1", (1990, 1)),
18
+ ("201011", (2010, 11)),
19
+ ("199003", (1990, 3)),
20
+ # day
21
+ ("1990-01-01", (1990, 1, 1)),
22
+ ("2001-3-4", (2001, 3, 4)),
23
+ ("19801231", (1980, 12, 31)),
24
+ # hour
25
+ ("1990-01-01 00", (1990, 1, 1, 0)),
26
+ ("1991-12-31T23", (1991, 12, 31, 23)),
27
+ ("2003-1-1T17", (2003, 1, 1, 17)),
28
+ ("20010409T12", (2001, 4, 9, 12)),
29
+ ("2014101018", (2014, 10, 10, 18)),
30
+ # minute
31
+ ("1990-01-01 00:10", (1990, 1, 1, 0, 10)),
32
+ ("1991-12-31T23:11", (1991, 12, 31, 23, 11)),
33
+ ("2003-1-1T17:0", (2003, 1, 1, 17, 0)),
34
+ ("2004-1-1T7:0", (2004, 1, 1, 7, 0)),
35
+ ("20010409T12:59", (2001, 4, 9, 12, 59)),
36
+ ("201410101830", (2014, 10, 10, 18, 30)),
37
+ # second
38
+ ("1990-01-01 00:10:12", (1990, 1, 1, 0, 10, 12)),
39
+ ("1991-12-31T23:11:0", (1991, 12, 31, 23, 11, 0)),
40
+ ("2003-1-1T17:0:11", (2003, 1, 1, 17, 0, 11)),
41
+ ("2004-1-1T7:0:10", (2004, 1, 1, 7, 0, 10)),
42
+ ("20010409T12:59:58", (2001, 4, 9, 12, 59, 58)),
43
+ ("20141010183012", (2014, 10, 10, 18, 30, 12)),
44
+ ],
45
+ )
46
+ def test_parse_to_date_argument(string_argument, expected_result):
47
+ """Check that we can properly parse what is accepted."""
48
+ result = to_time(string_argument)
49
+ assert result == expected_result
50
+
51
+
52
+ @pytest.mark.parametrize(
53
+ "dt",
54
+ [
55
+ "",
56
+ "132",
57
+ "12345",
58
+ ],
59
+ )
60
+ def test_invalid_time_format(dt: str):
61
+ """Check invalid time formats."""
62
+ with pytest.raises(InvalidTimeFormat):
63
+ to_time(dt)
@@ -13,7 +13,7 @@ authors = [
13
13
  maintainers = [
14
14
  { name="Nicco Kunzmann", email="niccokunzmann@rambler.ru" },
15
15
  ]
16
- description = "Find out what happens in ICS calendar files - query and filter RFC 5545 compatible `.ics` files for events, journals, TODOs and more."
16
+ description = "Find out what happens in ICS calendar files - query and filter RFC 5545 compatible .ics files for events, journals, TODOs and more."
17
17
  readme = "README.md"
18
18
  requires-python = ">=3.8"
19
19
  # see https://pypi.python.org/pypi?%3Aaction=list_classifiers
@@ -1,92 +0,0 @@
1
- """The command line interface."""
2
-
3
- from __future__ import annotations
4
-
5
- import functools
6
- import os # noqa: TCH003
7
- import sys
8
- import typing as t
9
-
10
- import click
11
- import recurring_ical_events
12
- from icalendar import Calendar
13
- from recurring_ical_events import CalendarQuery
14
-
15
- from . import parse
16
-
17
- if t.TYPE_CHECKING:
18
- from io import FileIO
19
-
20
- from icalendar.cal import Component
21
-
22
- from .parse import DateArgument
23
-
24
- print = functools.partial(print, file=sys.stderr) # noqa: A001
25
-
26
-
27
- class ComponentsResult:
28
- """Output interface for components."""
29
-
30
- def __init__(self, output: FileIO):
31
- """Create a new result."""
32
- self._file = output
33
-
34
- def add_component(self, component: Component):
35
- """Return a component."""
36
- self._file.write(component.to_ical())
37
-
38
-
39
- class ComponentsResultArgument(click.File):
40
- """Argument for the result."""
41
-
42
- def convert(
43
- self,
44
- value: str | os.PathLike[str] | t.IO[t.Any],
45
- param: click.Parameter | None,
46
- ctx: click.Context | None,
47
- ) -> ComponentsResult:
48
- """Return a ComponentsResult."""
49
- file = super().convert(value, param, ctx)
50
- return ComponentsResult(file)
51
-
52
-
53
- class CalendarQueryInputArgument(click.File):
54
- """Argument for the result."""
55
-
56
- def convert(
57
- self,
58
- value: str | os.PathLike[str] | t.IO[t.Any],
59
- param: click.Parameter | None,
60
- ctx: click.Context | None,
61
- ) -> recurring_ical_events.CalendarQuery:
62
- """Return a CalendarQuery."""
63
- file = super().convert(value, param, ctx)
64
- calendar = Calendar.from_ical(file.read())
65
- return recurring_ical_events.of(calendar)
66
-
67
-
68
- arg_calendar = click.argument("calendar", type=CalendarQueryInputArgument("rb"))
69
- arg_output = click.argument("output", type=ComponentsResultArgument("wb"))
70
-
71
-
72
- @click.group()
73
- def main():
74
- """Simple program that greets NAME for a total of COUNT times."""
75
- # sys.stdout = sys.stderr # remove accidential print impact
76
-
77
-
78
- pass_datetime = click.make_pass_decorator(parse.to_time)
79
-
80
-
81
- @main.command()
82
- @click.argument("date", type=parse.to_time)
83
- @arg_calendar
84
- @arg_output
85
- def at(calendar: CalendarQuery, output: ComponentsResult, date: DateArgument):
86
- """Get the components at a certain time."""
87
- for event in calendar.at(date):
88
- print("debug")
89
- output.add_component(event)
90
-
91
-
92
- __all__ = ["main"]
@@ -1,13 +0,0 @@
1
- """Functions for parsing the content."""
2
-
3
- from __future__ import annotations
4
-
5
- DateArgument = tuple[int]
6
-
7
-
8
- def to_time(dt: str) -> DateArgument:
9
- """Parse the time and date."""
10
- return tuple(map(int, dt.split("-")))
11
-
12
-
13
- __all__ = ["to_time", "DateArgument"]
File without changes
File without changes
File without changes