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.
- gsuite_calendar/__init__.py +13 -0
- gsuite_calendar/calendar_entity.py +31 -0
- gsuite_calendar/client.py +268 -0
- gsuite_calendar/event.py +57 -0
- gsuite_calendar/parser.py +119 -0
- gsuite_calendar/py.typed +0 -0
- gsuite_core/__init__.py +62 -0
- gsuite_core/api_utils.py +167 -0
- gsuite_core/auth/__init__.py +6 -0
- gsuite_core/auth/oauth.py +249 -0
- gsuite_core/auth/scopes.py +84 -0
- gsuite_core/config.py +73 -0
- gsuite_core/exceptions.py +125 -0
- gsuite_core/py.typed +0 -0
- gsuite_core/storage/__init__.py +13 -0
- gsuite_core/storage/base.py +65 -0
- gsuite_core/storage/secretmanager.py +141 -0
- gsuite_core/storage/sqlite.py +79 -0
- gsuite_drive/__init__.py +12 -0
- gsuite_drive/client.py +401 -0
- gsuite_drive/file.py +103 -0
- gsuite_drive/parser.py +66 -0
- gsuite_drive/py.typed +0 -0
- gsuite_gmail/__init__.py +17 -0
- gsuite_gmail/client.py +412 -0
- gsuite_gmail/label.py +56 -0
- gsuite_gmail/message.py +211 -0
- gsuite_gmail/parser.py +155 -0
- gsuite_gmail/py.typed +0 -0
- gsuite_gmail/query.py +227 -0
- gsuite_gmail/thread.py +54 -0
- gsuite_sdk-0.1.0.dist-info/METADATA +384 -0
- gsuite_sdk-0.1.0.dist-info/RECORD +42 -0
- gsuite_sdk-0.1.0.dist-info/WHEEL +5 -0
- gsuite_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- gsuite_sdk-0.1.0.dist-info/top_level.txt +5 -0
- gsuite_sheets/__init__.py +13 -0
- gsuite_sheets/client.py +375 -0
- gsuite_sheets/parser.py +76 -0
- gsuite_sheets/py.typed +0 -0
- gsuite_sheets/spreadsheet.py +97 -0
- gsuite_sheets/worksheet.py +185 -0
|
@@ -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)
|
gsuite_calendar/event.py
ADDED
|
@@ -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
|
gsuite_calendar/py.typed
ADDED
|
File without changes
|
gsuite_core/__init__.py
ADDED
|
@@ -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
|