From 56f7ac0200c2ace08d316bd39b1182b283ba1c3b Mon Sep 17 00:00:00 2001 From: David Barnett Date: Thu, 26 Sep 2024 12:48:20 -0600 Subject: [PATCH] Add `util inspect-auth` command to dump auth token metadata --- ChangeLog | 3 +- gcalcli/argparsers.py | 6 ++++ gcalcli/cli.py | 6 ++++ gcalcli/utils.py | 82 +++++++++++++++++++++++++++++++++++-------- 4 files changed, 82 insertions(+), 15 deletions(-) diff --git a/ChangeLog b/ChangeLog index 05a2a7a..e1b9e6b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,7 +2,8 @@ v4.5.0 * Drop support for python <3.10 * Add `init` command to explicitly request auth setup/refresh * Add support for config.toml file and `gcalcli config edit` command - * Add support for `gcalcli util config-schema|reset-cache` commands + * Add support for `gcalcli util config-schema|reset-cache|inspect-auth` + commands * Behavior change: `--noincluderc` now skips gcalclirc files unconditionally, w/ or w/o --config-folder - POSSIBLE ACTION REQUIRED: Use `@path/to/gcalclirc` explicitly if it stops diff --git a/gcalcli/argparsers.py b/gcalcli/argparsers.py index 5ee1608..1cf962d 100644 --- a/gcalcli/argparsers.py +++ b/gcalcli/argparsers.py @@ -735,6 +735,12 @@ def get_argument_parser(): description="Delete gcalcli's internal cache file as a workaround for " "caching bugs like insanum/gcalcli#622", ) + util_sub.add_parser( + 'inspect-auth', + help='show metadata about auth token', + description="Dump metadata about the saved auth token gcalcli is set " + "up to use for you", + ) # Enrich with argcomplete options. argcomplete.autocomplete(parser) diff --git a/gcalcli/cli.py b/gcalcli/cli.py index bca93b9..2499ef5 100755 --- a/gcalcli/cli.py +++ b/gcalcli/cli.py @@ -364,6 +364,12 @@ def main(): 'No cache file found. Exiting without deleting ' 'anything...\n' ) + elif parsed_args.subcommand == 'inspect-auth': + auth_data = utils.inspect_auth() + for k, v in auth_data.items(): + print(f"{k}: {v}") + if not auth_data: + print("No existing auth token found") except GcalcliError as exc: printer.err_msg(str(exc)) diff --git a/gcalcli/utils.py b/gcalcli/utils.py index f94cd4b..6b8ff48 100644 --- a/gcalcli/utils.py +++ b/gcalcli/utils.py @@ -1,18 +1,23 @@ import calendar +from collections import OrderedDict +import json import locale import os import pathlib +import pickle import re import subprocess import time from datetime import datetime, timedelta -from typing import Tuple +from typing import Any, Tuple import babel from dateutil.parser import parse as dateutil_parse from dateutil.tz import tzlocal from parsedatetime.parsedatetime import Calendar +from . import auth, env + locale.setlocale(locale.LC_ALL, '') fuzzy_date_parse = Calendar().parse fuzzy_datetime_parse = Calendar().parseDT @@ -21,11 +26,11 @@ REMINDER_REGEX = r'^(\d+)([wdhm]?)(?:\s+(popup|email|sms))?$' DURATION_REGEX = re.compile( - r'^((?P[\.\d]+?)(?:d|day|days))?[ :]*' - r'((?P[\.\d]+?)(?:h|hour|hours))?[ :]*' - r'((?P[\.\d]+?)(?:m|min|mins|minute|minutes))?[ :]*' - r'((?P[\.\d]+?)(?:s|sec|secs|second|seconds))?$' - ) + r'^((?P[\.\d]+?)(?:d|day|days))?[ :]*' + r'((?P[\.\d]+?)(?:h|hour|hours))?[ :]*' + r'((?P[\.\d]+?)(?:m|min|mins|minute|minutes))?[ :]*' + r'((?P[\.\d]+?)(?:s|sec|secs|second|seconds))?$' +) def parse_reminder(rem): @@ -54,8 +59,10 @@ def set_locale(new_locale): locale.setlocale(locale.LC_ALL, new_locale) except locale.Error as exc: raise ValueError( - 'Error: ' + str(exc) + - '!\n Check supported locales of your system.\n') + 'Error: ' + + str(exc) + + '!\n Check supported locales of your system.\n' + ) def get_times_from_duration( @@ -111,7 +118,8 @@ def get_time_from_str(when): on fuzzy matching with parsedatetime """ zero_oclock_today = datetime.now(tzlocal()).replace( - hour=0, minute=0, second=0, microsecond=0) + hour=0, minute=0, second=0, microsecond=0 + ) try: event_time = dateutil_parse( @@ -144,9 +152,11 @@ def get_timedelta_from_str(delta): parts = DURATION_REGEX.match(delta) if parts is not None: try: - time_params = {name: float(param) - for name, param - in parts.groupdict().items() if param} + time_params = { + name: float(param) + for name, param in parts.groupdict().items() + if param + } parsed_delta = timedelta(**time_params) except ValueError: pass @@ -175,8 +185,13 @@ def is_all_day(event): # and end at midnight. This is ambiguous with Google Calendar events that # are not all-day but happen to begin and end at midnight. - return (event['s'].hour == 0 and event['s'].minute == 0 - and event['e'].hour == 0 and event['e'].minute == 0) + return ( + event['s'].hour == 0 + and event['s'].minute == 0 + and event['e'].hour == 0 + and event['e'].minute == 0 + ) + def localize_datetime(dt): if not hasattr(dt, 'tzinfo'): # Why are we skipping these? @@ -217,3 +232,42 @@ def shorten_path(path: pathlib.Path) -> pathlib.Path: if path.parts[:expanduser_len] == tilde_home.expanduser().parts: return tilde_home.joinpath(*path.parts[expanduser_len:]) return path + + +def inspect_auth() -> dict[str, Any]: + auth_data: dict[str, Any] = OrderedDict() + auth_path = None + for path in env.data_file_paths('oauth'): + if path.exists(): + auth_path = path + auth_data['path'] = shorten_path(path) + break + if auth_path: + with auth_path.open('rb') as gcalcli_oauth: + try: + creds = pickle.load(gcalcli_oauth) + auth_data['format'] = 'pickle' + except (pickle.UnpicklingError, EOFError): + # Try reading as legacy json format as fallback. + try: + gcalcli_oauth.seek(0) + creds = auth.creds_from_legacy_json( + json.load(gcalcli_oauth) + ) + auth_data['format'] = 'json' + except (OSError, ValueError, EOFError): + pass + if 'format' in auth_data: + for k in [ + 'client_id', + 'scopes', + 'valid', + 'token_state', + 'expiry', + 'expired', + ]: + if hasattr(creds, k): + auth_data[k] = getattr(creds, k) + else: + auth_data['format'] = 'unknown' + return auth_data