Skip to content
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

ENH/WIP: add a free command #528

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ HowTo
list list available calendars
edit edit calendar events
agenda get an agenda for a time period
free get free time slots for a time period
updates get updates since a datetime for a time period
calw get a week-based agenda in calendar format
calm get a month agenda in calendar format
Expand Down
1 change: 1 addition & 0 deletions docs/man1/gcalcli.1
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Positional arguments:
list list available calendars
edit edit calendar events
agenda get an agenda for a time period
free get free time slots for a time period
updates get updates since a datetime for a time period
conflicts find conflicts between events matching search term
calw get a week-based agenda in calendar format
Expand Down
18 changes: 18 additions & 0 deletions gcalcli/argparsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,24 @@ def get_argument_parser():
description='Get a week-based agenda in calendar format.')
calw.add_argument('weeks', type=int, default=1, nargs='?')

free = sub.add_parser(
'free', parents=[details_parser, output_parser, start_end_parser],
help='get free time slots for a time period',
description='Get free timeslots in week.')
free.add_argument('--mintime', type=str, default='00:30', nargs='?',
help=('only list available blocks longer than this '
'in minutes or form hh:mm'))
free.add_argument('--daystart', type=str, default='00:01', nargs='?',
help=('only list time blocks after this time (hh:mm). '
'If --timezone is set, then this time is in that '
'timezone.'))
free.add_argument('--dayend', type=str, default='23:59', nargs='?',
help=('only list time blocks before this time (hh:mm). '
'If --timezone is set, then dayend is in that '
'timezone.'))
free.add_argument('--timezone', type=str, default=None, nargs='?',
help=('timezone to output free periods in.'))

sub.add_parser(
'calm', parents=[details_parser, output_parser, cal_query_parser],
help='get a month agenda in calendar format',
Expand Down
9 changes: 9 additions & 0 deletions gcalcli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ def main():
start_text=parsed_args.start
)

elif parsed_args.command == 'free':
gcal.FreeQuery(start=parsed_args.start,
end=parsed_args.end,
mintime=parsed_args.mintime,
daystart=parsed_args.daystart,
dayend=parsed_args.dayend,
timezone=parsed_args.timezone,
)

elif parsed_args.command == 'calm':
gcal.CalQuery(parsed_args.command, start_text=parsed_args.start)

Expand Down
122 changes: 117 additions & 5 deletions gcalcli/gcal.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
from gcalcli.conflicts import ShowConflicts

from dateutil.relativedelta import relativedelta
from datetime import datetime, timedelta, date
from dateutil.tz import tzlocal
from datetime import datetime, timedelta, date, tzinfo
from dateutil.tz import tzlocal, gettz
from dateutil.parser import parse
from apiclient.discovery import build
from apiclient.errors import HttpError
Expand Down Expand Up @@ -72,7 +72,6 @@ def __init__(self, cal_names=[], printer=Printer(), **options):
# stored as detail, but provided as option: TODO: fix that
self.details['width'] = options.get('width', 80)
self._get_cached()

self._select_cals(cal_names)

def _select_cals(self, selected_names):
Expand Down Expand Up @@ -999,7 +998,6 @@ def _iterate_events(self, start_datetime, event_list, year_date=False,
work=None):

selected = 0

if len(event_list) == 0:
self.printer.msg('\nNo Events Found...\n', 'yellow')
return selected
Expand Down Expand Up @@ -1142,6 +1140,111 @@ def ListAllCalendars(self):
self._calendar_color(cal)
)

def _display_free(self, start, end, mintime, daystart, dayend, timezone, search=None,
year_date=False):
_debug = True
if timezone is None:
tz = start.tzinfo
else:
tz = gettz(timezone)
start = start.astimezone(tz)
end = end.astimezone(tz)
if _debug:
print(start, end)
print(tz)
# start and end are local, unless we specify the time zone
# all times are assumed the timezone set by timezone...
event_list = self._search_for_events(start, end, search)
# should be hh:mm or int number of minutes:
mintime = utils.get_timedelta_from_str(mintime)
# should be hh:mm
daystart = utils.get_timedelta_from_str(daystart)
dayend = utils.get_timedelta_from_str(dayend)

ends = [start] # end of busy blocks
starts = []

for event in event_list:
if _debug:
print(event['s'], event['e'], event['summary'])

if 'transparency' in event and event['transparency'] == 'transparent':
if _debug:
print('This event transparent: skip')
pass
elif event['s'].astimezone(tz) > event['e'].astimezone(tz):
pass
elif event['s'].astimezone(tz) <= ends[-1]:
# this event overlaps with last
# so replace the previous end time with this one:
ends[-1] = event['e'].astimezone(tz)
else:
starts += [event['s'].astimezone(tz)]
ends += [event['e'].astimezone(tz)]
starts += [end]

self.printer.msg(f'\nAvailability {start.strftime("%b %d")} - {end.strftime("%b %d")} \n', 'yellow')
self.printer.msg('---------------------------- \n', 'yellow')
if tz != tzinfo('local'):
self.printer.msg(f'Timezone: {tz.tzname(starts[0])} \n\n', 'red')
else:
self.printer.msg('\n')

# now we have a list of ends and starts for busy time. These are
# the starts and ends of free time, so lets swap around. This
# block also tests if a free block spans midnight and if it does,
# insert an end/start pair. We do this because we want each day
# to get its own free blocks.
newstarts = []
newends = []
for s, e in zip(ends, starts):
if _debug:
print('s/e', s, e)
newstarts += [s]
midnight = s.replace(hour=0, minute=0, second=0) + timedelta(days=1)
if s < midnight < e:
# if free block spans midnight, insert midnight:
newstarts +=[midnight]
newends += [midnight - timedelta(seconds=1)]
while e > midnight + timedelta(days=1):
# if the new free block spans midnight keep adding more
midnight += timedelta(days=1)
newstarts += [midnight]
newends += [midnight - timedelta(seconds=1)]
newends += [e]

if _debug:
print('New:')
for s, e in zip(newstarts, newends):
print(s, e)

# now print for each day:
day = start.replace(hour=0, minute=0, second=0)
while day < end:
self.printer.msg(f"{day.strftime('%a %b %d')}:\n", 'green')
maxday = day

for s, e in zip(newstarts, newends):
if e - s < mintime:
pass
elif s>=day and s<day+timedelta(days=1):
# event is today. But we need to trim depending on if it
# is during working hours:
if s < day+daystart:
s = day + daystart
if e < day + daystart:
e = s
if e > day+dayend:
e = day + dayend
if s > day + dayend:
s = e
if e > maxday and e > s and e - s >= mintime:
self.printer.msg(f" {s.strftime('%H:%M')} to {e.strftime('%H:%M')}\n")
maxday = e

day += timedelta(days=1)


def _display_queried_events(self, start, end, search=None,
year_date=False):
event_list = self._search_for_events(start, end, search)
Expand Down Expand Up @@ -1217,6 +1320,16 @@ def AgendaUpdate(self, file=sys.stdin):

getattr(actions, action)(row, cal, self)

def FreeQuery(self, start=None, end=None, mintime=None,
daystart=None, dayend=None, timezone=None, count=1):
if not start:
start = self.now.replace(hour=0, minute=0, second=0, microsecond=0)
if not end:
end = (start + timedelta(days=(count * 8)))
if not mintime:
mintime = '00:30'
return self._display_free(start, end, mintime, daystart, dayend, timezone)

def CalQuery(self, cmd, start_text='', count=1):
if not start_text:
# convert now to midnight this morning and use for default
Expand Down Expand Up @@ -1258,7 +1371,6 @@ def CalQuery(self, cmd, start_text='', count=1):
count += 1

event_list = self._search_for_events(start, end, None)

self._GraphEvents(cmd, start, count, event_list)

def QuickAddEvent(self, event_text, reminders=None):
Expand Down