-
Notifications
You must be signed in to change notification settings - Fork 3
Google calendar branch #126
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
Changes from all commits
c33551e
d9f4cb2
0550cc8
048fa87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
|
|
||
| import json | ||
| import logging | ||
| from datetime import timezone | ||
| from typing import Any, Callable, Awaitable, Optional | ||
| from functools import wraps | ||
|
|
||
|
|
@@ -105,9 +106,73 @@ | |
| generate_resource_id, | ||
| etags_match, | ||
| REPLICA_NOW_RFC3339, | ||
| parse_rfc3339, | ||
| ) | ||
|
|
||
|
|
||
| # ============================================================================ | ||
| # WATCH EXPIRATION PARSING | ||
| # ============================================================================ | ||
|
|
||
|
|
||
| def parse_watch_expiration(expiration: Any) -> Optional[int]: | ||
| """ | ||
| Parse expiration value for watch channels. | ||
|
|
||
| Google Calendar API accepts expiration as milliseconds since Unix epoch. | ||
| Some clients may send ISO date strings which need to be converted. | ||
|
|
||
| Args: | ||
| expiration: The expiration value (int, string int, or ISO date string) | ||
|
|
||
| Returns: | ||
| Expiration in milliseconds since epoch, or None if not provided | ||
|
|
||
| Raises: | ||
| ValidationError: If the expiration format is invalid | ||
| """ | ||
| if expiration is None: | ||
| return None | ||
|
|
||
| # If already an int, return as-is | ||
| if isinstance(expiration, int): | ||
| return expiration | ||
|
Comment on lines
+137
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n backend/src/services/calendar/api/methods.py | sed -n '120,160p'Repository: hubert-marek/agent-diff Length of output: 1592 🏁 Script executed: cat -n backend/src/services/calendar/api/methods.py | sed -n '160,180p'Repository: hubert-marek/agent-diff Length of output: 838 Reject boolean expirations explicitly.
🔧 Suggested fix- if isinstance(expiration, int):
+ if isinstance(expiration, int) and not isinstance(expiration, bool):
return expiration🤖 Prompt for AI Agents |
||
|
|
||
| # If it's a string, try to parse it | ||
| if isinstance(expiration, str): | ||
| expiration = expiration.strip() | ||
| if not expiration: | ||
| return None | ||
|
|
||
| # Try parsing as integer (milliseconds) | ||
| try: | ||
| return int(expiration) | ||
| except ValueError: | ||
| pass | ||
|
|
||
| # Try parsing as ISO date string | ||
| try: | ||
| dt = parse_rfc3339(expiration) | ||
| # If datetime is naive, assume UTC | ||
| if dt.tzinfo is None: | ||
| dt = dt.replace(tzinfo=timezone.utc) | ||
| # Convert to milliseconds since epoch | ||
| return int(dt.timestamp() * 1000) | ||
| except (ValueError, AttributeError): | ||
| pass | ||
|
|
||
| # Invalid format | ||
| raise ValidationError( | ||
| f"Invalid expiration format: {expiration}. " | ||
| "Expected milliseconds since epoch or ISO 8601 date string." | ||
| ) | ||
|
|
||
| raise ValidationError( | ||
| f"Invalid expiration type: {type(expiration).__name__}. " | ||
| "Expected integer or string." | ||
| ) | ||
|
|
||
|
|
||
| # ============================================================================ | ||
| # REQUEST UTILITIES | ||
| # ============================================================================ | ||
|
|
@@ -1036,15 +1101,15 @@ async def calendar_list_watch(request: Request) -> JSONResponse: | |
| from ..database.schema import Channel | ||
|
|
||
| resource_id = generate_resource_id() | ||
| expiration = body.get("expiration") | ||
| expiration_ms = parse_watch_expiration(body.get("expiration")) | ||
|
|
||
| channel = Channel( | ||
| id=channel_id, | ||
| resource_id=resource_id, | ||
| resource_uri=f"/users/me/calendarList", | ||
| type=channel_type, | ||
| address=address, | ||
| expiration=int(expiration) if expiration else None, | ||
| expiration=expiration_ms, | ||
| token=body.get("token"), | ||
hubert-marek marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| params=body.get("params"), | ||
| payload=body.get("payload", False), | ||
|
|
@@ -1961,15 +2026,15 @@ async def events_watch(request: Request) -> JSONResponse: | |
| from ..database.schema import Channel | ||
|
|
||
| resource_id = generate_resource_id() | ||
| expiration = body.get("expiration") | ||
| expiration_ms = parse_watch_expiration(body.get("expiration")) | ||
|
|
||
| channel = Channel( | ||
| id=channel_id, | ||
| resource_id=resource_id, | ||
| resource_uri=f"/calendars/{calendar_id}/events", | ||
| type=channel_type, | ||
| address=address, | ||
| expiration=int(expiration) if expiration else None, | ||
| expiration=expiration_ms, | ||
| token=body.get("token"), | ||
| params=body.get("params"), | ||
| payload=body.get("payload", False), | ||
|
|
@@ -2400,15 +2465,15 @@ async def acl_watch(request: Request) -> JSONResponse: | |
| from ..database.schema import Channel | ||
|
|
||
| resource_id = generate_resource_id() | ||
| expiration = body.get("expiration") | ||
| expiration_ms = parse_watch_expiration(body.get("expiration")) | ||
|
|
||
| channel = Channel( | ||
| id=channel_id, | ||
| resource_id=resource_id, | ||
| resource_uri=f"/calendars/{calendar_id}/acl", | ||
| type=channel_type, | ||
| address=address, | ||
| expiration=int(expiration) if expiration else None, | ||
| expiration=expiration_ms, | ||
| token=body.get("token"), | ||
| user_id=user_id, # Track ownership | ||
| params=body.get("params"), | ||
|
|
@@ -2681,15 +2746,15 @@ async def settings_watch(request: Request) -> JSONResponse: | |
| from ..database.schema import Channel | ||
|
|
||
| resource_id = generate_resource_id() | ||
| expiration = body.get("expiration") | ||
| expiration_ms = parse_watch_expiration(body.get("expiration")) | ||
|
|
||
| channel = Channel( | ||
| id=channel_id, | ||
| resource_id=resource_id, | ||
| resource_uri=f"/users/{user_id}/settings", | ||
| type=channel_type, | ||
| address=address, | ||
| expiration=int(expiration) if expiration else None, | ||
| expiration=expiration_ms, | ||
| token=body.get("token"), | ||
| params=body.get("params"), | ||
| payload=body.get("payload", False), | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.