insanum / gcalcli

Google Calendar Command Line Interface
MIT License
3.32k stars 314 forks source link

Allow different Start- and End-Timezones #71

Open bekoeppel opened 11 years ago

bekoeppel commented 11 years ago

In the Google Calendar web interface, it's possible to create events with different start and end time zones: 1) Create Event 2) Click "Time zone" link 3) Enable "Use separate start and end time zones"

I would like to specify start- and end-timezones through gcalcli. For example. the 'add' option could take '--starttime' and '--endtime' parameters with a zone specifier (e.g. "-0400", or "UTC").

Regards Benedikt

tresni commented 11 years ago

You can specify a timezone in the --when option (e.g. --when 2007-09-24T15:30-8:00), both dateutil and parsedatetime should be able to deal with that. Given we only allow duration and not a specific end time, I'm not sure what the use case is here. Are you wanting an --until option instead of just --duration?

bekoeppel commented 11 years ago

Hi

When I use --when '2013-03-11T9:30-8:00' --duration 60, the following event is created:

It looks like gcalcli is converting the specified time into my local time zone.

I would expect that the following event was created:

I want to do this because I'm travelling for example to the US and will need to send the event as meeting invite to colleagues. I want that these invites are in the proper time zone.

Also, the --until parameter would be really nice, especially if it allowed me to specify a time zone as well (potentially a different time zone than in --when).

Benedikt

fj commented 9 years ago

:+1: for this request. However, one clarification: the ...Thh:mm+xx:yy-notation doesn't support adding time zones, it supports adding time offsets. Those are very different things. There are many zones with the same offset, and every zone can use more than one offset.

For example, U.S. Eastern Time is a time zone that maps different points in time to different time offsets: -4 UTC in the summer, -5 UTC in the winter. But there are plenty of other time zones with the offset -5 UTC (for example, US Central Time is -5 UTC in the summer). The events API in GCal supports adding either a time zone or a time offset, but you almost always want a zone, not an offset.

ac4000 commented 3 years ago

The following code should enable --until functionality. Once the time zone issue is sorted, --until should make it possible to have events start in one time zone and end in another. It would also be useful to avoid calculating the duration of events where the end time, not duration, is known. The --until argument overrides --duration if both are supplied. The changes are based on version 4.0.4-2 from the Debian 10 repos. If there's interest, I can try to find some time to clean it up and submit the code, but it might be better if someone more familiar with gcalcli and python does that.

diff -r -U0 ./argparsers.py /usr/lib/python3/dist-packages/gcalcli/argparsers.py
--- ./argparsers.py 2019-04-24 16:46:16.000000000 -0700
+++ /usr/lib/python3/dist-packages/gcalcli/argparsers.py    2021-06-20 14:43:32.938992990 -0700
@@ -301,0 +302 @@
+    add.add_argument('--until', default=None, type=str, help='Event end time (overrides --duration)')

diff -r -U0 ./cli.py /usr/lib/python3/dist-packages/gcalcli/cli.py
--- ./cli.py    2019-02-25 23:20:19.000000000 -0800
+++ /usr/lib/python3/dist-packages/gcalcli/cli.py   2021-06-20 14:34:12.832825535 -0700
@@ -70,6 +70,7 @@
-    if parsed_args.duration is None:
-        if parsed_args.allday:
-            prompt = 'Duration (days): '
-        else:
-            prompt = 'Duration (minutes): '
-        parsed_args.duration = get_input(printer, prompt, STR_TO_INT)
+    if parsed_args.until is None:
+        if parsed_args.duration is None:
+            if parsed_args.allday:
+                prompt = 'Duration (days): '
+            else:
+                prompt = 'Duration (minutes): '
+            parsed_args.duration = get_input(printer, prompt, STR_TO_INT)
@@ -181,10 +182,21 @@
-            try:
-                estart, eend = utils.get_times_from_duration(
-                        parsed_args.when, parsed_args.duration,
-                        parsed_args.allday
-                )
-            except ValueError as exc:
-                printer.err_msg(str(exc))
-                # Since we actually need a valid start and end time in order to
-                # add the event, we cannot proceed.
-                raise
+            if parsed_args.until is None:
+                try:
+                    estart, eend = utils.get_times_from_duration(
+                            parsed_args.when, parsed_args.duration,
+                            parsed_args.allday
+                    )
+                except ValueError as exc:
+                    printer.err_msg(str(exc))
+                    # Since we actually need a valid start and end time in order to
+                    # add the event, we cannot proceed.
+                    raise
+            else:
+                try:
+                    estart, eend = utils.get_times_from_until(
+                            parsed_args.when, parsed_args.until
+                    )
+                except ValueError as exc:
+                    printer.err_msg(str(exc))
+                    # Since we actually need a valid start and end time in order to
+                    # add the event, we cannot proceed.
+                    raise

diff -r -U0 ./utils.py /usr/lib/python3/dist-packages/gcalcli/utils.py
--- ./utils.py  2019-02-25 23:20:19.000000000 -0800
+++ /usr/lib/python3/dist-packages/gcalcli/utils.py 2021-06-20 14:36:57.392792278 -0700
@@ -66,0 +67,18 @@
+def get_times_from_until(when, until):
+
+    try:
+        start = get_time_from_str(when)
+    except Exception:
+        raise ValueError('Date and time is invalid: %s\n' % (when))
+
+    try:
+        stop = get_time_from_str(until)
+    except Exception:
+        raise ValueError('Date and time is invalid: %s\n' % (until))
+
+    start = start.isoformat()
+    stop = stop.isoformat()
+
+    return start, stop
+
+
ac4000 commented 3 years ago

Re the time zones, it appears that somewhere along the line, the supplied time zone info. is either getting mangled or dropped, possible at the parser, which may be returning a naive datetime object. Rather than get into that, I added two more parameters, --starttz and --endtz. This works just as well for my use cases. (It would be nice to parse the date input correctly, but for the few of us who need time zone functionality, we're probably happy enough to use the extra parameters.) The information supplied must be in a format the Google Calendar API can understand, but I'd recommend using IDs, like America/New_York, rather than offsets. So, e.g., incorporating --until, from above:

gcalcli ... --when '2021-06-21 1000' --until '2021-06-21 1930' --starttz 'America/New_York' --endtz 'Europe/London'

This diff is against the version as modified above (i.e., --until already included):

diff -U0 -r /tmp/gcalcli_2021-06-20/argparsers.py ./argparsers.py
--- /tmp/gcalcli_2021-06-20/argparsers.py   2021-06-20 14:43:32.000000000 -0700
+++ ./argparsers.py 2021-06-20 16:41:27.000000000 -0700
@@ -302,0 +303,2 @@
+    add.add_argument('--starttz', default=None, type=str, help='Event start time zone (Google Calendar API compatible format)')
+    add.add_argument('--endtz', default=None, type=str, help='Event end time zone (Google Calendar API compatible format)')

diff -U0 -r /tmp/gcalcli_2021-06-20/cli.py ./cli.py
--- /tmp/gcalcli_2021-06-20/cli.py  2021-06-20 14:34:12.000000000 -0700
+++ ./cli.py    2021-06-20 17:54:45.000000000 -0700
@@ -38,0 +39 @@
+from datetime import datetime
@@ -203,0 +205,8 @@
+            # strip time zone info if starttz or endtz supplied, but supply T so Google is happy
+            if parsed_args.starttz is not None:
+                estart_notz = datetime.strptime(estart, "%Y-%m-%dT%H:%M:%S%z").replace(tzinfo=None)
+                estart = str(estart_notz).replace(' ', 'T')
+            if parsed_args.endtz is not None:
+                eend_notz = datetime.strptime(eend, "%Y-%m-%dT%H:%M:%S%z").replace(tzinfo=None)
+                eend = str(eend_notz).replace(' ', 'T')
+
@@ -206 +215,2 @@
-                          parsed_args.reminders, parsed_args.event_color)
+                          parsed_args.reminders, parsed_args.event_color,
+                          parsed_args.starttz, parsed_args.endtz)

diff -U0 -r /tmp/gcalcli_2021-06-20/gcal.py ./gcal.py
--- /tmp/gcalcli_2021-06-20/gcal.py 2019-04-24 16:46:16.000000000 -0700
+++ ./gcal.py   2021-06-20 17:05:38.000000000 -0700
@@ -1269 +1269 @@
-    def AddEvent(self, title, where, start, end, descr, who, reminders, color):
+    def AddEvent(self, title, where, start, end, descr, who, reminders, color, starttz=None, endtz=None):
@@ -1284,4 +1284,12 @@
-            event['start'] = {'dateTime': start,
-                              'timeZone': self.cals[0]['timeZone']}
-            event['end'] = {'dateTime': end,
-                            'timeZone': self.cals[0]['timeZone']}
+            if starttz is None:
+                event['start'] = {'dateTime': start,
+                                  'timeZone': self.cals[0]['timeZone']}
+            else:
+                event['start'] = {'dateTime': start,
+                                  'timeZone': starttz}
+            if endtz is None:
+                event['end'] = {'dateTime': end,
+                                'timeZone': self.cals[0]['timeZone']}
+            else:
+                event['end'] = {'dateTime': end,
+                                'timeZone': endtz}
ac4000 commented 3 years ago

Again, building on the prior changes, the following allows you to retrieve the actual datetime values as Start: and End:, by supplying "start" and "end" under --details. Should now calculate Length: correctly as well, accounting for time zones. May require the --military parameter. If the time zone is not set correctly in the actual calendar event, "None" is displayed instead of the time zone. Note that the first line of output from search displays time in local time (however that's divined), so ignore that if you're parsing output and use Start: and End: instead.

Sample output from made-up flights (yeah, MAD-DEL is the scenic route):

# gcalcli search 'flight' --details start --details end --military

2021-06-23  16:04  IAD-MAD (AAL 1234; XF4GRK; A321)
                     Start: 2021-06-23 16:04 (EDT; UTC-0400)
                     End: 2021-06-23 17:22 (None; UTC+0200)

2021-06-24  06:00  MAD-BOM (MAD 1235; XF4GRK; A346)
                     Start: 2021-06-24 12:00 (CEST; UTC+0200)
                     End: 2021-06-25 13:00 (IST; UTC+0530)
diff -U0 -r /tmp/gcalcli_2021-06-21/argparsers.py ./argparsers.py
--- /tmp/gcalcli_2021-06-21/argparsers.py   2021-06-20 16:41:27.000000000 -0700
+++ ./argparsers.py 2021-06-20 21:21:48.000000000 -0700
@@ -11 +11 @@
-           'url', 'attendees', 'email', 'attachments']
+           'url', 'attendees', 'email', 'attachments', 'start', 'end']

diff -U0 -r /tmp/gcalcli_2021-06-21/gcal.py ./gcal.py
--- /tmp/gcalcli_2021-06-21/gcal.py 2021-06-20 17:05:38.000000000 -0700
+++ ./gcal.py   2021-06-21 15:05:16.000000000 -0700
@@ -10,0 +11 @@
+from pytz import timezone
@@ -758,0 +760,16 @@
+        if self.details.get('start'):
+            if event['start'].get('timeZone'):
+                start_dt_full = parse(event['start']['dateTime']).astimezone(timezone(event['start']['timeZone']))
+            else:
+                start_dt_full = parse(event['start']['dateTime'])
+            xstr = '%s  Start: %s (%s; UTC%s)\n' % (details_indent, start_dt_full.strftime("%Y-%m-%d %H:%M"), start_dt_full.tzname(), start_dt_full.strftime("%z"))
+            self.printer.msg(xstr, 'default')
+
+        if self.details.get('end'):
+            if event['start'].get('timeZone'):
+                end_dt_full = parse(event['end']['dateTime']).astimezone(timezone(event['end']['timeZone']))
+            else:
+                end_dt_full = parse(event['end']['dateTime'])
+            xstr = '%s  End: %s (%s; UTC%s)\n' % (details_indent, end_dt_full.strftime("%Y-%m-%d %H:%M"), end_dt_full.tzname(), end_dt_full.strftime("%z"))
+            self.printer.msg(xstr, 'default')
+
@@ -760 +777,9 @@
-            diff_date_time = (event['e'] - event['s'])
+            if event['start'].get('timeZone'):
+                start_dt_full = parse(event['start']['dateTime']).astimezone(timezone(event['start']['timeZone']))
+            else:
+                start_dt_full = parse(event['start']['dateTime'])
+            if event['start'].get('timeZone'):
+                end_dt_full = parse(event['end']['dateTime']).astimezone(timezone(event['end']['timeZone']))
+            else:
+                end_dt_full = parse(event['end']['dateTime'])
+            diff_date_time = (end_dt_full - start_dt_full)