Codebase list gcalcli / 0fadafc
Update upstream source from tag 'v4.2.1' Update to upstream version '4.2.1' with Debian dir 2f8555b6b4649e8579c7637ee36788b942d8fa3c Unit 193 4 years ago
21 changed file(s) with 422 addition(s) and 149 deletion(s). Raw diff Collapse all Expand all
22 python:
33 - "3.5"
44 - "3.6"
5 - "3.7"
56 install: pip install tox-travis
67 script: tox
0 v4.2.1
1 * Remove python2 support
2 * Allow flexible notion for durations (flicken)
3 * new `conflicts` command (flicken)
4 * Fixed issue when locale.nl_langinfo isn't available
5 * Fixed IndexError when attendee cannot be found in _DeclinedEvent (navignaw)
6
07 v4.2.0
18 * Prompt user for calendar on `add' when it isn't specified
29 * Add `end' time to details view
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, see recently updated
9 events, and even import those annoying ICS/vCal invites from Microsoft
8 events, add new events, delete events, edit events, see recently updated
9 events, and even import those annoying ICS/vCal invites from Microsoft
1010 Exchange and/or other sources. Additionally, gcalcli can be used as a reminder
1111 service and execute any application you want when an event is coming up.
1212
1515 Requirements
1616 ------------
1717
18 * [Python](http://www.python.org) (2.7, 3+)
18 * [Python3](http://www.python.org)
1919 * [dateutil](http://www.labix.org/python-dateutil)
2020 * [Google API Client](https://developers.google.com/api-client-library/python)
2121 * [httplib2](https://github.com/httplib2/httplib2)
2222 * [oauth2client](https://github.com/google/oauth2client)
23 * [six](https://pythonhosted.org/six/)
2423 * [parsedatetime](https://github.com/bear/parsedatetime)
2524 * A love for the command line!
2625
7372 * list your calendars
7473 * show an agenda using a specified start/end date and time
7574 * show updates since a specified datetime for events between a start/end date and time
75 * find conflicts between events matching search term
7676 * ascii text graphical calendar display with variable width
7777 * search for past and/or future events
7878 * "quick add" new events to a specified calendar
139139 you put here isn't important. It's just what will show up when gcalcli opens
140140 up the OAuth website. Anything optional can safely be left blank.
141141 * Go back to the credentials page and grab your ID and Secret.
142 * If desired, add the client_id and client_secret to your .gcalclirc:
143
144 --client_id=xxxxxxxxxxxxxxx.apps.googleusercontent.com
145 --client_secret=xxxxxxxxxxxxxxxxx
142 * If desired, add the client-id and client-secret to your .gcalclirc:
143
144 --client-id=xxxxxxxxxxxxxxx.apps.googleusercontent.com
145 --client-secret=xxxxxxxxxxxxxxxxx
146146
147147 * Remove your existing OAuth information (typically ~/.gcalcli_oauth).
148 * Run gcalcli with any desired argument, making sure the new client_id and
149 client_secret are passed on the command line or placed in your .gcalclirc. The
148 * Run gcalcli with any desired argument, making sure the new client-id and
149 client-secret are passed on the command line or placed in your .gcalclirc. The
150150 OAuth authorization page should be opened automatically in your default
151151 browser.
152152
2727
2828
2929 Positional arguments:
30 {list,search,edit,delete,agenda,updates,calw,calm,quick,add,import,remind}
30 {list,search,edit,delete,agenda,updates,conflicts,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
3636 updates get updates since a datetime for a time period
37 conflicts find conflicts between events matching search term
3738 calw get a week-based agenda in calendar format
3839 calm get a month agenda in calendar format
3940 quick quick-add an event to a calendar
00 __program__ = 'gcalcli'
1 __version__ = 'v4.2.0'
1 __version__ = 'v4.2.1'
22 __author__ = 'Eric Davis, Brian Hartvigsen, Joshua Crowgey'
33 __API_CLIENT_ID__ = '232867676714.apps.googleusercontent.com'
44 __API_CLIENT_SECRET__ = '3tZSxItw6_VnZMezQwC8lUqy'
9393
9494 def locale_has_24_hours():
9595 t = datetime.time(20)
96 formatted = t.strftime(locale.nl_langinfo(locale.T_FMT))
97 return '20' in formatted
96 try:
97 formatted = t.strftime(locale.nl_langinfo(locale.T_FMT))
98 return '20' in formatted
99 except AttributeError:
100 # Some locales don't support nl_langinfo (see #481)
101 return False
98102
99103
100104 def get_auto_width():
196200 return updates_parser
197201
198202
203 def get_conflicts_parser():
204 # optional search text, start and end filters
205 conflicts_parser = argparse.ArgumentParser(add_help=False)
206 conflicts_parser.add_argument('text', nargs='?', type=str)
207 conflicts_parser.add_argument(
208 'start', type=utils.get_time_from_str, nargs='?')
209 conflicts_parser.add_argument(
210 'end', type=utils.get_time_from_str, nargs='?')
211 return conflicts_parser
212
213
199214 def get_start_end_parser():
200215 se_parser = argparse.ArgumentParser(add_help=False)
201216 se_parser.add_argument('start', type=utils.get_time_from_str, nargs='?')
206221 def get_search_parser():
207222 # requires search text, optional start and end filters
208223 search_parser = argparse.ArgumentParser(add_help=False)
209 search_parser.add_argument('text', nargs=1, type=utils._u)
224 search_parser.add_argument('text', nargs=1)
210225 search_parser.add_argument(
211226 'start', type=utils.get_time_from_str, nargs='?')
212227 search_parser.add_argument('end', type=utils.get_time_from_str, nargs='?')
251266 remind_parser = get_remind_parser()
252267 cal_query_parser = get_cal_query_parser()
253268 updates_parser = get_updates_parser()
269 conflicts_parser = get_conflicts_parser()
254270
255271 # parsed start and end times
256272 start_end_parser = get_start_end_parser()
298314 '(defaults to through end of current month)',
299315 description='Get updates since a datetime for a time period '
300316 '(default to through end of current month).')
317
318 sub.add_parser(
319 'conflicts',
320 parents=[details_parser, output_parser, conflicts_parser],
321 help='find event conflicts',
322 description='Find conflicts between events matching search term '
323 '(default from now through 30 days into futures)')
301324
302325 calw = sub.add_parser(
303326 'calw', parents=[details_parser, output_parser, cal_query_parser],
1818 # Everything you need to know (Google API Calendar v3): http://goo.gl/HfTGQ #
1919 # #
2020 #############################################################################
21 from __future__ import absolute_import, print_function
22
2321 import os
2422 import signal
2523 import sys
3028 from gcalcli.exceptions import GcalcliError
3129 from gcalcli.gcal import GoogleCalendarInterface
3230 from gcalcli.printer import Printer, valid_color_name
33 from gcalcli.utils import _u
3431 from gcalcli.validators import (
35 PARSABLE_DATE, REMINDER, STR_ALLOW_EMPTY, STR_NOT_EMPTY, STR_TO_INT,
36 get_input
32 PARSABLE_DATE, REMINDER, STR_ALLOW_EMPTY, STR_NOT_EMPTY,
33 PARSABLE_DURATION, get_input
3734 )
3835
3936 CalName = namedtuple('CalName', ['name', 'color'])
7067 if parsed_args.allday:
7168 prompt = 'Duration (days): '
7269 else:
73 prompt = 'Duration (minutes): '
74 parsed_args.duration = get_input(printer, prompt, STR_TO_INT)
70 prompt = 'Duration (human readable): '
71 parsed_args.duration = get_input(printer, prompt, PARSABLE_DURATION)
7572 if parsed_args.description is None:
7673 parsed_args.description = get_input(
7774 printer, 'Description: ', STR_ALLOW_EMPTY)
159156 start=parsed_args.start,
160157 end=parsed_args.end)
161158
159 elif parsed_args.command == 'conflicts':
160 gcal.ConflictsQuery(
161 search_text=parsed_args.text,
162 start=parsed_args.start,
163 end=parsed_args.end)
164
162165 elif parsed_args.command == 'calw':
163166 gcal.CalQuery(
164167 parsed_args.command, count=parsed_args.weeks,
175178
176179 # allow unicode strings for input
177180 gcal.QuickAddEvent(
178 _u(parsed_args.text), reminders=parsed_args.reminders
181 parsed_args.text, reminders=parsed_args.reminders
179182 )
180183
181184 elif parsed_args.command == 'add':
0
1 class ShowConflicts:
2 active_events = []
3
4 def __init__(self, show):
5 if show:
6 self.show = show
7 else:
8 self.show = self._default_show
9
10 def show_conflicts(self, latest_event):
11 """Events must be passed in chronological order"""
12 start = latest_event['s']
13 for event in self.active_events:
14 if (event['e'] > start):
15 self.show(event)
16 self.active_events = list(
17 filter(lambda e: e['e'] > start, self.active_events))
18 self.active_events.append(latest_event)
19
20 def _default_show(self, e):
21 print(e)
0 from __future__ import absolute_import
10 import argparse
21 import functools
32
0 from __future__ import absolute_import
1
20 import os
31 import re
42 import shlex
3 import httplib2
54 import time
65 import textwrap
76 import json
87 import random
98 import sys
109 from unicodedata import east_asian_width
10 try:
11 import cPickle as pickle
12 except Exception:
13 import pickle
14
15 from gcalcli import __program__, __version__
16 from gcalcli import utils
17 from gcalcli.utils import days_since_epoch
18 from gcalcli.validators import (
19 get_input, get_override_color_id, STR_NOT_EMPTY, PARSABLE_DATE, STR_TO_INT,
20 VALID_COLORS, STR_ALLOW_EMPTY, REMINDER, PARSABLE_DURATION
21 )
22 from gcalcli.exceptions import GcalcliError
23 from gcalcli.printer import Printer
24 from gcalcli.conflicts import ShowConflicts
1125
1226 from dateutil.relativedelta import relativedelta
1327 from datetime import datetime, timedelta, date
14 from gcalcli import __program__, __version__
15 from gcalcli import utils
16 from gcalcli.utils import days_since_epoch, _u
17
18 from gcalcli.validators import (
19 get_input, get_override_color_id, STR_NOT_EMPTY, PARSABLE_DATE, STR_TO_INT,
20 VALID_COLORS, STR_ALLOW_EMPTY, REMINDER
21 )
22
23 from gcalcli.exceptions import GcalcliError
24 from gcalcli.printer import Printer
2528 from dateutil.tz import tzlocal
2629 from dateutil.parser import parse
27 import httplib2
28 from six import next
29 from six.moves import input, range, zip, map, cPickle as pickle
3030 from apiclient.discovery import build
3131 from apiclient.errors import HttpError
3232 from oauth2client.file import Storage
3333 from oauth2client.client import OAuth2WebServerFlow
3434 from oauth2client import tools
35
3635 from collections import namedtuple
3736
3837 EventTitle = namedtuple('EventTitle', ['title', 'color'])
4443 all_cals = []
4544 now = datetime.now(tzlocal())
4645 agenda_length = 5
46 conflicts_lookahead_days = 30
4747 max_retries = 5
4848 auth_http = None
4949 cal_service = None
390390 # so we convert them to unicode and then check their size. Fixes
391391 # the output issues we were seeing around non-US locale strings
392392 return sum(
393 self.UNIWIDTH[east_asian_width(char)] for char in _u(string)
393 self.UNIWIDTH[east_asian_width(char)] for char in string
394394 )
395395
396396 def _word_cut(self, word):
403403 def _next_cut(self, string):
404404 print_len = 0
405405
406 words = _u(string).split()
406 words = string.split()
407407 word_lens = []
408408 for i, word in enumerate(words):
409409 word_lens.append(self._printed_len(word))
591591 continue
592592 if self.options['ignore_declined'] and self._DeclinedEvent(event):
593593 continue
594 output = '%s\t%s\t%s\t%s' % (_u(event['s'].strftime('%Y-%m-%d')),
595 _u(event['s'].strftime('%H:%M')),
596 _u(event['e'].strftime('%Y-%m-%d')),
597 _u(event['e'].strftime('%H:%M')))
594 output = '%s\t%s\t%s\t%s' % (event['s'].strftime('%Y-%m-%d'),
595 event['s'].strftime('%H:%M'),
596 event['e'].strftime('%Y-%m-%d'),
597 event['e'].strftime('%H:%M'))
598598
599599 if self.details.get('url'):
600600 output += '\t%s' % (event['htmlLink']
602602 output += '\t%s' % (event['hangoutLink']
603603 if 'hangoutLink' in event else '')
604604
605 output += '\t%s' % _u(self._valid_title(event).strip())
605 output += '\t%s' % self._valid_title(event).strip()
606606
607607 if self.details.get('location'):
608 output += '\t%s' % (_u(event['location'].strip())
608 output += '\t%s' % (event['location'].strip()
609609 if 'location' in event else '')
610610
611611 if self.details.get('description'):
612 output += '\t%s' % (_u(event['description'].strip())
612 output += '\t%s' % (event['description'].strip()
613613 if 'description' in event else '')
614614
615615 if self.details.get('calendar'):
616 output += '\t%s' % _u(event['gcalcli_cal']['summary'].strip())
616 output += '\t%s' % event['gcalcli_cal']['summary'].strip()
617617
618618 if self.details.get('email'):
619619 output += '\t%s' % (event['creator']['email'].strip()
620620 if 'email' in event['creator'] else '')
621621
622622 output = '%s\n' % output.replace('\n', '''\\n''')
623 sys.stdout.write(_u(output))
623 sys.stdout.write(output)
624624
625625 def _PrintEvent(self, event, prefix):
626626
953953
954954 elif val.lower() == 'g':
955955 val = get_input(
956 self.printer, 'Length (mins): ', STR_TO_INT
956 self.printer, 'Length (mins or human readable): ',
957 PARSABLE_DURATION
957958 )
958959 if val:
959960 all_day = self.options.get('allday')
11191120
11201121 def _DeclinedEvent(self, event):
11211122 if 'attendees' in event:
1122 attendee = [a for a in event['attendees']
1123 if a['email'] == event['gcalcli_cal']['id']][0]
1124 if attendee and attendee['responseStatus'] == 'declined':
1123 attendees = [a for a in event['attendees']
1124 if a['email'] == event['gcalcli_cal']['id']]
1125 if attendees and attendees[0]['responseStatus'] == 'declined':
11251126 return True
11261127 return False
11271128
11851186 "until",
11861187 end)
11871188 return self._iterate_events(start, event_list, year_date=False)
1189
1190 def ConflictsQuery(self, search_text='', start=None, end=None):
1191 if not start:
1192 start = self.now.replace(hour=0, minute=0, second=0, microsecond=0)
1193
1194 if not end:
1195 end = (start + timedelta(days=self.conflicts_lookahead_days))
1196
1197 event_list = self._search_for_events(start, end, search_text)
1198 show_conflicts = ShowConflicts(
1199 lambda e: self._PrintEvent(e, "\t !!! Conflict: "))
1200
1201 return self._iterate_events(start,
1202 event_list,
1203 year_date=False,
1204 work=show_conflicts.show_conflicts)
11881205
11891206 def AgendaQuery(self, start=None, end=None):
11901207 if not start:
15791596 continue
15801597 if val.lower() == 'i':
15811598 new_event = self._retry_with_backoff(
1582 self._cal_service()
1599 self.get_cal_service()
15831600 .events()
15841601 .insert(
15851602 calendarId=self.cals[0]['id'],
0 from __future__ import absolute_import
10 import argparse
21 import sys
3 from gcalcli.utils import _u
42
53 COLOR_NAMES = set(('default', 'black', 'red', 'green', 'yellow', 'blue',
64 'magenta', 'cyan', 'white', 'brightblack', 'brightred',
2018 'bte': '\033(0\x76\033(B',
2119 'ute': '\033(0\x77\033(B'},
2220 'unicode': {
23 'hrz': _u(b'\xe2\x94\x80'),
24 'vrt': _u(b'\xe2\x94\x82'),
25 'lrc': _u(b'\xe2\x94\x98'),
26 'urc': _u(b'\xe2\x94\x90'),
27 'ulc': _u(b'\xe2\x94\x8c'),
28 'llc': _u(b'\xe2\x94\x94'),
29 'crs': _u(b'\xe2\x94\xbc'),
30 'lte': _u(b'\xe2\x94\x9c'),
31 'rte': _u(b'\xe2\x94\xa4'),
32 'bte': _u(b'\xe2\x94\xb4'),
33 'ute': _u(b'\xe2\x94\xac')},
21 'hrz': b'\xe2\x94\x80',
22 'vrt': b'\xe2\x94\x82',
23 'lrc': b'\xe2\x94\x98',
24 'urc': b'\xe2\x94\x90',
25 'ulc': b'\xe2\x94\x8c',
26 'llc': b'\xe2\x94\x94',
27 'crs': b'\xe2\x94\xbc',
28 'lte': b'\xe2\x94\x9c',
29 'rte': b'\xe2\x94\xa4',
30 'bte': b'\xe2\x94\xb4',
31 'ute': b'\xe2\x94\xac'},
3432 'ascii': {
3533 'hrz': '-',
3634 'vrt': '|',
8785 def msg(self, msg, colorname='default', file=sys.stdout):
8886 if self.use_color:
8987 msg = self.colors[colorname] + msg + self.colors['default']
90 file.write(_u(msg))
88 file.write(msg)
9189
9290 def err_msg(self, msg):
9391 self.msg(msg, 'brightred', file=sys.stderr)
00 import calendar
11 import time
22 import locale
3 import six
43 import re
54 from dateutil.tz import tzlocal
65 from dateutil.parser import parse as dateutil_parse
109
1110 locale.setlocale(locale.LC_ALL, '')
1211 fuzzy_date_parse = Calendar().parse
12 fuzzy_datetime_parse = Calendar().parseDT
13
1314
1415 REMINDER_REGEX = r'^(\d+)([wdhm]?)(?:\s+(popup|email|sms))?$'
16
17 DURATION_REGEX = re.compile(
18 r'^((?P<days>[\.\d]+?)(?:d|day|days))?[ :]*'
19 r'((?P<hours>[\.\d]+?)(?:h|hour|hours))?[ :]*'
20 r'((?P<minutes>[\.\d]+?)(?:m|min|mins|minute|minutes))?[ :]*'
21 r'((?P<seconds>[\.\d]+?)(?:s|sec|secs|second|seconds))?$'
22 )
1523
1624
1725 def parse_reminder(rem):
4452 '!\n Check supported locales of your system.\n')
4553
4654
47 def _u(text):
48 encoding = locale.getlocale()[1] or \
49 locale.getpreferredencoding(False) or 'UTF-8'
50 if issubclass(type(text), six.text_type):
51 return text
52 if not issubclass(type(text), six.string_types):
53 if six.PY3:
54 if isinstance(text, bytes):
55 return six.text_type(text, encoding, 'replace')
56 else:
57 return six.text_type(text)
58 elif hasattr(text, '__unicode__'):
59 return six.text_type(text)
60 else:
61 return six.text_type(bytes(text), encoding, 'replace')
62 else:
63 return text.decode(encoding, 'replace')
64
65
6655 def get_times_from_duration(when, duration=0, allday=False):
6756
6857 try:
8271
8372 else:
8473 try:
85 stop = start + timedelta(minutes=float(duration))
74 stop = start + get_timedelta_from_str(duration)
8675 except Exception:
8776 raise ValueError(
88 'Duration time (minutes) is invalid: %s\n' % (duration))
77 'Duration time is invalid: %s\n' % (duration))
8978
9079 start = start.isoformat()
9180 stop = stop.isoformat()
111100 return event_time
112101
113102
103 def get_timedelta_from_str(delta):
104 """
105 Parse a time string a timedelta object.
106 Formats:
107 - number -> duration in minutes
108 - "1:10" -> hour and minutes
109 - "1d 1h 1m" -> days, hours, minutes
110 Based on https://stackoverflow.com/a/51916936/12880
111 """
112 parsed_delta = None
113 try:
114 parsed_delta = timedelta(minutes=float(delta))
115 except ValueError:
116 pass
117 if parsed_delta is None:
118 parts = DURATION_REGEX.match(delta)
119 if parts is not None:
120 try:
121 time_params = {name: float(param)
122 for name, param
123 in parts.groupdict().items() if param}
124 parsed_delta = timedelta(**time_params)
125 except ValueError:
126 pass
127 if parsed_delta is None:
128 dt, result = fuzzy_datetime_parse(delta, sourceTime=datetime.min)
129 if result:
130 parsed_delta = dt - datetime.min
131 if parsed_delta is None:
132 raise ValueError('Duration is invalid: %s' % (delta))
133 return parsed_delta
134
135
114136 def days_since_epoch(dt):
115137 __DAYS_IN_SECONDS__ = 24 * 60 * 60
116138 return calendar.timegm(dt.timetuple()) / __DAYS_IN_SECONDS__
0 from __future__ import absolute_import
1
20 import re
31
42 from gcalcli.exceptions import ValidationError
5 from gcalcli.utils import REMINDER_REGEX, get_time_from_str
6 from six.moves import input
3 from gcalcli.utils import (REMINDER_REGEX, get_time_from_str,
4 get_timedelta_from_str)
75
86 # TODO: in the future, pull these from the API
97 # https://developers.google.com/calendar/v3/reference/colors
7674 )
7775
7876
77 def parsable_duration_validator(input_str):
78 """
79 A filter allowing any duration string which can be parsed
80 by parsedatetime.
81 Raises ValidationError otherwise.
82 """
83 try:
84 get_timedelta_from_str(input_str)
85 return input_str
86 except ValueError:
87 raise ValidationError(
88 'Expected format: a duration (e.g. 1m, 1s, 1h3m)'
89 '(Ctrl-C to exit)\n'
90 )
91
92
7993 def str_allow_empty_validator(input_str):
8094 """
8195 A simple filter that allows any string to pass.
122136 STR_ALLOW_EMPTY = str_allow_empty_validator
123137 STR_TO_INT = str_to_int_validator
124138 PARSABLE_DATE = parsable_date_validator
139 PARSABLE_DURATION = parsable_duration_validator
125140 VALID_COLORS = color_validator
126141 REMINDER = reminder_validator
00 #!/usr/bin/env python
1 from __future__ import print_function
21 from setuptools import setup
32 from gcalcli import __version__
43
3433 'httplib2',
3534 'oauth2client',
3635 'parsedatetime',
37 'six'
3836 ],
3937 extras_require={
4038 'vobject': ["vobject"],
4846 "Environment :: Console",
4947 "Intended Audience :: End Users/Desktop",
5048 "License :: OSI Approved :: MIT License",
51 "Programming Language :: Python :: 2.7",
5249 "Programming Language :: Python :: 3",
5350 ])
6464 assert parsed_updates.end
6565
6666
67 def test_conflicts_parser():
68 updates_parser = argparsers.get_conflicts_parser()
69
70 argv = shlex.split('search 2019-08-01 2019-09-01')
71 parsed_conflicts = updates_parser.parse_args(argv)
72 assert parsed_conflicts.text
73 assert parsed_conflicts.start
74 assert parsed_conflicts.end
75
76
6777 def test_details_parser():
6878 details_parser = argparsers.get_details_parser()
6979
0 from gcalcli.conflicts import ShowConflicts
1 from datetime import datetime
2 from dateutil.tz import tzlocal
3
4 minimal_event = {
5 'e': datetime(2019, 1, 8, 15, 15, tzinfo=tzlocal()),
6 'id': 'minimial_event',
7 's': datetime(2019, 1, 8, 14, 15, tzinfo=tzlocal())
8 }
9 minimal_event_overlapping = {
10 'e': datetime(2019, 1, 8, 16, 15, tzinfo=tzlocal()),
11 'id': 'minimial_event_overlapping',
12 's': datetime(2019, 1, 8, 14, 30, tzinfo=tzlocal())
13 }
14 minimal_event_nonoverlapping = {
15 'e': datetime(2019, 1, 8, 16, 15, tzinfo=tzlocal()),
16 'id': 'minimal_event_nonoverlapping',
17 's': datetime(2019, 1, 8, 15, 30, tzinfo=tzlocal())
18 }
19
20
21 def test_finds_no_conflicts_for_one_event():
22 """Basic test that only ensures the function can be run without error"""
23 conflicts = []
24 show_conflicts = ShowConflicts(conflicts.append)
25 show_conflicts.show_conflicts(minimal_event)
26 assert conflicts == []
27
28
29 def test_finds_conflicts_for_second_overlapping_event():
30 conflicts = []
31 show_conflicts = ShowConflicts(conflicts.append)
32 show_conflicts.show_conflicts(minimal_event)
33 show_conflicts.show_conflicts(minimal_event_overlapping)
34 assert conflicts == [minimal_event]
35
36
37 def test_does_not_find_conflict_for_second_non_overlapping_event():
38 conflicts = []
39 show_conflicts = ShowConflicts(conflicts.append)
40 show_conflicts.show_conflicts(minimal_event)
41 show_conflicts.show_conflicts(minimal_event_nonoverlapping)
42 assert conflicts == []
55 from dateutil.tz import tzutc
66 from datetime import datetime
77
8 from gcalcli.utils import parse_reminder, _u
8 from gcalcli.utils import parse_reminder
99 from gcalcli.argparsers import (get_start_end_parser,
1010 get_color_parser,
1111 get_cal_query_parser,
1212 get_output_parser,
1313 get_updates_parser,
14 get_conflicts_parser,
1415 get_search_parser)
1516 from gcalcli.gcal import GoogleCalendarInterface
1617 from gcalcli.cli import parse_cal_names
3334
3435 gcal.ListAllCalendars()
3536 captured = capsys.readouterr()
36 assert captured.out.startswith(_u(expected_header))
37 assert captured.out.startswith(expected_header)
3738
3839 # +3 cos one for the header, one for the '----' decorations,
3940 # and one for the eom
5859 ['2019-07-10', '2019-07-19', '2019-08-01'])
5960 assert PatchedGCalI().UpdatesQuery(
6061 last_updated_datetime=opts.since,
62 start=opts.start,
63 end=opts.end) == 0
64
65
66 def test_conflicts(PatchedGCalI):
67 assert PatchedGCalI().ConflictsQuery() == 0
68
69 opts = get_conflicts_parser().parse_args(
70 ['search text', '2019-07-19', '2019-08-01'])
71 assert PatchedGCalI().ConflictsQuery(
72 'search text',
6173 start=opts.start,
6274 end=opts.end) == 0
6375
134146 assert gcal.TextQuery(opts.text, opts.start, opts.end) == 0
135147
136148
149 def test_declined_event_no_attendees(PatchedGCalI):
150 gcal = PatchedGCalI()
151 event = {
152 'gcalcli_cal': {
153 'id': 'user@email.com',
154 },
155 'attendees': []
156 }
157 assert not gcal._DeclinedEvent(event)
158
159
160 def test_declined_event_non_matching_attendees(PatchedGCalI):
161 gcal = PatchedGCalI()
162 event = {
163 'gcalcli_cal': {
164 'id': 'user@email.com',
165 },
166 'attendees': [{
167 'email': 'user2@otheremail.com',
168 'responseStatus': 'declined',
169 }]
170 }
171 assert not gcal._DeclinedEvent(event)
172
173
174 def test_declined_event_matching_attendee_declined(PatchedGCalI):
175 gcal = PatchedGCalI()
176 event = {
177 'gcalcli_cal': {
178 'id': 'user@email.com',
179 },
180 'attendees': [
181 {
182 'email': 'user@email.com',
183 'responseStatus': 'declined',
184 },
185 {
186 'email': 'user2@otheremail.com',
187 'responseStatus': 'accepted',
188 },
189 ]
190 }
191 assert gcal._DeclinedEvent(event)
192
193
194 def test_declined_event_matching_attendee_accepted(PatchedGCalI):
195 gcal = PatchedGCalI()
196 event = {
197 'gcalcli_cal': {
198 'id': 'user@email.com',
199 },
200 'attendees': [
201 {
202 'email': 'user@email.com',
203 'responseStatus': 'accepted',
204 },
205 {
206 'email': 'user2@otheremail.com',
207 'responseStatus': 'declined',
208 },
209 ]
210 }
211 assert not gcal._DeclinedEvent(event)
212
213
137214 def test_modify_event(PatchedGCalI):
138215 opts = get_search_parser().parse_args(['test'])
139216 gcal = PatchedGCalI(**vars(opts))
22 from gcalcli.validators import validate_input, ValidationError
33 from gcalcli.validators import (STR_NOT_EMPTY,
44 PARSABLE_DATE,
5 PARSABLE_DURATION,
56 STR_TO_INT,
67 STR_ALLOW_EMPTY,
78 REMINDER,
89 VALID_COLORS)
9 import gcalcli.validators
10
1110 # Tests required:
1211 #
1312 # * Title: any string, not blank
2120
2221 def test_any_string_not_blank_validator(monkeypatch):
2322 # Empty string raises ValidationError
24 monkeypatch.setattr(gcalcli.validators, "input", lambda: "")
23 monkeypatch.setattr("builtins.input", lambda: "")
2524 with pytest.raises(ValidationError):
2625 validate_input(STR_NOT_EMPTY) == ValidationError(
2726 "Input here cannot be empty")
2827
2928 # None raises ValidationError
30 monkeypatch.setattr(gcalcli.validators, "input", lambda: None)
29 monkeypatch.setattr("builtins.input", lambda: None)
3130 with pytest.raises(ValidationError):
3231 validate_input(STR_NOT_EMPTY) == ValidationError(
3332 "Input here cannot be empty")
3433
3534 # Valid string passes
36 monkeypatch.setattr(gcalcli.validators, "input", lambda: "Valid Text")
35 monkeypatch.setattr("builtins.input", lambda: "Valid Text")
3736 assert validate_input(STR_NOT_EMPTY) == "Valid Text"
3837
3938
4039 def test_any_string_parsable_by_dateutil(monkeypatch):
4140 # non-date raises ValidationError
42 monkeypatch.setattr(gcalcli.validators, "input", lambda: "NON-DATE STR")
41 monkeypatch.setattr("builtins.input", lambda: "NON-DATE STR")
4342 with pytest.raises(ValidationError):
4443 validate_input(PARSABLE_DATE) == ValidationError(
4544 "Expected format: a date (e.g. 2019-01-01, tomorrow 10am, "
4847 )
4948
5049 # date string passes
51 monkeypatch.setattr(gcalcli.validators, "input", lambda: "2nd January")
50 monkeypatch.setattr("builtins.input", lambda: "2nd January")
5251 validate_input(PARSABLE_DATE) == "2nd January"
52
53
54 def test_any_string_parsable_by_parsedatetime(monkeypatch):
55 # non-date raises ValidationError
56 monkeypatch.setattr("builtins.input", lambda: "NON-DATE STR")
57 with pytest.raises(ValidationError) as ve:
58 validate_input(PARSABLE_DURATION)
59 assert ve.value.message == (
60 'Expected format: a duration (e.g. 1m, 1s, 1h3m)'
61 '(Ctrl-C to exit)\n'
62 )
63
64 # duration string passes
65 monkeypatch.setattr("builtins.input", lambda: "1m")
66 assert validate_input(PARSABLE_DURATION) == "1m"
67
68 # duration string passes
69 monkeypatch.setattr("builtins.input", lambda: "1h2m")
70 assert validate_input(PARSABLE_DURATION) == "1h2m"
5371
5472
5573 def test_string_can_be_cast_to_int(monkeypatch):
5674 # non int-castable string raises ValidationError
57 monkeypatch.setattr(gcalcli.validators, "input", lambda: "X")
75 monkeypatch.setattr("builtins.input", lambda: "X")
5876 with pytest.raises(ValidationError):
5977 validate_input(STR_TO_INT) == ValidationError(
6078 "Input here must be a number")
6179
6280 # int string passes
63 monkeypatch.setattr(gcalcli.validators, "input", lambda: "10")
81 monkeypatch.setattr("builtins.input", lambda: "10")
6482 validate_input(STR_TO_INT) == "10"
6583
6684
6785 def test_for_valid_colour_name(monkeypatch):
6886 # non valid colour raises ValidationError
69 monkeypatch.setattr(gcalcli.validators, "input", lambda: "purple")
87 monkeypatch.setattr("builtins.input", lambda: "purple")
7088 with pytest.raises(ValidationError):
7189 validate_input(VALID_COLORS) == ValidationError(
7290 "purple is not a valid color value to use here. Please "
7492 "tomato, safe, flamingo or banana."
7593 )
7694 # valid colour passes
77 monkeypatch.setattr(gcalcli.validators, "input", lambda: "grape")
95 monkeypatch.setattr("builtins.input", lambda: "grape")
7896 validate_input(VALID_COLORS) == "grape"
7997
8098 # empty str passes
81 monkeypatch.setattr(gcalcli.validators, "input", lambda: "")
99 monkeypatch.setattr("builtins.input", lambda: "")
82100 validate_input(VALID_COLORS) == ""
83101
84102
85103 def test_any_string_and_blank(monkeypatch):
86104 # string passes
87 monkeypatch.setattr(gcalcli.validators, "input", lambda: "TEST")
105 monkeypatch.setattr("builtins.input", lambda: "TEST")
88106 validate_input(STR_ALLOW_EMPTY) == "TEST"
89107
90108
91109 def test_reminder(monkeypatch):
92110 # valid reminders pass
93 monkeypatch.setattr(gcalcli.validators, "input", lambda: "10m email")
111 monkeypatch.setattr("builtins.input", lambda: "10m email")
94112 validate_input(REMINDER) == "10m email"
95113
96 monkeypatch.setattr(gcalcli.validators, "input", lambda: "10 popup")
114 monkeypatch.setattr("builtins.input", lambda: "10 popup")
97115 validate_input(REMINDER) == "10m email"
98116
99 monkeypatch.setattr(gcalcli.validators, "input", lambda: "10m sms")
117 monkeypatch.setattr("builtins.input", lambda: "10m sms")
100118 validate_input(REMINDER) == "10m email"
101119
102 monkeypatch.setattr(gcalcli.validators, "input", lambda: "12323")
120 monkeypatch.setattr("builtins.input", lambda: "12323")
103121 validate_input(REMINDER) == "10m email"
104122
105123 # invalid reminder raises ValidationError
106 monkeypatch.setattr(gcalcli.validators, "input", lambda: "meaningless")
124 monkeypatch.setattr("builtins.input", lambda: "meaningless")
107125 with pytest.raises(ValidationError):
108126 validate_input(REMINDER) == ValidationError(
109127 "Format: <number><w|d|h|m> <popup|email|sms>\n")
110128
111129 # invalid reminder raises ValidationError
112 monkeypatch.setattr(gcalcli.validators, "input", lambda: "")
130 monkeypatch.setattr("builtins.input", lambda: "")
113131 with pytest.raises(ValidationError):
114132 validate_input(REMINDER) == ValidationError(
115133 "Format: <number><w|d|h|m> <popup|email|sms>\n")
33
44 import pytest
55 from gcalcli.printer import COLOR_NAMES, Printer, valid_color_name
6 from gcalcli.utils import _u
76
87
98 def test_init():
2120 cp = Printer()
2221 for color_name in COLOR_NAMES:
2322 out = StringIO()
24 cp.msg(_u('msg'), color_name, file=out)
23 cp.msg('msg', color_name, file=out)
2524 out.seek(0)
26 assert out.read() == _u(cp.colors[color_name] + 'msg' + '\033[0m')
25 assert out.read() == cp.colors[color_name] + 'msg' + '\033[0m'
2726
2827
2928 def test_red_msg():
3029 cp = Printer()
3130 out = StringIO()
32 cp.msg(_u('msg'), 'red', file=out)
31 cp.msg('msg', 'red', file=out)
3332 out.seek(0)
34 assert out.read() == _u('\033[0;31mmsg\033[0m')
33 assert out.read() == '\033[0;31mmsg\033[0m'
3534
3635
3736 def test_err_msg(monkeypatch):
3837 err = StringIO()
3938 monkeypatch.setattr(sys, 'stderr', err)
4039 cp = Printer()
41 cp.err_msg(_u('error'))
40 cp.err_msg('error')
4241 err.seek(0)
43 assert err.read() == _u('\033[31;1merror\033[0m')
42 assert err.read() == '\033[31;1merror\033[0m'
4443
4544
4645 def test_debug_msg(monkeypatch):
4746 err = StringIO()
4847 monkeypatch.setattr(sys, 'stderr', err)
4948 cp = Printer()
50 cp.debug_msg(_u('debug'))
49 cp.debug_msg('debug')
5150 err.seek(0)
52 assert err.read() == _u('\033[0;33mdebug\033[0m')
51 assert err.read() == '\033[0;33mdebug\033[0m'
5352
5453
5554 def test_conky_red_msg():
5655 cp = Printer(conky=True)
5756 out = StringIO()
58 cp.msg(_u('msg'), 'red', file=out)
57 cp.msg('msg', 'red', file=out)
5958 out.seek(0)
60 assert out.read() == _u('${color red}msg${color}')
59 assert out.read() == '${color red}msg${color}'
6160
6261
6362 def test_conky_err_msg(monkeypatch):
6463 err = StringIO()
6564 monkeypatch.setattr(sys, 'stderr', err)
6665 cp = Printer(conky=True)
67 cp.err_msg(_u('error'))
66 cp.err_msg('error')
6867 err.seek(0)
69 assert err.read() == _u('${color red}error${color}')
68 assert err.read() == '${color red}error${color}'
7069
7170
7271 def test_conky_debug_msg(monkeypatch):
7372 err = StringIO()
7473 monkeypatch.setattr(sys, 'stderr', err)
7574 cp = Printer(conky=True)
76 cp.debug_msg(_u('debug'))
75 cp.debug_msg('debug')
7776 err.seek(0)
78 assert err.read() == _u('${color yellow}debug${color}')
77 assert err.read() == '${color yellow}debug${color}'
7978
8079
8180 def test_no_color():
8281 cp = Printer(use_color=False)
8382 out = StringIO()
84 cp.msg(_u('msg'), 'red', file=out)
83 cp.msg('msg', 'red', file=out)
8584 out.seek(0)
86 assert out.read() == _u('msg')
85 assert out.read() == 'msg'
00 import gcalcli.utils as utils
1 from datetime import datetime
1 from datetime import datetime, timedelta
22 from dateutil.tz import UTC
3 import six
43 import pytest
54
65
76 def test_get_time_from_str():
87 assert utils.get_time_from_str('7am tomorrow')
8
9
10 def test_get_parsed_timedelta_from_str():
11 assert utils.get_timedelta_from_str('3.5h') == timedelta(
12 hours=3, minutes=30)
13 assert utils.get_timedelta_from_str('1') == timedelta(minutes=1)
14 assert utils.get_timedelta_from_str('1m') == timedelta(minutes=1)
15 assert utils.get_timedelta_from_str('1h') == timedelta(hours=1)
16 assert utils.get_timedelta_from_str('1h1m') == timedelta(
17 hours=1, minutes=1)
18 assert utils.get_timedelta_from_str('1:10') == timedelta(
19 hours=1, minutes=10)
20 assert utils.get_timedelta_from_str('2d:1h:3m') == timedelta(
21 days=2, hours=1, minutes=3)
22 assert utils.get_timedelta_from_str('2d 1h 3m 10s') == timedelta(
23 days=2, hours=1, minutes=3, seconds=10)
24 assert utils.get_timedelta_from_str(
25 '2 days 1 hour 2 minutes 40 seconds') == timedelta(
26 days=2, hours=1, minutes=2, seconds=40)
27 with pytest.raises(ValueError) as ve:
28 utils.get_timedelta_from_str('junk')
29 assert str(ve.value) == "Duration is invalid: junk"
930
1031
1132 def test_get_times_from_duration():
1536 next_day = '1970-01-02'
1637 assert (begin_1970_midnight, two_hrs_later) == \
1738 utils.get_times_from_duration(begin_1970_midnight, duration=120)
39
40 assert (begin_1970_midnight, two_hrs_later) == \
41 utils.get_times_from_duration(
42 begin_1970_midnight, duration="2h")
43
44 assert (begin_1970_midnight, two_hrs_later) == \
45 utils.get_times_from_duration(
46 begin_1970_midnight, duration="120m")
1847
1948 assert (begin_1970, next_day) == \
2049 utils.get_times_from_duration(
3766 assert utils.days_since_epoch(datetime(1970, 12, 31)) == 364
3867
3968
40 def test_u():
41 for text in [b'text', 'text', '\u309f', u'\xe1', b'\xff\xff', 42]:
42 if six.PY2:
43 assert isinstance(utils._u(text), unicode) # noqa: F821
44 else:
45 assert isinstance(utils._u(text), str)
46
47
4869 def test_set_locale():
4970 with pytest.raises(ValueError):
5071 utils.set_locale('not_a_real_locale')
00 [tox]
1 envlist = py35,py36
1 envlist = py35,py36,py37,py38
22
33 [testenv]
44 usedevelop=true