Codebase list gcalcli / f86bc70
Update upstream source from tag 'v4.2.0' Update to upstream version '4.2.0' with Debian dir fc96c1caf50dde220382a4d6b7c2475fe84a30ba Unit 193 4 years ago
11 changed file(s) with 178 addition(s) and 31 deletion(s). Raw diff Collapse all Expand all
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
06 v4.1.1
17 * Fixed regression on now marking
28 * Fixed version string management
55
66 gcalcli is a Python application that allows you to access your Google
77 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.
1212
1313 gcalcli uses the [Google Calendar API version 3](https://developers.google.com/google-apps/calendar/).
1414
7272 * OAuth2 authention with your Google account
7373 * list your calendars
7474 * 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
7576 * ascii text graphical calendar display with variable width
7677 * search for past and/or future events
7778 * "quick add" new events to a specified calendar
109110 list list available calendars
110111 edit edit calendar events
111112 agenda get an agenda for a time period
113 updates get updates since a datetime for a time period
112114 calw get a week-based agenda in calendar format
113115 calm get a month agenda in calendar format
114116 quick quick-add an event to a calendar
2727
2828
2929 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}
3131 Invoking a subcommand with --help prints subcommand
3232 usage.
3333 list list available calendars
3434 edit edit calendar events
3535 agenda get an agenda for a time period
36 updates get updates since a datetime for a time period
3637 calw get a week-based agenda in calendar format
3738 calm get a month agenda in calendar format
3839 quick quick-add an event to a calendar
00 __program__ = 'gcalcli'
1 __version__ = 'v4.1.1'
1 __version__ = 'v4.2.0'
22 __author__ = 'Eric Davis, Brian Hartvigsen, Joshua Crowgey'
33 __API_CLIENT_ID__ = '232867676714.apps.googleusercontent.com'
44 __API_CLIENT_SECRET__ = '3tZSxItw6_VnZMezQwC8lUqy'
44 from gcalcli.deprecations import parser_allow_deprecated, DeprecatedStoreTrue
55 from gcalcli.printer import valid_color_name
66 from oauth2client import tools
7 from shutil import get_terminal_size
78 import copy as _copy
9 import datetime
10 import locale
811
912 DETAILS = ['calendar', 'location', 'length', 'reminders', 'description',
10 'url', 'attendees', 'email', 'attachments']
13 'url', 'attendees', 'email', 'attachments', 'end']
1114
1215
1316 PROGRAM_OPTIONS = {
4750 'help': 'Choose line art style for calendars: ' +
4851 '"fancy": for VTcodes, "unicode" for ' +
4952 'Unicode box drawing characters, "ascii" ' +
50 'for old-school plusses, hyphens and pipes.'}
53 'for old-school plusses, hyphens and pipes.'},
5154 }
5255
5356
8891 return details_parser
8992
9093
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
91106 def get_output_parser(parents=[]):
92107 output_parser = argparse.ArgumentParser(add_help=False, parents=parents)
93108 output_parser.add_argument(
99114 output_parser.add_argument(
100115 '--nodeclined', action='store_true', dest='ignore_declined',
101116 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,
107124 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')
108128 output_parser.add_argument(
109129 '--override-color', action='store_true', default=False,
110130 help='Use overridden color for event')
166186 return cal_query_parser
167187
168188
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
169199 def get_start_end_parser():
170200 se_parser = argparse.ArgumentParser(add_help=False)
171201 se_parser.add_argument('start', type=utils.get_time_from_str, nargs='?')
220250
221251 remind_parser = get_remind_parser()
222252 cal_query_parser = get_cal_query_parser()
253 updates_parser = get_updates_parser()
223254
224255 # parsed start and end times
225256 start_end_parser = get_start_end_parser()
259290 parents=[details_parser, output_parser, start_end_parser],
260291 help='get an agenda for a time period',
261292 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).')
262301
263302 calw = sub.add_parser(
264303 'calw', parents=[details_parser, output_parser, cal_query_parser],
153153 elif parsed_args.command == 'agenda':
154154 gcal.AgendaQuery(start=parsed_args.start, end=parsed_args.end)
155155
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
156162 elif parsed_args.command == 'calw':
157163 gcal.CalQuery(
158164 parsed_args.command, count=parsed_args.weeks,
55 def __init__(self, message):
66 super(ValidationError, self).__init__(message)
77 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 )
99 import sys
1010 from unicodedata import east_asian_width
1111
12 from dateutil.relativedelta import relativedelta
1213 from datetime import datetime, timedelta, date
1314 from gcalcli import __program__, __version__
1415 from gcalcli import utils
653654 indent = 10 * ' '
654655 details_indent = 19 * ' '
655656
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
665657 if not prefix:
666658 prefix = indent
667
668659 self.printer.msg(prefix, self.options['color_date'])
669660
670661 happening_now = event['s'] <= self.now <= event['e']
679670 if happening_now and not all_day \
680671 else self._calendar_color(event)
681672
673 time_width = '%-5s' if self.options['military'] else '%-7s'
682674 if all_day:
683 fmt = ' ' + time_format + ' %s\n'
675 fmt = ' ' + time_width + ' %s\n'
684676 self.printer.msg(
685677 fmt % ('', self._valid_title(event).strip()),
686678 event_color
687679 )
688680 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
690691 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()),
692694 event_color
693695 )
694696
11651167
11661168 return self._display_queried_events(start, end, search_text, True)
11671169
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
11681189 def AgendaQuery(self, start=None, end=None):
11691190 if not start:
11701191 start = self.now.replace(hour=0, minute=0, second=0, microsecond=0)
12651286 def AddEvent(self, title, where, start, end, descr, who, reminders, color):
12661287
12671288 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')
12711306
12721307 event = {}
12731308 event['summary'] = title
114114 def days_since_epoch(dt):
115115 __DAYS_IN_SECONDS__ = 24 * 60 * 60
116116 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
00 from gcalcli import argparsers
1 from collections import namedtuple
12 import shlex
23 import pytest
34
1819 assert len(remind_parser.parse_args(argv).reminders) == 1
1920
2021
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
2231 output_parser = argparsers.get_output_parser()
2332 argv = shlex.split('-w 9')
2433 with pytest.raises(SystemExit):
2736 argv = shlex.split('-w 10')
2837 assert output_parser.parse_args(argv).cal_width == 10
2938
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
3050
3151 def test_search_parser():
3252 search_parser = argparsers.get_search_parser()
3454 search_parser.parse_args([])
3555
3656
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
3767 def test_details_parser():
3868 details_parser = argparsers.get_details_parser()
3969
40 argv = shlex.split('--details attendees --details url --details location')
70 argv = shlex.split('--details attendees --details url '
71 '--details location --details end')
4172 parsed_details = details_parser.parse_args(argv).details
4273 assert parsed_details['attendees']
4374 assert parsed_details['location']
4475 assert parsed_details['url']
76 assert parsed_details['end']
4577
4678 argv = shlex.split('--details all')
4779 parsed_details = details_parser.parse_args(argv).details
1010 get_color_parser,
1111 get_cal_query_parser,
1212 get_output_parser,
13 get_updates_parser,
1314 get_search_parser)
1415 from gcalcli.gcal import GoogleCalendarInterface
1516 from gcalcli.cli import parse_cal_names
4748
4849 opts = get_start_end_parser().parse_args(['today', 'tomorrow'])
4950 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
5063
5164
5265 def test_cal_query(capsys, PatchedGCalI):