Update upstream source from tag 'v4.2.1'
Update to upstream version '4.2.1'
with Debian dir 2f8555b6b4649e8579c7637ee36788b942d8fa3c
Unit 193
4 years ago
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 | ||
0 | 7 | v4.2.0 |
1 | 8 | * Prompt user for calendar on `add' when it isn't specified |
2 | 9 | * Add `end' time to details view |
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, 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 | |
10 | 10 | Exchange and/or other sources. Additionally, gcalcli can be used as a reminder |
11 | 11 | service and execute any application you want when an event is coming up. |
12 | 12 | |
15 | 15 | Requirements |
16 | 16 | ------------ |
17 | 17 | |
18 | * [Python](http://www.python.org) (2.7, 3+) | |
18 | * [Python3](http://www.python.org) | |
19 | 19 | * [dateutil](http://www.labix.org/python-dateutil) |
20 | 20 | * [Google API Client](https://developers.google.com/api-client-library/python) |
21 | 21 | * [httplib2](https://github.com/httplib2/httplib2) |
22 | 22 | * [oauth2client](https://github.com/google/oauth2client) |
23 | * [six](https://pythonhosted.org/six/) | |
24 | 23 | * [parsedatetime](https://github.com/bear/parsedatetime) |
25 | 24 | * A love for the command line! |
26 | 25 | |
73 | 72 | * list your calendars |
74 | 73 | * show an agenda using a specified start/end date and time |
75 | 74 | * show updates since a specified datetime for events between a start/end date and time |
75 | * find conflicts between events matching search term | |
76 | 76 | * ascii text graphical calendar display with variable width |
77 | 77 | * search for past and/or future events |
78 | 78 | * "quick add" new events to a specified calendar |
139 | 139 | you put here isn't important. It's just what will show up when gcalcli opens |
140 | 140 | up the OAuth website. Anything optional can safely be left blank. |
141 | 141 | * 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 | |
146 | 146 | |
147 | 147 | * 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 | |
150 | 150 | OAuth authorization page should be opened automatically in your default |
151 | 151 | browser. |
152 | 152 |
27 | 27 | |
28 | 28 | |
29 | 29 | 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} | |
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 | 36 | updates get updates since a datetime for a time period |
37 | conflicts find conflicts between events matching search term | |
37 | 38 | calw get a week-based agenda in calendar format |
38 | 39 | calm get a month agenda in calendar format |
39 | 40 | quick quick-add an event to a calendar |
0 | 0 | __program__ = 'gcalcli' |
1 | __version__ = 'v4.2.0' | |
1 | __version__ = 'v4.2.1' | |
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' |
93 | 93 | |
94 | 94 | def locale_has_24_hours(): |
95 | 95 | 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 | |
98 | 102 | |
99 | 103 | |
100 | 104 | def get_auto_width(): |
196 | 200 | return updates_parser |
197 | 201 | |
198 | 202 | |
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 | ||
199 | 214 | def get_start_end_parser(): |
200 | 215 | se_parser = argparse.ArgumentParser(add_help=False) |
201 | 216 | se_parser.add_argument('start', type=utils.get_time_from_str, nargs='?') |
206 | 221 | def get_search_parser(): |
207 | 222 | # requires search text, optional start and end filters |
208 | 223 | 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) | |
210 | 225 | search_parser.add_argument( |
211 | 226 | 'start', type=utils.get_time_from_str, nargs='?') |
212 | 227 | search_parser.add_argument('end', type=utils.get_time_from_str, nargs='?') |
251 | 266 | remind_parser = get_remind_parser() |
252 | 267 | cal_query_parser = get_cal_query_parser() |
253 | 268 | updates_parser = get_updates_parser() |
269 | conflicts_parser = get_conflicts_parser() | |
254 | 270 | |
255 | 271 | # parsed start and end times |
256 | 272 | start_end_parser = get_start_end_parser() |
298 | 314 | '(defaults to through end of current month)', |
299 | 315 | description='Get updates since a datetime for a time period ' |
300 | 316 | '(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)') | |
301 | 324 | |
302 | 325 | calw = sub.add_parser( |
303 | 326 | 'calw', parents=[details_parser, output_parser, cal_query_parser], |
18 | 18 | # Everything you need to know (Google API Calendar v3): http://goo.gl/HfTGQ # |
19 | 19 | # # |
20 | 20 | ############################################################################# |
21 | from __future__ import absolute_import, print_function | |
22 | ||
23 | 21 | import os |
24 | 22 | import signal |
25 | 23 | import sys |
30 | 28 | from gcalcli.exceptions import GcalcliError |
31 | 29 | from gcalcli.gcal import GoogleCalendarInterface |
32 | 30 | from gcalcli.printer import Printer, valid_color_name |
33 | from gcalcli.utils import _u | |
34 | 31 | 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 | |
37 | 34 | ) |
38 | 35 | |
39 | 36 | CalName = namedtuple('CalName', ['name', 'color']) |
70 | 67 | if parsed_args.allday: |
71 | 68 | prompt = 'Duration (days): ' |
72 | 69 | 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) | |
75 | 72 | if parsed_args.description is None: |
76 | 73 | parsed_args.description = get_input( |
77 | 74 | printer, 'Description: ', STR_ALLOW_EMPTY) |
159 | 156 | start=parsed_args.start, |
160 | 157 | end=parsed_args.end) |
161 | 158 | |
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 | ||
162 | 165 | elif parsed_args.command == 'calw': |
163 | 166 | gcal.CalQuery( |
164 | 167 | parsed_args.command, count=parsed_args.weeks, |
175 | 178 | |
176 | 179 | # allow unicode strings for input |
177 | 180 | gcal.QuickAddEvent( |
178 | _u(parsed_args.text), reminders=parsed_args.reminders | |
181 | parsed_args.text, reminders=parsed_args.reminders | |
179 | 182 | ) |
180 | 183 | |
181 | 184 | 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 | |
1 | ||
2 | 0 | import os |
3 | 1 | import re |
4 | 2 | import shlex |
3 | import httplib2 | |
5 | 4 | import time |
6 | 5 | import textwrap |
7 | 6 | import json |
8 | 7 | import random |
9 | 8 | import sys |
10 | 9 | 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 | |
11 | 25 | |
12 | 26 | from dateutil.relativedelta import relativedelta |
13 | 27 | 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 | |
25 | 28 | from dateutil.tz import tzlocal |
26 | 29 | 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 | |
30 | 30 | from apiclient.discovery import build |
31 | 31 | from apiclient.errors import HttpError |
32 | 32 | from oauth2client.file import Storage |
33 | 33 | from oauth2client.client import OAuth2WebServerFlow |
34 | 34 | from oauth2client import tools |
35 | ||
36 | 35 | from collections import namedtuple |
37 | 36 | |
38 | 37 | EventTitle = namedtuple('EventTitle', ['title', 'color']) |
44 | 43 | all_cals = [] |
45 | 44 | now = datetime.now(tzlocal()) |
46 | 45 | agenda_length = 5 |
46 | conflicts_lookahead_days = 30 | |
47 | 47 | max_retries = 5 |
48 | 48 | auth_http = None |
49 | 49 | cal_service = None |
390 | 390 | # so we convert them to unicode and then check their size. Fixes |
391 | 391 | # the output issues we were seeing around non-US locale strings |
392 | 392 | 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 | |
394 | 394 | ) |
395 | 395 | |
396 | 396 | def _word_cut(self, word): |
403 | 403 | def _next_cut(self, string): |
404 | 404 | print_len = 0 |
405 | 405 | |
406 | words = _u(string).split() | |
406 | words = string.split() | |
407 | 407 | word_lens = [] |
408 | 408 | for i, word in enumerate(words): |
409 | 409 | word_lens.append(self._printed_len(word)) |
591 | 591 | continue |
592 | 592 | if self.options['ignore_declined'] and self._DeclinedEvent(event): |
593 | 593 | 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')) | |
598 | 598 | |
599 | 599 | if self.details.get('url'): |
600 | 600 | output += '\t%s' % (event['htmlLink'] |
602 | 602 | output += '\t%s' % (event['hangoutLink'] |
603 | 603 | if 'hangoutLink' in event else '') |
604 | 604 | |
605 | output += '\t%s' % _u(self._valid_title(event).strip()) | |
605 | output += '\t%s' % self._valid_title(event).strip() | |
606 | 606 | |
607 | 607 | if self.details.get('location'): |
608 | output += '\t%s' % (_u(event['location'].strip()) | |
608 | output += '\t%s' % (event['location'].strip() | |
609 | 609 | if 'location' in event else '') |
610 | 610 | |
611 | 611 | if self.details.get('description'): |
612 | output += '\t%s' % (_u(event['description'].strip()) | |
612 | output += '\t%s' % (event['description'].strip() | |
613 | 613 | if 'description' in event else '') |
614 | 614 | |
615 | 615 | if self.details.get('calendar'): |
616 | output += '\t%s' % _u(event['gcalcli_cal']['summary'].strip()) | |
616 | output += '\t%s' % event['gcalcli_cal']['summary'].strip() | |
617 | 617 | |
618 | 618 | if self.details.get('email'): |
619 | 619 | output += '\t%s' % (event['creator']['email'].strip() |
620 | 620 | if 'email' in event['creator'] else '') |
621 | 621 | |
622 | 622 | output = '%s\n' % output.replace('\n', '''\\n''') |
623 | sys.stdout.write(_u(output)) | |
623 | sys.stdout.write(output) | |
624 | 624 | |
625 | 625 | def _PrintEvent(self, event, prefix): |
626 | 626 | |
953 | 953 | |
954 | 954 | elif val.lower() == 'g': |
955 | 955 | val = get_input( |
956 | self.printer, 'Length (mins): ', STR_TO_INT | |
956 | self.printer, 'Length (mins or human readable): ', | |
957 | PARSABLE_DURATION | |
957 | 958 | ) |
958 | 959 | if val: |
959 | 960 | all_day = self.options.get('allday') |
1119 | 1120 | |
1120 | 1121 | def _DeclinedEvent(self, event): |
1121 | 1122 | 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': | |
1125 | 1126 | return True |
1126 | 1127 | return False |
1127 | 1128 | |
1185 | 1186 | "until", |
1186 | 1187 | end) |
1187 | 1188 | 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) | |
1188 | 1205 | |
1189 | 1206 | def AgendaQuery(self, start=None, end=None): |
1190 | 1207 | if not start: |
1579 | 1596 | continue |
1580 | 1597 | if val.lower() == 'i': |
1581 | 1598 | new_event = self._retry_with_backoff( |
1582 | self._cal_service() | |
1599 | self.get_cal_service() | |
1583 | 1600 | .events() |
1584 | 1601 | .insert( |
1585 | 1602 | calendarId=self.cals[0]['id'], |
0 | from __future__ import absolute_import | |
1 | 0 | import argparse |
2 | 1 | import sys |
3 | from gcalcli.utils import _u | |
4 | 2 | |
5 | 3 | COLOR_NAMES = set(('default', 'black', 'red', 'green', 'yellow', 'blue', |
6 | 4 | 'magenta', 'cyan', 'white', 'brightblack', 'brightred', |
20 | 18 | 'bte': '\033(0\x76\033(B', |
21 | 19 | 'ute': '\033(0\x77\033(B'}, |
22 | 20 | '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'}, | |
34 | 32 | 'ascii': { |
35 | 33 | 'hrz': '-', |
36 | 34 | 'vrt': '|', |
87 | 85 | def msg(self, msg, colorname='default', file=sys.stdout): |
88 | 86 | if self.use_color: |
89 | 87 | msg = self.colors[colorname] + msg + self.colors['default'] |
90 | file.write(_u(msg)) | |
88 | file.write(msg) | |
91 | 89 | |
92 | 90 | def err_msg(self, msg): |
93 | 91 | self.msg(msg, 'brightred', file=sys.stderr) |
0 | 0 | import calendar |
1 | 1 | import time |
2 | 2 | import locale |
3 | import six | |
4 | 3 | import re |
5 | 4 | from dateutil.tz import tzlocal |
6 | 5 | from dateutil.parser import parse as dateutil_parse |
10 | 9 | |
11 | 10 | locale.setlocale(locale.LC_ALL, '') |
12 | 11 | fuzzy_date_parse = Calendar().parse |
12 | fuzzy_datetime_parse = Calendar().parseDT | |
13 | ||
13 | 14 | |
14 | 15 | 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 | ) | |
15 | 23 | |
16 | 24 | |
17 | 25 | def parse_reminder(rem): |
44 | 52 | '!\n Check supported locales of your system.\n') |
45 | 53 | |
46 | 54 | |
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 | ||
66 | 55 | def get_times_from_duration(when, duration=0, allday=False): |
67 | 56 | |
68 | 57 | try: |
82 | 71 | |
83 | 72 | else: |
84 | 73 | try: |
85 | stop = start + timedelta(minutes=float(duration)) | |
74 | stop = start + get_timedelta_from_str(duration) | |
86 | 75 | except Exception: |
87 | 76 | raise ValueError( |
88 | 'Duration time (minutes) is invalid: %s\n' % (duration)) | |
77 | 'Duration time is invalid: %s\n' % (duration)) | |
89 | 78 | |
90 | 79 | start = start.isoformat() |
91 | 80 | stop = stop.isoformat() |
111 | 100 | return event_time |
112 | 101 | |
113 | 102 | |
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 | ||
114 | 136 | def days_since_epoch(dt): |
115 | 137 | __DAYS_IN_SECONDS__ = 24 * 60 * 60 |
116 | 138 | return calendar.timegm(dt.timetuple()) / __DAYS_IN_SECONDS__ |
0 | from __future__ import absolute_import | |
1 | ||
2 | 0 | import re |
3 | 1 | |
4 | 2 | 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) | |
7 | 5 | |
8 | 6 | # TODO: in the future, pull these from the API |
9 | 7 | # https://developers.google.com/calendar/v3/reference/colors |
76 | 74 | ) |
77 | 75 | |
78 | 76 | |
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 | ||
79 | 93 | def str_allow_empty_validator(input_str): |
80 | 94 | """ |
81 | 95 | A simple filter that allows any string to pass. |
122 | 136 | STR_ALLOW_EMPTY = str_allow_empty_validator |
123 | 137 | STR_TO_INT = str_to_int_validator |
124 | 138 | PARSABLE_DATE = parsable_date_validator |
139 | PARSABLE_DURATION = parsable_duration_validator | |
125 | 140 | VALID_COLORS = color_validator |
126 | 141 | REMINDER = reminder_validator |
0 | 0 | #!/usr/bin/env python |
1 | from __future__ import print_function | |
2 | 1 | from setuptools import setup |
3 | 2 | from gcalcli import __version__ |
4 | 3 | |
34 | 33 | 'httplib2', |
35 | 34 | 'oauth2client', |
36 | 35 | 'parsedatetime', |
37 | 'six' | |
38 | 36 | ], |
39 | 37 | extras_require={ |
40 | 38 | 'vobject': ["vobject"], |
48 | 46 | "Environment :: Console", |
49 | 47 | "Intended Audience :: End Users/Desktop", |
50 | 48 | "License :: OSI Approved :: MIT License", |
51 | "Programming Language :: Python :: 2.7", | |
52 | 49 | "Programming Language :: Python :: 3", |
53 | 50 | ]) |
64 | 64 | assert parsed_updates.end |
65 | 65 | |
66 | 66 | |
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 | ||
67 | 77 | def test_details_parser(): |
68 | 78 | details_parser = argparsers.get_details_parser() |
69 | 79 |
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 == [] |
5 | 5 | from dateutil.tz import tzutc |
6 | 6 | from datetime import datetime |
7 | 7 | |
8 | from gcalcli.utils import parse_reminder, _u | |
8 | from gcalcli.utils import parse_reminder | |
9 | 9 | from gcalcli.argparsers import (get_start_end_parser, |
10 | 10 | get_color_parser, |
11 | 11 | get_cal_query_parser, |
12 | 12 | get_output_parser, |
13 | 13 | get_updates_parser, |
14 | get_conflicts_parser, | |
14 | 15 | get_search_parser) |
15 | 16 | from gcalcli.gcal import GoogleCalendarInterface |
16 | 17 | from gcalcli.cli import parse_cal_names |
33 | 34 | |
34 | 35 | gcal.ListAllCalendars() |
35 | 36 | captured = capsys.readouterr() |
36 | assert captured.out.startswith(_u(expected_header)) | |
37 | assert captured.out.startswith(expected_header) | |
37 | 38 | |
38 | 39 | # +3 cos one for the header, one for the '----' decorations, |
39 | 40 | # and one for the eom |
58 | 59 | ['2019-07-10', '2019-07-19', '2019-08-01']) |
59 | 60 | assert PatchedGCalI().UpdatesQuery( |
60 | 61 | 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', | |
61 | 73 | start=opts.start, |
62 | 74 | end=opts.end) == 0 |
63 | 75 | |
134 | 146 | assert gcal.TextQuery(opts.text, opts.start, opts.end) == 0 |
135 | 147 | |
136 | 148 | |
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 | ||
137 | 214 | def test_modify_event(PatchedGCalI): |
138 | 215 | opts = get_search_parser().parse_args(['test']) |
139 | 216 | gcal = PatchedGCalI(**vars(opts)) |
2 | 2 | from gcalcli.validators import validate_input, ValidationError |
3 | 3 | from gcalcli.validators import (STR_NOT_EMPTY, |
4 | 4 | PARSABLE_DATE, |
5 | PARSABLE_DURATION, | |
5 | 6 | STR_TO_INT, |
6 | 7 | STR_ALLOW_EMPTY, |
7 | 8 | REMINDER, |
8 | 9 | VALID_COLORS) |
9 | import gcalcli.validators | |
10 | ||
11 | 10 | # Tests required: |
12 | 11 | # |
13 | 12 | # * Title: any string, not blank |
21 | 20 | |
22 | 21 | def test_any_string_not_blank_validator(monkeypatch): |
23 | 22 | # Empty string raises ValidationError |
24 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "") | |
23 | monkeypatch.setattr("builtins.input", lambda: "") | |
25 | 24 | with pytest.raises(ValidationError): |
26 | 25 | validate_input(STR_NOT_EMPTY) == ValidationError( |
27 | 26 | "Input here cannot be empty") |
28 | 27 | |
29 | 28 | # None raises ValidationError |
30 | monkeypatch.setattr(gcalcli.validators, "input", lambda: None) | |
29 | monkeypatch.setattr("builtins.input", lambda: None) | |
31 | 30 | with pytest.raises(ValidationError): |
32 | 31 | validate_input(STR_NOT_EMPTY) == ValidationError( |
33 | 32 | "Input here cannot be empty") |
34 | 33 | |
35 | 34 | # Valid string passes |
36 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "Valid Text") | |
35 | monkeypatch.setattr("builtins.input", lambda: "Valid Text") | |
37 | 36 | assert validate_input(STR_NOT_EMPTY) == "Valid Text" |
38 | 37 | |
39 | 38 | |
40 | 39 | def test_any_string_parsable_by_dateutil(monkeypatch): |
41 | 40 | # non-date raises ValidationError |
42 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "NON-DATE STR") | |
41 | monkeypatch.setattr("builtins.input", lambda: "NON-DATE STR") | |
43 | 42 | with pytest.raises(ValidationError): |
44 | 43 | validate_input(PARSABLE_DATE) == ValidationError( |
45 | 44 | "Expected format: a date (e.g. 2019-01-01, tomorrow 10am, " |
48 | 47 | ) |
49 | 48 | |
50 | 49 | # date string passes |
51 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "2nd January") | |
50 | monkeypatch.setattr("builtins.input", lambda: "2nd January") | |
52 | 51 | 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" | |
53 | 71 | |
54 | 72 | |
55 | 73 | def test_string_can_be_cast_to_int(monkeypatch): |
56 | 74 | # non int-castable string raises ValidationError |
57 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "X") | |
75 | monkeypatch.setattr("builtins.input", lambda: "X") | |
58 | 76 | with pytest.raises(ValidationError): |
59 | 77 | validate_input(STR_TO_INT) == ValidationError( |
60 | 78 | "Input here must be a number") |
61 | 79 | |
62 | 80 | # int string passes |
63 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "10") | |
81 | monkeypatch.setattr("builtins.input", lambda: "10") | |
64 | 82 | validate_input(STR_TO_INT) == "10" |
65 | 83 | |
66 | 84 | |
67 | 85 | def test_for_valid_colour_name(monkeypatch): |
68 | 86 | # non valid colour raises ValidationError |
69 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "purple") | |
87 | monkeypatch.setattr("builtins.input", lambda: "purple") | |
70 | 88 | with pytest.raises(ValidationError): |
71 | 89 | validate_input(VALID_COLORS) == ValidationError( |
72 | 90 | "purple is not a valid color value to use here. Please " |
74 | 92 | "tomato, safe, flamingo or banana." |
75 | 93 | ) |
76 | 94 | # valid colour passes |
77 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "grape") | |
95 | monkeypatch.setattr("builtins.input", lambda: "grape") | |
78 | 96 | validate_input(VALID_COLORS) == "grape" |
79 | 97 | |
80 | 98 | # empty str passes |
81 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "") | |
99 | monkeypatch.setattr("builtins.input", lambda: "") | |
82 | 100 | validate_input(VALID_COLORS) == "" |
83 | 101 | |
84 | 102 | |
85 | 103 | def test_any_string_and_blank(monkeypatch): |
86 | 104 | # string passes |
87 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "TEST") | |
105 | monkeypatch.setattr("builtins.input", lambda: "TEST") | |
88 | 106 | validate_input(STR_ALLOW_EMPTY) == "TEST" |
89 | 107 | |
90 | 108 | |
91 | 109 | def test_reminder(monkeypatch): |
92 | 110 | # valid reminders pass |
93 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "10m email") | |
111 | monkeypatch.setattr("builtins.input", lambda: "10m email") | |
94 | 112 | validate_input(REMINDER) == "10m email" |
95 | 113 | |
96 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "10 popup") | |
114 | monkeypatch.setattr("builtins.input", lambda: "10 popup") | |
97 | 115 | validate_input(REMINDER) == "10m email" |
98 | 116 | |
99 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "10m sms") | |
117 | monkeypatch.setattr("builtins.input", lambda: "10m sms") | |
100 | 118 | validate_input(REMINDER) == "10m email" |
101 | 119 | |
102 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "12323") | |
120 | monkeypatch.setattr("builtins.input", lambda: "12323") | |
103 | 121 | validate_input(REMINDER) == "10m email" |
104 | 122 | |
105 | 123 | # invalid reminder raises ValidationError |
106 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "meaningless") | |
124 | monkeypatch.setattr("builtins.input", lambda: "meaningless") | |
107 | 125 | with pytest.raises(ValidationError): |
108 | 126 | validate_input(REMINDER) == ValidationError( |
109 | 127 | "Format: <number><w|d|h|m> <popup|email|sms>\n") |
110 | 128 | |
111 | 129 | # invalid reminder raises ValidationError |
112 | monkeypatch.setattr(gcalcli.validators, "input", lambda: "") | |
130 | monkeypatch.setattr("builtins.input", lambda: "") | |
113 | 131 | with pytest.raises(ValidationError): |
114 | 132 | validate_input(REMINDER) == ValidationError( |
115 | 133 | "Format: <number><w|d|h|m> <popup|email|sms>\n") |
3 | 3 | |
4 | 4 | import pytest |
5 | 5 | from gcalcli.printer import COLOR_NAMES, Printer, valid_color_name |
6 | from gcalcli.utils import _u | |
7 | 6 | |
8 | 7 | |
9 | 8 | def test_init(): |
21 | 20 | cp = Printer() |
22 | 21 | for color_name in COLOR_NAMES: |
23 | 22 | out = StringIO() |
24 | cp.msg(_u('msg'), color_name, file=out) | |
23 | cp.msg('msg', color_name, file=out) | |
25 | 24 | 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' | |
27 | 26 | |
28 | 27 | |
29 | 28 | def test_red_msg(): |
30 | 29 | cp = Printer() |
31 | 30 | out = StringIO() |
32 | cp.msg(_u('msg'), 'red', file=out) | |
31 | cp.msg('msg', 'red', file=out) | |
33 | 32 | out.seek(0) |
34 | assert out.read() == _u('\033[0;31mmsg\033[0m') | |
33 | assert out.read() == '\033[0;31mmsg\033[0m' | |
35 | 34 | |
36 | 35 | |
37 | 36 | def test_err_msg(monkeypatch): |
38 | 37 | err = StringIO() |
39 | 38 | monkeypatch.setattr(sys, 'stderr', err) |
40 | 39 | cp = Printer() |
41 | cp.err_msg(_u('error')) | |
40 | cp.err_msg('error') | |
42 | 41 | err.seek(0) |
43 | assert err.read() == _u('\033[31;1merror\033[0m') | |
42 | assert err.read() == '\033[31;1merror\033[0m' | |
44 | 43 | |
45 | 44 | |
46 | 45 | def test_debug_msg(monkeypatch): |
47 | 46 | err = StringIO() |
48 | 47 | monkeypatch.setattr(sys, 'stderr', err) |
49 | 48 | cp = Printer() |
50 | cp.debug_msg(_u('debug')) | |
49 | cp.debug_msg('debug') | |
51 | 50 | err.seek(0) |
52 | assert err.read() == _u('\033[0;33mdebug\033[0m') | |
51 | assert err.read() == '\033[0;33mdebug\033[0m' | |
53 | 52 | |
54 | 53 | |
55 | 54 | def test_conky_red_msg(): |
56 | 55 | cp = Printer(conky=True) |
57 | 56 | out = StringIO() |
58 | cp.msg(_u('msg'), 'red', file=out) | |
57 | cp.msg('msg', 'red', file=out) | |
59 | 58 | out.seek(0) |
60 | assert out.read() == _u('${color red}msg${color}') | |
59 | assert out.read() == '${color red}msg${color}' | |
61 | 60 | |
62 | 61 | |
63 | 62 | def test_conky_err_msg(monkeypatch): |
64 | 63 | err = StringIO() |
65 | 64 | monkeypatch.setattr(sys, 'stderr', err) |
66 | 65 | cp = Printer(conky=True) |
67 | cp.err_msg(_u('error')) | |
66 | cp.err_msg('error') | |
68 | 67 | err.seek(0) |
69 | assert err.read() == _u('${color red}error${color}') | |
68 | assert err.read() == '${color red}error${color}' | |
70 | 69 | |
71 | 70 | |
72 | 71 | def test_conky_debug_msg(monkeypatch): |
73 | 72 | err = StringIO() |
74 | 73 | monkeypatch.setattr(sys, 'stderr', err) |
75 | 74 | cp = Printer(conky=True) |
76 | cp.debug_msg(_u('debug')) | |
75 | cp.debug_msg('debug') | |
77 | 76 | err.seek(0) |
78 | assert err.read() == _u('${color yellow}debug${color}') | |
77 | assert err.read() == '${color yellow}debug${color}' | |
79 | 78 | |
80 | 79 | |
81 | 80 | def test_no_color(): |
82 | 81 | cp = Printer(use_color=False) |
83 | 82 | out = StringIO() |
84 | cp.msg(_u('msg'), 'red', file=out) | |
83 | cp.msg('msg', 'red', file=out) | |
85 | 84 | out.seek(0) |
86 | assert out.read() == _u('msg') | |
85 | assert out.read() == 'msg' |
0 | 0 | import gcalcli.utils as utils |
1 | from datetime import datetime | |
1 | from datetime import datetime, timedelta | |
2 | 2 | from dateutil.tz import UTC |
3 | import six | |
4 | 3 | import pytest |
5 | 4 | |
6 | 5 | |
7 | 6 | def test_get_time_from_str(): |
8 | 7 | 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" | |
9 | 30 | |
10 | 31 | |
11 | 32 | def test_get_times_from_duration(): |
15 | 36 | next_day = '1970-01-02' |
16 | 37 | assert (begin_1970_midnight, two_hrs_later) == \ |
17 | 38 | 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") | |
18 | 47 | |
19 | 48 | assert (begin_1970, next_day) == \ |
20 | 49 | utils.get_times_from_duration( |
37 | 66 | assert utils.days_since_epoch(datetime(1970, 12, 31)) == 364 |
38 | 67 | |
39 | 68 | |
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 | ||
48 | 69 | def test_set_locale(): |
49 | 70 | with pytest.raises(ValueError): |
50 | 71 | utils.set_locale('not_a_real_locale') |