gsuite-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ """Google Suite Calendar - Simple Calendar API client."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from gsuite_calendar.calendar_entity import CalendarEntity
6
+ from gsuite_calendar.client import Calendar
7
+ from gsuite_calendar.event import Event
8
+
9
+ __all__ = [
10
+ "Calendar",
11
+ "Event",
12
+ "CalendarEntity",
13
+ ]
@@ -0,0 +1,31 @@
1
+ """Calendar entity."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class CalendarEntity:
8
+ """
9
+ A calendar.
10
+
11
+ Represents a single calendar (primary or shared).
12
+ """
13
+
14
+ id: str
15
+ summary: str
16
+ description: str | None = None
17
+ time_zone: str | None = None
18
+ primary: bool = False
19
+ access_role: str = "reader" # freeBusyReader, reader, writer, owner
20
+ background_color: str | None = None
21
+ foreground_color: str | None = None
22
+
23
+ @property
24
+ def is_primary(self) -> bool:
25
+ """Check if this is the user's primary calendar."""
26
+ return self.primary
27
+
28
+ @property
29
+ def is_writable(self) -> bool:
30
+ """Check if user can write to this calendar."""
31
+ return self.access_role in ("writer", "owner")
@@ -0,0 +1,268 @@
1
+ """Calendar client - high-level interface."""
2
+
3
+ import logging
4
+ from datetime import date, datetime, timedelta
5
+
6
+ from googleapiclient.discovery import build
7
+ from googleapiclient.errors import HttpError
8
+
9
+ from gsuite_calendar.calendar_entity import CalendarEntity
10
+ from gsuite_calendar.event import Event
11
+ from gsuite_calendar.parser import CalendarParser
12
+ from gsuite_core import GoogleAuth, get_settings
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class Calendar:
18
+ """
19
+ High-level Calendar client.
20
+
21
+ Example:
22
+ auth = GoogleAuth()
23
+ auth.authenticate()
24
+
25
+ cal = Calendar(auth)
26
+
27
+ # Get upcoming events
28
+ for event in cal.get_upcoming(days=7):
29
+ print(f"{event.start}: {event.summary}")
30
+
31
+ # Create event
32
+ cal.create_event(
33
+ summary="Meeting",
34
+ start=datetime(2026, 1, 30, 10, 0),
35
+ end=datetime(2026, 1, 30, 11, 0),
36
+ )
37
+ """
38
+
39
+ def __init__(self, auth: GoogleAuth, calendar_id: str = "primary"):
40
+ """
41
+ Initialize Calendar client.
42
+
43
+ Args:
44
+ auth: GoogleAuth instance with valid credentials
45
+ calendar_id: Default calendar ID ("primary" for main calendar)
46
+ """
47
+ self.auth = auth
48
+ self.calendar_id = calendar_id
49
+ self._service = None
50
+
51
+ @property
52
+ def service(self):
53
+ """Lazy-load Calendar API service."""
54
+ if self._service is None:
55
+ self._service = build("calendar", "v3", credentials=self.auth.credentials)
56
+ return self._service
57
+
58
+ # ========== Event retrieval ==========
59
+
60
+ def get_events(
61
+ self,
62
+ time_min: datetime | None = None,
63
+ time_max: datetime | None = None,
64
+ calendar_id: str | None = None,
65
+ max_results: int = 250,
66
+ single_events: bool = True,
67
+ order_by: str = "startTime",
68
+ ) -> list[Event]:
69
+ """
70
+ Get events in a time range.
71
+
72
+ Args:
73
+ time_min: Start of range (default: now)
74
+ time_max: End of range
75
+ calendar_id: Calendar ID (default: primary)
76
+ max_results: Maximum events to return
77
+ single_events: Expand recurring events
78
+ order_by: Sort order (startTime or updated)
79
+
80
+ Returns:
81
+ List of Event objects
82
+ """
83
+ cal_id = calendar_id or self.calendar_id
84
+ time_min = time_min or datetime.utcnow()
85
+
86
+ request_params = {
87
+ "calendarId": cal_id,
88
+ "timeMin": time_min.isoformat() + "Z",
89
+ "maxResults": max_results,
90
+ "singleEvents": single_events,
91
+ "orderBy": order_by,
92
+ }
93
+
94
+ if time_max:
95
+ request_params["timeMax"] = time_max.isoformat() + "Z"
96
+
97
+ response = self.service.events().list(**request_params).execute()
98
+
99
+ events = []
100
+ for event_data in response.get("items", []):
101
+ events.append(self._parse_event(event_data, cal_id))
102
+
103
+ return events
104
+
105
+ def get_upcoming(
106
+ self,
107
+ days: int = 7,
108
+ calendar_id: str | None = None,
109
+ max_results: int = 100,
110
+ ) -> list[Event]:
111
+ """
112
+ Get upcoming events.
113
+
114
+ Args:
115
+ days: Number of days ahead (default: 7)
116
+ calendar_id: Calendar ID
117
+ max_results: Maximum events
118
+
119
+ Returns:
120
+ List of upcoming events
121
+ """
122
+ time_min = datetime.utcnow()
123
+ time_max = time_min + timedelta(days=days)
124
+
125
+ return self.get_events(
126
+ time_min=time_min,
127
+ time_max=time_max,
128
+ calendar_id=calendar_id,
129
+ max_results=max_results,
130
+ )
131
+
132
+ def get_today(self, calendar_id: str | None = None) -> list[Event]:
133
+ """Get today's events."""
134
+ today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
135
+ tomorrow = today + timedelta(days=1)
136
+
137
+ return self.get_events(time_min=today, time_max=tomorrow, calendar_id=calendar_id)
138
+
139
+ def get_event(self, event_id: str, calendar_id: str | None = None) -> Event | None:
140
+ """Get a specific event by ID."""
141
+ cal_id = calendar_id or self.calendar_id
142
+
143
+ try:
144
+ event_data = (
145
+ self.service.events()
146
+ .get(
147
+ calendarId=cal_id,
148
+ eventId=event_id,
149
+ )
150
+ .execute()
151
+ )
152
+ return self._parse_event(event_data, cal_id)
153
+ except HttpError as e:
154
+ if e.resp.status == 404:
155
+ logger.debug(f"Event not found: {event_id}")
156
+ return None
157
+ logger.error(f"Error getting event {event_id}: {e}")
158
+ raise
159
+ except Exception as e:
160
+ logger.error(f"Unexpected error getting event {event_id}: {e}")
161
+ return None
162
+
163
+ # ========== Calendars ==========
164
+
165
+ def get_calendars(self) -> list[CalendarEntity]:
166
+ """Get all accessible calendars."""
167
+ response = self.service.calendarList().list().execute()
168
+
169
+ return [CalendarParser.parse_calendar(cal_data) for cal_data in response.get("items", [])]
170
+
171
+ # ========== Create/Update ==========
172
+
173
+ def create_event(
174
+ self,
175
+ summary: str,
176
+ start: datetime | date,
177
+ end: datetime | date | None = None,
178
+ description: str | None = None,
179
+ location: str | None = None,
180
+ attendees: list[str] | None = None,
181
+ calendar_id: str | None = None,
182
+ all_day: bool = False,
183
+ ) -> Event:
184
+ """
185
+ Create a new event.
186
+
187
+ Args:
188
+ summary: Event title
189
+ start: Start time (datetime) or date (for all-day)
190
+ end: End time (default: start + 1 hour)
191
+ description: Event description
192
+ location: Event location
193
+ attendees: List of attendee emails
194
+ calendar_id: Calendar to create in
195
+ all_day: Create as all-day event
196
+
197
+ Returns:
198
+ Created Event
199
+ """
200
+ cal_id = calendar_id or self.calendar_id
201
+
202
+ # Handle all-day events
203
+ if all_day or isinstance(start, date) and not isinstance(start, datetime):
204
+ start_body = {
205
+ "date": start.isoformat() if isinstance(start, date) else start.date().isoformat()
206
+ }
207
+ end_date = end or start
208
+ if isinstance(end_date, datetime):
209
+ end_date = end_date.date()
210
+ end_body = {"date": (end_date + timedelta(days=1)).isoformat()}
211
+ else:
212
+ if end is None:
213
+ end = start + timedelta(hours=1)
214
+ settings = get_settings()
215
+ tz = settings.default_timezone
216
+ start_body = {"dateTime": start.isoformat(), "timeZone": tz}
217
+ end_body = {"dateTime": end.isoformat(), "timeZone": tz}
218
+
219
+ event_body = {
220
+ "summary": summary,
221
+ "start": start_body,
222
+ "end": end_body,
223
+ }
224
+
225
+ if description:
226
+ event_body["description"] = description
227
+ if location:
228
+ event_body["location"] = location
229
+ if attendees:
230
+ event_body["attendees"] = [{"email": email} for email in attendees]
231
+
232
+ created = (
233
+ self.service.events()
234
+ .insert(
235
+ calendarId=cal_id,
236
+ body=event_body,
237
+ )
238
+ .execute()
239
+ )
240
+
241
+ return self._parse_event(created, cal_id)
242
+
243
+ def delete_event(self, event_id: str, calendar_id: str | None = None) -> bool:
244
+ """Delete an event."""
245
+ cal_id = calendar_id or self.calendar_id
246
+
247
+ try:
248
+ self.service.events().delete(
249
+ calendarId=cal_id,
250
+ eventId=event_id,
251
+ ).execute()
252
+ logger.info(f"Deleted event {event_id}")
253
+ return True
254
+ except HttpError as e:
255
+ if e.resp.status == 404:
256
+ logger.warning(f"Event not found for deletion: {event_id}")
257
+ else:
258
+ logger.error(f"Error deleting event {event_id}: {e}")
259
+ return False
260
+ except Exception as e:
261
+ logger.error(f"Unexpected error deleting event {event_id}: {e}")
262
+ return False
263
+
264
+ # ========== Parsing ==========
265
+
266
+ def _parse_event(self, data: dict, calendar_id: str) -> Event:
267
+ """Parse Calendar API response to Event object."""
268
+ return CalendarParser.parse_event(data, calendar_id)
@@ -0,0 +1,57 @@
1
+ """Calendar Event entity."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+
6
+
7
+ @dataclass
8
+ class Attendee:
9
+ """Event attendee."""
10
+
11
+ email: str
12
+ name: str | None = None
13
+ response_status: str = "needsAction" # needsAction, declined, tentative, accepted
14
+ organizer: bool = False
15
+ self_: bool = False
16
+
17
+
18
+ @dataclass
19
+ class Event:
20
+ """
21
+ Calendar event.
22
+
23
+ Represents a single calendar event with all its metadata.
24
+ """
25
+
26
+ id: str
27
+ summary: str
28
+ description: str | None = None
29
+ location: str | None = None
30
+ start: datetime | None = None
31
+ end: datetime | None = None
32
+ all_day: bool = False
33
+ recurring: bool = False
34
+ recurrence: list[str] | None = None
35
+ attendees: list[Attendee] = field(default_factory=list)
36
+ organizer: str | None = None
37
+ calendar_id: str = "primary"
38
+ html_link: str | None = None
39
+ status: str = "confirmed" # confirmed, tentative, cancelled
40
+
41
+ @property
42
+ def duration_minutes(self) -> int | None:
43
+ """Get event duration in minutes."""
44
+ if self.start and self.end:
45
+ delta = self.end - self.start
46
+ return int(delta.total_seconds() / 60)
47
+ return None
48
+
49
+ @property
50
+ def is_all_day(self) -> bool:
51
+ """Check if this is an all-day event."""
52
+ return self.all_day
53
+
54
+ @property
55
+ def is_recurring(self) -> bool:
56
+ """Check if this is a recurring event."""
57
+ return self.recurring or bool(self.recurrence)
@@ -0,0 +1,119 @@
1
+ """Calendar response parsers - converts API responses to domain entities."""
2
+
3
+ from datetime import datetime
4
+
5
+ from gsuite_calendar.calendar_entity import CalendarEntity
6
+ from gsuite_calendar.event import Attendee, Event
7
+
8
+
9
+ class CalendarParser:
10
+ """Parser for Calendar API responses."""
11
+
12
+ @staticmethod
13
+ def parse_event(data: dict, calendar_id: str) -> Event:
14
+ """
15
+ Parse Calendar API response to Event entity.
16
+
17
+ Args:
18
+ data: Raw API response dict
19
+ calendar_id: Calendar ID the event belongs to
20
+
21
+ Returns:
22
+ Event entity
23
+ """
24
+ # Parse start/end times
25
+ start_data = data.get("start", {})
26
+ end_data = data.get("end", {})
27
+
28
+ all_day = "date" in start_data
29
+
30
+ if all_day:
31
+ start = CalendarParser._parse_date(start_data.get("date"))
32
+ end = CalendarParser._parse_date(end_data.get("date"))
33
+ else:
34
+ start = CalendarParser._parse_datetime(start_data.get("dateTime"))
35
+ end = CalendarParser._parse_datetime(end_data.get("dateTime"))
36
+
37
+ # Parse attendees
38
+ attendees = [
39
+ CalendarParser.parse_attendee(att_data) for att_data in data.get("attendees", [])
40
+ ]
41
+
42
+ return Event(
43
+ id=data["id"],
44
+ summary=data.get("summary", ""),
45
+ description=data.get("description"),
46
+ location=data.get("location"),
47
+ start=start,
48
+ end=end,
49
+ all_day=all_day,
50
+ recurring="recurringEventId" in data,
51
+ recurrence=data.get("recurrence"),
52
+ attendees=attendees,
53
+ organizer=data.get("organizer", {}).get("email"),
54
+ calendar_id=calendar_id,
55
+ html_link=data.get("htmlLink"),
56
+ status=data.get("status", "confirmed"),
57
+ )
58
+
59
+ @staticmethod
60
+ def parse_attendee(data: dict) -> Attendee:
61
+ """
62
+ Parse attendee data to Attendee entity.
63
+
64
+ Args:
65
+ data: Raw attendee dict from API
66
+
67
+ Returns:
68
+ Attendee entity
69
+ """
70
+ return Attendee(
71
+ email=data.get("email", ""),
72
+ name=data.get("displayName"),
73
+ response_status=data.get("responseStatus", "needsAction"),
74
+ organizer=data.get("organizer", False),
75
+ self_=data.get("self", False),
76
+ )
77
+
78
+ @staticmethod
79
+ def parse_calendar(data: dict) -> CalendarEntity:
80
+ """
81
+ Parse Calendar API response to CalendarEntity.
82
+
83
+ Args:
84
+ data: Raw API response dict
85
+
86
+ Returns:
87
+ CalendarEntity
88
+ """
89
+ return CalendarEntity(
90
+ id=data["id"],
91
+ summary=data.get("summary", ""),
92
+ description=data.get("description"),
93
+ time_zone=data.get("timeZone"),
94
+ primary=data.get("primary", False),
95
+ access_role=data.get("accessRole", "reader"),
96
+ background_color=data.get("backgroundColor"),
97
+ foreground_color=data.get("foregroundColor"),
98
+ )
99
+
100
+ @staticmethod
101
+ def _parse_datetime(dt_string: str | None) -> datetime | None:
102
+ """Parse ISO datetime string to datetime object."""
103
+ if not dt_string:
104
+ return None
105
+ try:
106
+ # Handle Z suffix and timezone offset
107
+ return datetime.fromisoformat(dt_string.replace("Z", "+00:00"))
108
+ except (ValueError, TypeError):
109
+ return None
110
+
111
+ @staticmethod
112
+ def _parse_date(date_string: str | None) -> datetime | None:
113
+ """Parse ISO date string to datetime object."""
114
+ if not date_string:
115
+ return None
116
+ try:
117
+ return datetime.fromisoformat(date_string)
118
+ except (ValueError, TypeError):
119
+ return None
File without changes
@@ -0,0 +1,62 @@
1
+ """Google Suite Core - Shared auth, config, and utilities."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from gsuite_core.api_utils import api_call, api_call_optional, map_http_error
6
+ from gsuite_core.auth.oauth import GoogleAuth
7
+ from gsuite_core.auth.scopes import Scopes
8
+ from gsuite_core.config import Settings, get_settings
9
+ from gsuite_core.exceptions import (
10
+ APIError,
11
+ AuthenticationError,
12
+ ConfigurationError,
13
+ CredentialsNotFoundError,
14
+ GSuiteError,
15
+ NotAuthenticatedError,
16
+ NotFoundError,
17
+ PermissionDeniedError,
18
+ QuotaExceededError,
19
+ RateLimitError,
20
+ TokenExpiredError,
21
+ TokenRefreshError,
22
+ ValidationError,
23
+ )
24
+ from gsuite_core.storage import SQLiteTokenStore, TokenStore
25
+
26
+ __all__ = [
27
+ # Auth
28
+ "GoogleAuth",
29
+ "Scopes",
30
+ # Config
31
+ "Settings",
32
+ "get_settings",
33
+ # API Utils
34
+ "api_call",
35
+ "api_call_optional",
36
+ "map_http_error",
37
+ # Storage
38
+ "TokenStore",
39
+ "SQLiteTokenStore",
40
+ # Exceptions
41
+ "GSuiteError",
42
+ "AuthenticationError",
43
+ "CredentialsNotFoundError",
44
+ "TokenExpiredError",
45
+ "TokenRefreshError",
46
+ "NotAuthenticatedError",
47
+ "APIError",
48
+ "RateLimitError",
49
+ "QuotaExceededError",
50
+ "NotFoundError",
51
+ "PermissionDeniedError",
52
+ "ValidationError",
53
+ "ConfigurationError",
54
+ ]
55
+
56
+ # Conditionally export SecretManagerTokenStore
57
+ try:
58
+ from gsuite_core.storage import SecretManagerTokenStore
59
+
60
+ __all__.append("SecretManagerTokenStore")
61
+ except ImportError:
62
+ pass