Download intake/calendar_intake.py from s-b3/LifeStack: direct link, hf CLI and curl.
- Browser
- Download file 6.58 kB
-
https://huggingface.co/spaces/s-b3/LifeStack/resolve/main/intake/calendar_intake.py
- Command line
-
hf download hf://spaces/s-b3/LifeStack/intake/calendar_intake.py
-
curl -L -o calendar_intake.py https://huggingface.co/spaces/s-b3/LifeStack/resolve/main/intake/calendar_intake.py
6.58 kB
| """ | |
| calendar_intake.py β Extract life-state signals from Google Calendar. | |
| Real OAuth flow mirrors gmail_intake.py. Falls back to demo_signals.json | |
| automatically when credentials.json is absent (hackathon demo mode). | |
| SETUP (real mode): | |
| 1. Enable Google Calendar API in console.cloud.google.com | |
| 2. Download credentials.json to the project root | |
| 3. pip install google-auth google-auth-oauthlib google-api-python-client | |
| """ | |
| import os | |
| import json | |
| from datetime import datetime, timedelta, timezone | |
| SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'] | |
| _DEMO_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'demo_signals.json') | |
| class CalendarIntake: | |
| # ββ Real OAuth path ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def authenticate(self): | |
| """Return an authenticated Calendar API service. Raises if credentials missing.""" | |
| from google.auth.transport.requests import Request | |
| from google.oauth2.credentials import Credentials | |
| from google_auth_oauthlib.flow import InstalledAppFlow | |
| from googleapiclient.discovery import build | |
| creds = None | |
| token_file = 'calendar_token.json' | |
| if os.path.exists(token_file): | |
| creds = Credentials.from_authorized_user_file(token_file, SCOPES) | |
| if not creds or not creds.valid: | |
| if creds and creds.expired and creds.refresh_token: | |
| creds.refresh(Request()) | |
| else: | |
| if not os.path.exists('credentials.json'): | |
| raise FileNotFoundError("credentials.json missing.") | |
| flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES) | |
| creds = flow.run_local_server(port=0) | |
| with open(token_file, 'w') as f: | |
| f.write(creds.to_json()) | |
| return build('calendar', 'v3', credentials=creds) | |
| def extract_signals(self, service, days: int = 7) -> dict: | |
| """Pull real calendar data and return structured signals.""" | |
| now = datetime.now(timezone.utc) | |
| end = now + timedelta(days=days) | |
| events_result = service.events().list( | |
| calendarId='primary', | |
| timeMin=now.isoformat(), | |
| timeMax=end.isoformat(), | |
| singleEvents=True, | |
| orderBy='startTime', | |
| maxResults=100, | |
| ).execute() | |
| events = events_result.get('items', []) | |
| total_minutes = 0 | |
| back_to_back = 0 | |
| personal = 0 | |
| deadlines = [] | |
| prev_end = None | |
| for ev in events: | |
| start_str = ev.get('start', {}).get('dateTime') or ev.get('start', {}).get('date') | |
| end_str = ev.get('end', {}).get('dateTime') or ev.get('end', {}).get('date') | |
| if not start_str or not end_str: | |
| continue | |
| try: | |
| s = datetime.fromisoformat(start_str.replace('Z', '+00:00')) | |
| e = datetime.fromisoformat(end_str.replace('Z', '+00:00')) | |
| except ValueError: | |
| continue | |
| duration = (e - s).total_seconds() / 60 | |
| total_minutes += duration | |
| if prev_end and (s - prev_end).total_seconds() < 600: | |
| back_to_back += 1 | |
| prev_end = e | |
| title = ev.get('summary', '').lower() | |
| if any(w in title for w in ('personal', 'gym', 'family', 'date', 'birthday', 'doctor')): | |
| personal += 1 | |
| importance = ev.get('colorId') | |
| if importance in ('11', '4') or any(w in title for w in ('deadline', 'submit', 'launch', 'board', 'review')): | |
| deadlines.append({ | |
| "title": ev.get('summary', 'Untitled'), | |
| "due_in_hours": round((s - now).total_seconds() / 3600), | |
| "priority": "critical" if importance == '11' else "high", | |
| }) | |
| working_minutes = days * 8 * 60 | |
| occupancy = min(100, round(total_minutes / working_minutes * 100)) | |
| avg_meeting_h = round(total_minutes / 60 / days, 1) | |
| focus_blocks = max(0, days - back_to_back - 1) | |
| return { | |
| "week_occupancy_pct": occupancy, | |
| "avg_meeting_hours_per_day": avg_meeting_h, | |
| "back_to_back_blocks": back_to_back, | |
| "focus_blocks_count": focus_blocks, | |
| "personal_events_this_week": personal, | |
| "upcoming_deadlines": deadlines[:3], | |
| "summary": ( | |
| f"{occupancy}% of working hours booked. " | |
| f"{avg_meeting_h}h meetings/day. " | |
| f"{back_to_back} back-to-back chains. " | |
| f"{len(deadlines)} deadlines upcoming." | |
| ), | |
| } | |
| def to_life_metrics(self, signals: dict) -> dict: | |
| """Map calendar signals to LifeMetrics deltas.""" | |
| occ = signals.get('week_occupancy_pct', 50) | |
| btb = signals.get('back_to_back_blocks', 0) | |
| focus = signals.get('focus_blocks_count', 3) | |
| return { | |
| "time.free_hours_per_week": -((occ - 50) / 5), | |
| "time.schedule_control": -(occ / 10), | |
| "mental_wellbeing.stress_level": (occ / 10) + (btb * 2), | |
| "mental_wellbeing.focus_quality": focus * 5 - 10, | |
| "career.workload": (occ - 50) / 2, | |
| } | |
| # ββ Demo fallback ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def demo_signals() -> dict: | |
| with open(_DEMO_PATH) as f: | |
| return json.load(f)['calendar'] | |
| def demo_life_metrics() -> dict: | |
| with open(_DEMO_PATH) as f: | |
| d = json.load(f) | |
| return {k: v for k, v in d['derived_metric_deltas'].items() | |
| if k.startswith('time.') or k.startswith('career.')} | |
| # ββ Unified entry point ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def sync(self) -> tuple[dict, dict, bool]: | |
| """ | |
| Returns (signals, metric_deltas, is_demo). | |
| Tries real OAuth first; silently falls back to demo on any failure. | |
| """ | |
| try: | |
| svc = self.authenticate() | |
| sigs = self.extract_signals(svc) | |
| return sigs, self.to_life_metrics(sigs), False | |
| except Exception: | |
| return self.demo_signals(), self.demo_life_metrics(), True | |