-
Notifications
You must be signed in to change notification settings - Fork 314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add agendaupdate
subcommand
#553
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
935013d
refactor: move _tsv() implementation to gcalcli.details
michaelmhoffman 7f5e741
ci: eliminate flake8-docstrings method docstring checks for details.py
michaelmhoffman d46a950
refactor: derive `argparsers.DETAILS` from `details.HANDLERS`
michaelmhoffman 6139d0b
fix: change SimpleSingleColumnHandler.header to a simple list
michaelmhoffman 97e21b1
feat!: add header row for `gcalcli agenda --tsv`
michaelmhoffman 784d812
feat: add `--detail id` for `gcalcli agenda --tsv`
michaelmhoffman 72710b4
refactor: change `header` to `fieldnames` and `column` to `field`
michaelmhoffman abb964f
feat: add agendaupdate for title, id, location, description
michaelmhoffman fea6ad7
fix: gcalcli agenda to work with refactor 72710b4cb
michaelmhoffman ba7e543
refactor: change `Handler.patch()` to include `fieldname` argument
michaelmhoffman 06f409a
feat: add agendaupdate for time
michaelmhoffman 8849d71
refactor: move gcal.GoogleCalendarInterface._isallday() to utils.is_a…
michaelmhoffman e0780dd
feat!: gcal agenda --tsv prints empty time fields for all-day events
michaelmhoffman 384ba5d
refactor: `details.Url` uses `details.URL_PROPS` to identify properti…
michaelmhoffman 719f85b
feat: add agendaupdate for url
michaelmhoffman f14e6f3
feat: add agendaupdate for conference
michaelmhoffman 5a3179b
refactor: add `exceptions.ReadonlyError`, `exceptions.ReadonlyCheckEr…
michaelmhoffman d7589ed
feat: add agendaupdate for calendar
michaelmhoffman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,276 @@ | ||
"""Handlers for specific details of events.""" | ||
|
||
from collections import OrderedDict | ||
from datetime import datetime | ||
from itertools import chain | ||
from typing import List | ||
|
||
from dateutil.parser import isoparse, parse | ||
|
||
from gcalcli.exceptions import ReadonlyError, ReadonlyCheckError | ||
from gcalcli.utils import is_all_day | ||
|
||
FMT_DATE = '%Y-%m-%d' | ||
FMT_TIME = '%H:%M' | ||
TODAY = datetime.now().date() | ||
|
||
URL_PROPS = OrderedDict([('html_link', 'htmlLink'), | ||
('hangout_link', 'hangoutLink')]) | ||
ENTRY_POINT_PROPS = OrderedDict([('conference_entry_point_type', | ||
'entryPointType'), | ||
('conference_uri', 'uri')]) | ||
|
||
|
||
def _valid_title(event): | ||
if 'summary' in event and event['summary'].strip(): | ||
return event['summary'] | ||
else: | ||
return '(No title)' | ||
|
||
|
||
class Handler: | ||
"""Handler for a specific detail of an event.""" | ||
|
||
# list of strings for fieldnames provided by this object | ||
# XXX: py36+: change to `fieldnames: List[str]` | ||
fieldnames = [] # type: List[str] | ||
|
||
@classmethod | ||
def get(cls, event): | ||
"""Return simple string representation for columnar output.""" | ||
raise NotImplementedError | ||
|
||
@classmethod | ||
def patch(cls, cal, event, fieldname, value): | ||
"""Patch event from value.""" | ||
raise NotImplementedError | ||
|
||
|
||
class SingleFieldHandler(Handler): | ||
"""Handler for a detail that only produces one column.""" | ||
|
||
@classmethod | ||
def get(cls, event): | ||
return [cls._get(event).strip()] | ||
|
||
@classmethod | ||
def patch(cls, cal, event, fieldname, value): | ||
return cls._patch(event, value) | ||
|
||
|
||
class SimpleSingleFieldHandler(SingleFieldHandler): | ||
"""Handler for single-string details that require no special processing.""" | ||
|
||
@classmethod | ||
def _get(cls, event): | ||
return event.get(cls.fieldnames[0], '') | ||
|
||
@classmethod | ||
def _patch(cls, event, value): | ||
event[cls.fieldnames[0]] = value | ||
|
||
|
||
class Time(Handler): | ||
"""Handler for dates and times.""" | ||
|
||
fieldnames = ['start_date', 'start_time', 'end_date', 'end_time'] | ||
|
||
@classmethod | ||
def _datetime_to_fields(cls, instant, all_day): | ||
instant_date = instant.strftime(FMT_DATE) | ||
|
||
if all_day: | ||
instant_time = '' | ||
else: | ||
instant_time = instant.strftime(FMT_TIME) | ||
|
||
return [instant_date, instant_time] | ||
|
||
@classmethod | ||
def get(cls, event): | ||
all_day = is_all_day(event) | ||
|
||
start_fields = cls._datetime_to_fields(event['s'], all_day) | ||
end_fields = cls._datetime_to_fields(event['e'], all_day) | ||
|
||
return start_fields + end_fields | ||
|
||
@classmethod | ||
def patch(cls, cal, event, fieldname, value): | ||
instant_name, _, unit = fieldname.partition('_') | ||
|
||
assert instant_name in {'start', 'end'} | ||
|
||
if unit == 'date': | ||
instant = event[instant_name] = {} | ||
instant_date = parse(value, default=TODAY) | ||
|
||
instant['date'] = instant_date.isoformat() | ||
instant['dateTime'] = None # clear any previous non-all-day time | ||
else: | ||
assert unit == 'time' | ||
|
||
# If the time field is empty, do nothing. | ||
# This enables all day events. | ||
if not value.strip(): | ||
return | ||
|
||
# date must be an earlier TSV field than time | ||
instant = event[instant_name] | ||
instant_date = isoparse(instant['date']) | ||
instant_datetime = parse(value, default=instant_date) | ||
|
||
instant['date'] = None # clear all-day date | ||
instant['dateTime'] = instant_datetime.isoformat() | ||
instant['timeZone'] = cal['timeZone'] | ||
|
||
|
||
class Url(Handler): | ||
"""Handler for HTML and legacy Hangout links.""" | ||
|
||
fieldnames = list(URL_PROPS.keys()) | ||
|
||
@classmethod | ||
def get(cls, event): | ||
return [event.get(prop, '') for prop in URL_PROPS.values()] | ||
|
||
@classmethod | ||
def patch(cls, cal, event, fieldname, value): | ||
if fieldname == 'html_link': | ||
raise ReadonlyError(fieldname, | ||
'It is not possible to verify that the value ' | ||
'has not changed. ' | ||
'Remove it from the input.') | ||
|
||
prop = URL_PROPS[fieldname] | ||
|
||
# Fail if the current value doesn't | ||
# match the desired patch. This requires an additional API query for | ||
# each row, so best to avoid attempting to update these fields. | ||
|
||
curr_value = event.get(prop, '') | ||
|
||
if curr_value != value: | ||
raise ReadonlyCheckError(fieldname, curr_value, value) | ||
|
||
|
||
class Conference(Handler): | ||
"""Handler for videoconference and teleconference details.""" | ||
|
||
fieldnames = list(ENTRY_POINT_PROPS.keys()) | ||
|
||
@classmethod | ||
def get(cls, event): | ||
if 'conferenceData' not in event: | ||
return ['', ''] | ||
|
||
data = event['conferenceData'] | ||
|
||
# only display first entry point for TSV | ||
# https://github.com/insanum/gcalcli/issues/533 | ||
entry_point = data['entryPoints'][0] | ||
|
||
return [entry_point.get(prop, '') | ||
for prop in ENTRY_POINT_PROPS.values()] | ||
|
||
@classmethod | ||
def patch(cls, cal, event, fieldname, value): | ||
if not value: | ||
return | ||
|
||
prop = ENTRY_POINT_PROPS[fieldname] | ||
|
||
data = event.setdefault('conferenceData', {}) | ||
entry_points = data.setdefault('entryPoints', []) | ||
if not entry_points: | ||
entry_points.append({}) | ||
|
||
entry_point = entry_points[0] | ||
entry_point[prop] = value | ||
|
||
|
||
class Title(SingleFieldHandler): | ||
"""Handler for title.""" | ||
|
||
fieldnames = ['title'] | ||
|
||
@classmethod | ||
def _get(cls, event): | ||
return _valid_title(event) | ||
|
||
@classmethod | ||
def _patch(cls, event, value): | ||
event['summary'] = value | ||
|
||
|
||
class Location(SimpleSingleFieldHandler): | ||
"""Handler for location.""" | ||
|
||
fieldnames = ['location'] | ||
|
||
|
||
class Description(SimpleSingleFieldHandler): | ||
"""Handler for description.""" | ||
|
||
fieldnames = ['description'] | ||
|
||
|
||
class Calendar(SingleFieldHandler): | ||
"""Handler for calendar.""" | ||
|
||
fieldnames = ['calendar'] | ||
|
||
@classmethod | ||
def _get(cls, event): | ||
return event['gcalcli_cal']['summary'] | ||
|
||
@classmethod | ||
def patch(cls, cal, event, fieldname, value): | ||
curr_value = cal['summary'] | ||
|
||
if curr_value != value: | ||
raise ReadonlyCheckError(fieldname, curr_value, value) | ||
|
||
|
||
class Email(SingleFieldHandler): | ||
"""Handler for emails.""" | ||
|
||
fieldnames = ['email'] | ||
|
||
@classmethod | ||
def _get(cls, event): | ||
return event['creator'].get('email', '') | ||
|
||
|
||
class ID(SimpleSingleFieldHandler): | ||
"""Handler for event ID.""" | ||
|
||
fieldnames = ['id'] | ||
|
||
|
||
HANDLERS = OrderedDict([('id', ID), | ||
('time', Time), | ||
('url', Url), | ||
('conference', Conference), | ||
('title', Title), | ||
('location', Location), | ||
('description', Description), | ||
('calendar', Calendar), | ||
('email', Email)]) | ||
HANDLERS_READONLY = {Url, Calendar} | ||
|
||
FIELD_HANDLERS = dict(chain.from_iterable( | ||
(((fieldname, handler) | ||
for fieldname in handler.fieldnames) | ||
for handler in HANDLERS.values()))) | ||
|
||
FIELDNAMES_READONLY = frozenset(fieldname | ||
for fieldname, handler | ||
in FIELD_HANDLERS.items() | ||
if handler in HANDLERS_READONLY) | ||
|
||
_DETAILS_WITHOUT_HANDLERS = ['length', 'reminders', 'attendees', | ||
'attachments', 'end'] | ||
|
||
DETAILS = list(HANDLERS.keys()) + _DETAILS_WITHOUT_HANDLERS | ||
DETAILS_DEFAULT = {'time', 'title'} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to incorporate the 'end' detail into the Time handler?