Update upstream source from tag 'v4.2.0'
Update to upstream version '4.2.0'
with Debian dir fc96c1caf50dde220382a4d6b7c2475fe84a30ba
Unit 193
4 years ago
0 | v4.2.0 | |
1 | * Prompt user for calendar on `add' when it isn't specified | |
2 | * Add `end' time to details view | |
3 | * New `updates' command | |
4 | * Automatically use available console width | |
5 | ||
0 | 6 | v4.1.1 |
1 | 7 | * Fixed regression on now marking |
2 | 8 | * Fixed version string management |
5 | 5 | |
6 | 6 | gcalcli is a Python application that allows you to access your Google |
7 | 7 | Calendar(s) from a command line. It's easy to get your agenda, search for |
8 | events, add new events, delete events, edit events, and even import those | |
9 | annoying ICS/vCal invites from Microsoft Exchange and/or other sources. | |
10 | Additionally, gcalcli can be used as a reminder service and execute any | |
11 | application you want when an event is coming up. | |
8 | events, add new events, delete events, edit events, see recently updated | |
9 | events, and even import those annoying ICS/vCal invites from Microsoft | |
10 | Exchange and/or other sources. Additionally, gcalcli can be used as a reminder | |
11 | service and execute any application you want when an event is coming up. | |
12 | 12 | |
13 | 13 | gcalcli uses the [Google Calendar API version 3](https://developers.google.com/google-apps/calendar/). |
14 | 14 | |
72 | 72 | * OAuth2 authention with your Google account |
73 | 73 | * list your calendars |
74 | 74 | * show an agenda using a specified start/end date and time |
75 | * show updates since a specified datetime for events between a start/end date and time | |
75 | 76 | * ascii text graphical calendar display with variable width |
76 | 77 | * search for past and/or future events |
77 | 78 | * "quick add" new events to a specified calendar |
109 | 110 | list list available calendars |
110 | 111 | edit edit calendar events |
111 | 112 | agenda get an agenda for a time period |
113 | updates get updates since a datetime for a time period | |
112 | 114 | calw get a week-based agenda in calendar format |
113 | 115 | calm get a month agenda in calendar format |
114 | 116 | quick quick-add an event to a calendar |
27 | 27 | |
28 | 28 | |
29 | 29 | Positional arguments: |
30 | {list,search,edit,delete,agenda,calw,calm,quick,add,import,remind} | |
30 | {list,search,edit,delete,agenda,updates,calw,calm,quick,add,import,remind} | |
31 | 31 | Invoking a subcommand with --help prints subcommand |
32 | 32 | usage. |
33 | 33 | list list available calendars |
34 | 34 | edit edit calendar events |
35 | 35 | agenda get an agenda for a time period |
36 | updates get updates since a datetime for a time period | |
36 | 37 | calw get a week-based agenda in calendar format |
37 | 38 | calm get a month agenda in calendar format |
38 | 39 | quick quick-add an event to a calendar |
0 | 0 | __program__ = 'gcalcli' |
1 | __version__ = 'v4.1.1' | |
1 | __version__ = 'v4.2.0' | |
2 | 2 | __author__ = 'Eric Davis, Brian Hartvigsen, Joshua Crowgey' |
3 | 3 | __API_CLIENT_ID__ = '232867676714.apps.googleusercontent.com' |
4 | 4 | __API_CLIENT_SECRET__ = '3tZSxItw6_VnZMezQwC8lUqy' |
4 | 4 | from gcalcli.deprecations import parser_allow_deprecated, DeprecatedStoreTrue |
5 | 5 | from gcalcli.printer import valid_color_name |
6 | 6 | from oauth2client import tools |
7 | from shutil import get_terminal_size | |
7 | 8 | import copy as _copy |
9 | import datetime | |
10 | import locale | |
8 | 11 | |
9 | 12 | DETAILS = ['calendar', 'location', 'length', 'reminders', 'description', |
10 | 'url', 'attendees', 'email', 'attachments'] | |
13 | 'url', 'attendees', 'email', 'attachments', 'end'] | |
11 | 14 | |
12 | 15 | |
13 | 16 | PROGRAM_OPTIONS = { |
47 | 50 | 'help': 'Choose line art style for calendars: ' + |
48 | 51 | '"fancy": for VTcodes, "unicode" for ' + |
49 | 52 | 'Unicode box drawing characters, "ascii" ' + |
50 | 'for old-school plusses, hyphens and pipes.'} | |
53 | 'for old-school plusses, hyphens and pipes.'}, | |
51 | 54 | } |
52 | 55 | |
53 | 56 | |
88 | 91 | return details_parser |
89 | 92 | |
90 | 93 | |
94 | def locale_has_24_hours(): | |
95 | t = datetime.time(20) | |
96 | formatted = t.strftime(locale.nl_langinfo(locale.T_FMT)) | |
97 | return '20' in formatted | |
98 | ||
99 | ||
100 | def get_auto_width(): | |
101 | console_width = get_terminal_size().columns | |
102 | day_width = int((console_width - 8) / 7) | |
103 | return day_width if day_width > 9 else 10 | |
104 | ||
105 | ||
91 | 106 | def get_output_parser(parents=[]): |
92 | 107 | output_parser = argparse.ArgumentParser(add_help=False, parents=parents) |
93 | 108 | output_parser.add_argument( |
99 | 114 | output_parser.add_argument( |
100 | 115 | '--nodeclined', action='store_true', dest='ignore_declined', |
101 | 116 | default=False, help='Hide events that have been declined') |
102 | output_parser.add_argument( | |
103 | '--width', '-w', default=10, dest='cal_width', type=validwidth, | |
104 | help='Set output width') | |
105 | output_parser.add_argument( | |
106 | '--military', action='store_true', default=False, | |
117 | auto_width = get_auto_width() | |
118 | output_parser.add_argument( | |
119 | '--width', '-w', default=auto_width, dest='cal_width', | |
120 | type=validwidth, help='Set output width') | |
121 | has_24_hours = locale_has_24_hours() | |
122 | output_parser.add_argument( | |
123 | '--military', action='store_true', default=has_24_hours, | |
107 | 124 | help='Use 24 hour display') |
125 | output_parser.add_argument( | |
126 | '--no-military', action='store_false', default=has_24_hours, | |
127 | help='Use 12 hour display', dest='military') | |
108 | 128 | output_parser.add_argument( |
109 | 129 | '--override-color', action='store_true', default=False, |
110 | 130 | help='Use overridden color for event') |
166 | 186 | return cal_query_parser |
167 | 187 | |
168 | 188 | |
189 | def get_updates_parser(): | |
190 | updates_parser = argparse.ArgumentParser(add_help=False) | |
191 | updates_parser.add_argument('since', type=utils.get_time_from_str) | |
192 | updates_parser.add_argument( | |
193 | 'start', | |
194 | type=utils.get_time_from_str, nargs='?') | |
195 | updates_parser.add_argument('end', type=utils.get_time_from_str, nargs='?') | |
196 | return updates_parser | |
197 | ||
198 | ||
169 | 199 | def get_start_end_parser(): |
170 | 200 | se_parser = argparse.ArgumentParser(add_help=False) |
171 | 201 | se_parser.add_argument('start', type=utils.get_time_from_str, nargs='?') |
220 | 250 | |
221 | 251 | remind_parser = get_remind_parser() |
222 | 252 | cal_query_parser = get_cal_query_parser() |
253 | updates_parser = get_updates_parser() | |
223 | 254 | |
224 | 255 | # parsed start and end times |
225 | 256 | start_end_parser = get_start_end_parser() |
259 | 290 | parents=[details_parser, output_parser, start_end_parser], |
260 | 291 | help='get an agenda for a time period', |
261 | 292 | description='Get an agenda for a time period.') |
293 | ||
294 | sub.add_parser( | |
295 | 'updates', | |
296 | parents=[details_parser, output_parser, updates_parser], | |
297 | help='get updates since a datetime for a time period ' | |
298 | '(defaults to through end of current month)', | |
299 | description='Get updates since a datetime for a time period ' | |
300 | '(default to through end of current month).') | |
262 | 301 | |
263 | 302 | calw = sub.add_parser( |
264 | 303 | 'calw', parents=[details_parser, output_parser, cal_query_parser], |
153 | 153 | elif parsed_args.command == 'agenda': |
154 | 154 | gcal.AgendaQuery(start=parsed_args.start, end=parsed_args.end) |
155 | 155 | |
156 | elif parsed_args.command == 'updates': | |
157 | gcal.UpdatesQuery( | |
158 | last_updated_datetime=parsed_args.since, | |
159 | start=parsed_args.start, | |
160 | end=parsed_args.end) | |
161 | ||
156 | 162 | elif parsed_args.command == 'calw': |
157 | 163 | gcal.CalQuery( |
158 | 164 | parsed_args.command, count=parsed_args.weeks, |
5 | 5 | def __init__(self, message): |
6 | 6 | super(ValidationError, self).__init__(message) |
7 | 7 | self.message = message |
8 | ||
9 | ||
10 | def raise_one_cal_error(cals): | |
11 | raise GcalcliError( | |
12 | 'You must only specify a single calendar\n' | |
13 | 'Calendars: {}\n'.format(cals) | |
14 | ) |
9 | 9 | import sys |
10 | 10 | from unicodedata import east_asian_width |
11 | 11 | |
12 | from dateutil.relativedelta import relativedelta | |
12 | 13 | from datetime import datetime, timedelta, date |
13 | 14 | from gcalcli import __program__, __version__ |
14 | 15 | from gcalcli import utils |
653 | 654 | indent = 10 * ' ' |
654 | 655 | details_indent = 19 * ' ' |
655 | 656 | |
656 | if self.options['military']: | |
657 | time_format = '%-5s' | |
658 | tmp_time_str = event['s'].strftime('%H:%M') | |
659 | else: | |
660 | time_format = '%-7s' | |
661 | tmp_time_str = \ | |
662 | event['s'].strftime('%I:%M').lstrip('0').rjust(5) + \ | |
663 | event['s'].strftime('%p').lower() | |
664 | ||
665 | 657 | if not prefix: |
666 | 658 | prefix = indent |
667 | ||
668 | 659 | self.printer.msg(prefix, self.options['color_date']) |
669 | 660 | |
670 | 661 | happening_now = event['s'] <= self.now <= event['e'] |
679 | 670 | if happening_now and not all_day \ |
680 | 671 | else self._calendar_color(event) |
681 | 672 | |
673 | time_width = '%-5s' if self.options['military'] else '%-7s' | |
682 | 674 | if all_day: |
683 | fmt = ' ' + time_format + ' %s\n' | |
675 | fmt = ' ' + time_width + ' %s\n' | |
684 | 676 | self.printer.msg( |
685 | 677 | fmt % ('', self._valid_title(event).strip()), |
686 | 678 | event_color |
687 | 679 | ) |
688 | 680 | else: |
689 | fmt = ' ' + time_format + ' %s\n' | |
681 | tmp_start_time_str = \ | |
682 | utils.agenda_time_fmt(event['s'], self.options['military']) | |
683 | tmp_end_time_str = '' | |
684 | fmt = ' ' + time_width + ' ' + time_width + ' %s\n' | |
685 | ||
686 | if self.details.get('end'): | |
687 | tmp_end_time_str = \ | |
688 | utils.agenda_time_fmt(event['e'], self.options['military']) | |
689 | fmt = ' ' + time_width + ' - ' + time_width + ' %s\n' | |
690 | ||
690 | 691 | self.printer.msg( |
691 | fmt % (tmp_time_str, self._valid_title(event).strip()), | |
692 | fmt % (tmp_start_time_str, tmp_end_time_str, | |
693 | self._valid_title(event).strip()), | |
692 | 694 | event_color |
693 | 695 | ) |
694 | 696 | |
1165 | 1167 | |
1166 | 1168 | return self._display_queried_events(start, end, search_text, True) |
1167 | 1169 | |
1170 | def UpdatesQuery(self, last_updated_datetime, start=None, end=None): | |
1171 | if not start: | |
1172 | start = self.now.replace(hour=0, minute=0, second=0, microsecond=0) | |
1173 | ||
1174 | if not end: | |
1175 | end = (start + relativedelta(months=+1)).replace(day=1) | |
1176 | ||
1177 | event_list = self._search_for_events(start, end, None) | |
1178 | event_list = [e for e in event_list | |
1179 | if (utils.get_time_from_str(e['updated']) >= | |
1180 | last_updated_datetime)] | |
1181 | print("Updates since:", | |
1182 | last_updated_datetime, | |
1183 | "events starting", | |
1184 | start, | |
1185 | "until", | |
1186 | end) | |
1187 | return self._iterate_events(start, event_list, year_date=False) | |
1188 | ||
1168 | 1189 | def AgendaQuery(self, start=None, end=None): |
1169 | 1190 | if not start: |
1170 | 1191 | start = self.now.replace(hour=0, minute=0, second=0, microsecond=0) |
1265 | 1286 | def AddEvent(self, title, where, start, end, descr, who, reminders, color): |
1266 | 1287 | |
1267 | 1288 | if len(self.cals) != 1: |
1268 | # TODO: get a better name for this exception class | |
1269 | # and use it elsewhere | |
1270 | raise GcalcliError('You must only specify a single calendar\n') | |
1289 | # Calendar not specified. Prompt the user to select it | |
1290 | writers = (self.ACCESS_OWNER, self.ACCESS_WRITER) | |
1291 | cals_with_write_perms = [cal for cal in self.cals | |
1292 | if cal['accessRole'] in writers] | |
1293 | ||
1294 | cal_names_with_idx = [] | |
1295 | for idx, cal in enumerate(cals_with_write_perms): | |
1296 | cal_names_with_idx.append(str(idx) + ' ' + cal['summary']) | |
1297 | cal_names_with_idx = '\n'.join(cal_names_with_idx) | |
1298 | print(cal_names_with_idx) | |
1299 | val = get_input(self.printer, 'Specify calendar from above: ', | |
1300 | STR_TO_INT) | |
1301 | try: | |
1302 | self.cals = [cals_with_write_perms[int(val)]] | |
1303 | except IndexError: | |
1304 | raise GcalcliError('The entered number doesn\'t appear on the ' | |
1305 | 'list above\n') | |
1271 | 1306 | |
1272 | 1307 | event = {} |
1273 | 1308 | event['summary'] = title |
114 | 114 | def days_since_epoch(dt): |
115 | 115 | __DAYS_IN_SECONDS__ = 24 * 60 * 60 |
116 | 116 | return calendar.timegm(dt.timetuple()) / __DAYS_IN_SECONDS__ |
117 | ||
118 | ||
119 | def agenda_time_fmt(dt, military): | |
120 | hour_min_fmt = '%H:%M' if military else '%I:%M' | |
121 | ampm = '' if military else dt.strftime('%p').lower() | |
122 | return dt.strftime(hour_min_fmt).lstrip('0') + ampm |
0 | 0 | from gcalcli import argparsers |
1 | from collections import namedtuple | |
1 | 2 | import shlex |
2 | 3 | import pytest |
3 | 4 | |
18 | 19 | assert len(remind_parser.parse_args(argv).reminders) == 1 |
19 | 20 | |
20 | 21 | |
21 | def test_output_parser(): | |
22 | def test_output_parser(monkeypatch): | |
23 | def sub_terminal_size(columns): | |
24 | ts = namedtuple('terminal_size', ['lines', 'columns']) | |
25 | ||
26 | def fake_get_terminal_size(): | |
27 | return ts(123, columns) | |
28 | ||
29 | return fake_get_terminal_size | |
30 | ||
22 | 31 | output_parser = argparsers.get_output_parser() |
23 | 32 | argv = shlex.split('-w 9') |
24 | 33 | with pytest.raises(SystemExit): |
27 | 36 | argv = shlex.split('-w 10') |
28 | 37 | assert output_parser.parse_args(argv).cal_width == 10 |
29 | 38 | |
39 | argv = shlex.split('') | |
40 | monkeypatch.setattr(argparsers, 'get_terminal_size', sub_terminal_size(70)) | |
41 | output_parser = argparsers.get_output_parser() | |
42 | assert output_parser.parse_args(argv).cal_width == 10 | |
43 | ||
44 | argv = shlex.split('') | |
45 | monkeypatch.setattr(argparsers, 'get_terminal_size', | |
46 | sub_terminal_size(100)) | |
47 | output_parser = argparsers.get_output_parser() | |
48 | assert output_parser.parse_args(argv).cal_width == 13 | |
49 | ||
30 | 50 | |
31 | 51 | def test_search_parser(): |
32 | 52 | search_parser = argparsers.get_search_parser() |
34 | 54 | search_parser.parse_args([]) |
35 | 55 | |
36 | 56 | |
57 | def test_updates_parser(): | |
58 | updates_parser = argparsers.get_updates_parser() | |
59 | ||
60 | argv = shlex.split('2019-07-18 2019-08-01 2019-09-01') | |
61 | parsed_updates = updates_parser.parse_args(argv) | |
62 | assert parsed_updates.since | |
63 | assert parsed_updates.start | |
64 | assert parsed_updates.end | |
65 | ||
66 | ||
37 | 67 | def test_details_parser(): |
38 | 68 | details_parser = argparsers.get_details_parser() |
39 | 69 | |
40 | argv = shlex.split('--details attendees --details url --details location') | |
70 | argv = shlex.split('--details attendees --details url ' | |
71 | '--details location --details end') | |
41 | 72 | parsed_details = details_parser.parse_args(argv).details |
42 | 73 | assert parsed_details['attendees'] |
43 | 74 | assert parsed_details['location'] |
44 | 75 | assert parsed_details['url'] |
76 | assert parsed_details['end'] | |
45 | 77 | |
46 | 78 | argv = shlex.split('--details all') |
47 | 79 | parsed_details = details_parser.parse_args(argv).details |
10 | 10 | get_color_parser, |
11 | 11 | get_cal_query_parser, |
12 | 12 | get_output_parser, |
13 | get_updates_parser, | |
13 | 14 | get_search_parser) |
14 | 15 | from gcalcli.gcal import GoogleCalendarInterface |
15 | 16 | from gcalcli.cli import parse_cal_names |
47 | 48 | |
48 | 49 | opts = get_start_end_parser().parse_args(['today', 'tomorrow']) |
49 | 50 | assert PatchedGCalI().AgendaQuery(start=opts.start, end=opts.end) == 0 |
51 | ||
52 | ||
53 | def test_updates(PatchedGCalI): | |
54 | since = datetime(2019, 7, 10) | |
55 | assert PatchedGCalI().UpdatesQuery(since) == 0 | |
56 | ||
57 | opts = get_updates_parser().parse_args( | |
58 | ['2019-07-10', '2019-07-19', '2019-08-01']) | |
59 | assert PatchedGCalI().UpdatesQuery( | |
60 | last_updated_datetime=opts.since, | |
61 | start=opts.start, | |
62 | end=opts.end) == 0 | |
50 | 63 | |
51 | 64 | |
52 | 65 | def test_cal_query(capsys, PatchedGCalI): |