Created
February 24, 2026 23:51
-
-
Save lonniev/26e288ecb67d63934a0fa9dd0c6779cf to your computer and use it in GitHub Desktop.
Query Google Calendar for meetings by attendee email pattern. Includes printf+jq one-liner for total hours and a shell infographic recipe.
This file contains hidden or 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
| Query Google Calendar for meetings by attendee email pattern. Includes printf+jq one-liner for total hours and a shell infographic recipe. | |
| #!/usr/bin/env python3 | |
| """Query Google Calendar for meetings by attendee email pattern. | |
| Returns JSON events filtered by date range and regex email match. | |
| Useful for calculating time spent in meetings with specific people. | |
| Prerequisites: | |
| pip install google-api-python-client google-auth google-auth-oauthlib python-dateutil pytz tzlocal | |
| Create ~/.gcalclirc with your OAuth2 credentials: | |
| --client-id=YOUR_CLIENT_ID | |
| --client-secret=YOUR_CLIENT_SECRET | |
| Example — total hours in meetings with a specific person: | |
| printf "%.2f hours in meetings with Collins in 2025 and 2026\\n" \\ | |
| $(python ~/meetingsWith.py \\ | |
| --start-date 2025-03-01 \\ | |
| --end-date 2026-12-01 \\ | |
| --email-pattern 'edaklavan@rtx.com' \\ | |
| | jq '.[] | {summary, start: .start.dateTime, end: .end.dateTime} | |
| | (.end | fromdateiso8601) - (.start | fromdateiso8601)' \\ | |
| | jq -s 'add / 3600') | |
| # Output: 22.00 hours in meetings with Collins in 2025 and 2026 | |
| Example — mini infographic with meeting count and averages: | |
| EMAIL='edaklavan@rtx.com'; START=2025-03-01; END=2026-12-01 | |
| DATA=$(python ~/meetingsWith.py --start-date $START --end-date $END --email-pattern "$EMAIL") | |
| HOURS=$(echo "$DATA" | jq '[.[] | ((.end.dateTime | fromdateiso8601) - (.start.dateTime | fromdateiso8601))] | add / 3600') | |
| COUNT=$(echo "$DATA" | jq '. | length') | |
| printf "\\n ┌─────────────────────────────────────┐\\n" | |
| printf " │ Meeting Stats: %-20s│\\n" "Collins" | |
| printf " ├─────────────────────────────────────┤\\n" | |
| printf " │ Period: %s → %s │\\n" "$START" "$END" | |
| printf " │ Meetings: %3d │\\n" "$COUNT" | |
| printf " │ Total: %6.1f hours │\\n" "$HOURS" | |
| printf " │ Average: %6.1f hrs/mtg │\\n" "$(echo "$HOURS / $COUNT" | bc -l)" | |
| printf " └─────────────────────────────────────┘\\n\\n" | |
| """ | |
| import argparse | |
| import os | |
| import sys | |
| import re | |
| import json | |
| from googleapiclient.discovery import build | |
| from google.oauth2.credentials import Credentials | |
| from google.auth.transport.requests import Request | |
| from google_auth_oauthlib.flow import InstalledAppFlow | |
| from dateutil import parser | |
| from datetime import datetime | |
| import pytz | |
| from tzlocal import get_localzone | |
| import itertools | |
| from typing import Any, Union | |
| def load_credentials(): | |
| creds = {} | |
| with open(os.path.expanduser('~/.gcalclirc'), 'r') as file: | |
| for line in file: | |
| key, value = line.strip().split('=') | |
| creds[key.lstrip('--')] = value | |
| return creds | |
| def oAuthenticate( client_id, client_secret ): | |
| SCOPES = [ 'https://www.googleapis.com/auth/calendar.readonly' ] | |
| credentials = None | |
| savedCredsFile = os.path.expanduser("~/.gcal_token.json") | |
| if os.path.exists(savedCredsFile): | |
| credentials = Credentials.from_authorized_user_file( savedCredsFile, SCOPES ) | |
| if not credentials or not credentials.valid: | |
| if credentials and credentials.expired and credentials.refresh_token: | |
| credentials.refresh( Request() ) | |
| else: | |
| original_stdout = sys.stdout | |
| sys.stdout = open( os.devnull, 'w' ) | |
| try: | |
| flow = InstalledAppFlow.from_client_config( | |
| { | |
| "installed": { | |
| "client_id": client_id, | |
| "client_secret": client_secret, | |
| "auth_uri": "https://accounts.google.com/o/oauth2/auth", | |
| "token_uri": "https://oauth2.googleapis.com/token", | |
| "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"] | |
| } | |
| }, | |
| SCOPES | |
| ) | |
| credentials = flow.run_local_server(port=0) | |
| finally: | |
| sys.stdout = original_stdout | |
| with open( savedCredsFile, "w" ) as tokenfile: | |
| tokenfile.write( credentials.to_json() ) | |
| refresh_token = credentials.refresh_token | |
| token_uri = credentials.token_uri | |
| return { | |
| 'client_id': client_id, | |
| 'client_secret': client_secret, | |
| 'refresh_token': refresh_token, | |
| 'token_uri': token_uri | |
| } | |
| def get_calendar_events(start_date, end_date, pageSize, service, pageToken ): | |
| return service.events().list( | |
| calendarId='primary', timeMin=start_date, timeMax=end_date, singleEvents=True, | |
| orderBy='startTime', maxResults= pageSize, pageToken= pageToken ).execute() | |
| def findEvents( start_date, end_date, email_pattern ): | |
| creds_data = load_credentials() | |
| oauth_creds = oAuthenticate( creds_data['client-id'], creds_data['client-secret'] ) | |
| creds = Credentials.from_authorized_user_info(oauth_creds) | |
| service = build('calendar', 'v3', credentials=creds) | |
| def fetch_pages(): | |
| pageToken = None | |
| while True: | |
| response = get_calendar_events( start_date, end_date, 100, service, pageToken ) | |
| yield response['items'] | |
| pageToken = response.get('nextPageToken') | |
| if not pageToken: | |
| break | |
| events = itertools.chain.from_iterable( fetch_pages() ) | |
| matched_events = [ | |
| event for event in events | |
| if ( any(re.search(email_pattern, attendee.get('email', '')) for attendee in event.get('attendees', [])) or re.search(email_pattern, event.get('organizer', {}).get('email', 'UNORGANIZED')) ) | |
| ] | |
| return matched_events | |
| def parseToOffsetIso8601( date_str ): | |
| local_tz = get_localzone() | |
| local_dt = parser.parse( date_str ).astimezone( local_tz ) | |
| return local_dt.strftime('%Y-%m-%dT%H:%M:%S%z') | |
| def toZuluIso8601(date_str): | |
| dt = datetime.fromisoformat(date_str) | |
| dt_utc = dt.astimezone( pytz.utc ) | |
| return dt_utc.strftime("%Y-%m-%dT%H:%M:%SZ") | |
| def mapTimestampsToUtc(d): | |
| return { | |
| **d, | |
| 'start': {**d['start'], 'dateTime': toZuluIso8601(d['start']['dateTime'])}, | |
| 'end': {**d['end'], 'dateTime': toZuluIso8601(d['end']['dateTime'])} | |
| } | |
| def main(): | |
| arg_parser = argparse.ArgumentParser(description='Fetch Google Calendar events by date range and email pattern.') | |
| arg_parser.add_argument('--start-date', required=True, help='Start date in YYYY-MM-DD format') | |
| arg_parser.add_argument('--end-date', required=True, help='End date in YYYY-MM-DD format') | |
| arg_parser.add_argument('--email-pattern', required=True, help='Email regex pattern to match participants') | |
| args = arg_parser.parse_args() | |
| args.start_date = parseToOffsetIso8601( args.start_date ) | |
| args.end_date = parseToOffsetIso8601( args.end_date ) | |
| events = findEvents(args.start_date, args.end_date, args.email_pattern) | |
| print(json.dumps( [ mapTimestampsToUtc( event ) for event in events ], indent=2)) | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment