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
gsuite_gmail/parser.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Gmail response parsers - converts API responses to domain entities."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from email.utils import parsedate_to_datetime
|
|
6
|
+
|
|
7
|
+
from gsuite_gmail.label import Label, LabelType
|
|
8
|
+
from gsuite_gmail.message import Attachment, Message
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GmailParser:
|
|
12
|
+
"""Parser for Gmail API responses."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def parse_message(
|
|
16
|
+
data: dict,
|
|
17
|
+
include_body: bool = True,
|
|
18
|
+
) -> Message:
|
|
19
|
+
"""
|
|
20
|
+
Parse Gmail API response to Message entity.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
data: Raw API response dict
|
|
24
|
+
include_body: Whether to parse body content
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Message entity
|
|
28
|
+
"""
|
|
29
|
+
payload = data.get("payload", {})
|
|
30
|
+
headers = payload.get("headers", [])
|
|
31
|
+
|
|
32
|
+
def get_header(name: str) -> str:
|
|
33
|
+
for h in headers:
|
|
34
|
+
if h.get("name", "").lower() == name.lower():
|
|
35
|
+
return h.get("value", "")
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
# Parse date
|
|
39
|
+
date = GmailParser._parse_date(get_header("Date"), data.get("internalDate"))
|
|
40
|
+
|
|
41
|
+
# Parse body and attachments
|
|
42
|
+
plain, html, attachments = None, None, []
|
|
43
|
+
if include_body:
|
|
44
|
+
plain, html, attachments = GmailParser._parse_payload(payload, data["id"])
|
|
45
|
+
|
|
46
|
+
return Message(
|
|
47
|
+
id=data["id"],
|
|
48
|
+
thread_id=data.get("threadId", data["id"]),
|
|
49
|
+
subject=get_header("Subject"),
|
|
50
|
+
sender=get_header("From"),
|
|
51
|
+
recipient=get_header("To"),
|
|
52
|
+
cc=[addr.strip() for addr in get_header("Cc").split(",") if addr.strip()],
|
|
53
|
+
date=date,
|
|
54
|
+
snippet=data.get("snippet", ""),
|
|
55
|
+
plain=plain,
|
|
56
|
+
html=html,
|
|
57
|
+
labels=data.get("labelIds", []),
|
|
58
|
+
attachments=attachments,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def parse_label(data: dict) -> Label:
|
|
63
|
+
"""
|
|
64
|
+
Parse Gmail API response to Label entity.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
data: Raw API response dict
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Label entity
|
|
71
|
+
"""
|
|
72
|
+
return Label(
|
|
73
|
+
id=data["id"],
|
|
74
|
+
name=data.get("name", data["id"]),
|
|
75
|
+
type=LabelType.SYSTEM if data.get("type") == "system" else LabelType.USER,
|
|
76
|
+
messages_total=data.get("messagesTotal", 0),
|
|
77
|
+
messages_unread=data.get("messagesUnread", 0),
|
|
78
|
+
threads_total=data.get("threadsTotal", 0),
|
|
79
|
+
threads_unread=data.get("threadsUnread", 0),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _parse_date(date_header: str, internal_date: str | None) -> datetime | None:
|
|
84
|
+
"""Parse date from header or internal timestamp."""
|
|
85
|
+
if date_header:
|
|
86
|
+
try:
|
|
87
|
+
return parsedate_to_datetime(date_header)
|
|
88
|
+
except (ValueError, TypeError):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
if internal_date:
|
|
92
|
+
try:
|
|
93
|
+
return datetime.fromtimestamp(int(internal_date) / 1000)
|
|
94
|
+
except (ValueError, TypeError):
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def _parse_payload(
|
|
101
|
+
payload: dict,
|
|
102
|
+
message_id: str,
|
|
103
|
+
) -> tuple[str | None, str | None, list[Attachment]]:
|
|
104
|
+
"""
|
|
105
|
+
Parse message payload for body content and attachments.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
payload: Message payload dict
|
|
109
|
+
message_id: Parent message ID
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Tuple of (plain_text, html, attachments)
|
|
113
|
+
"""
|
|
114
|
+
plain = None
|
|
115
|
+
html = None
|
|
116
|
+
attachments = []
|
|
117
|
+
|
|
118
|
+
def extract_parts(part: dict) -> None:
|
|
119
|
+
nonlocal plain, html
|
|
120
|
+
|
|
121
|
+
mime_type = part.get("mimeType", "")
|
|
122
|
+
body = part.get("body", {})
|
|
123
|
+
data = body.get("data")
|
|
124
|
+
|
|
125
|
+
# Text content
|
|
126
|
+
if data:
|
|
127
|
+
try:
|
|
128
|
+
decoded = base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
|
|
129
|
+
if mime_type == "text/plain" and not plain:
|
|
130
|
+
plain = decoded
|
|
131
|
+
elif mime_type == "text/html" and not html:
|
|
132
|
+
html = decoded
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
# Attachments
|
|
137
|
+
filename = part.get("filename")
|
|
138
|
+
attachment_id = body.get("attachmentId")
|
|
139
|
+
if filename and attachment_id:
|
|
140
|
+
attachments.append(
|
|
141
|
+
Attachment(
|
|
142
|
+
id=attachment_id,
|
|
143
|
+
filename=filename,
|
|
144
|
+
mime_type=mime_type,
|
|
145
|
+
size=body.get("size", 0),
|
|
146
|
+
_message_id=message_id,
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Recurse into nested parts
|
|
151
|
+
for sub_part in part.get("parts", []):
|
|
152
|
+
extract_parts(sub_part)
|
|
153
|
+
|
|
154
|
+
extract_parts(payload)
|
|
155
|
+
return plain, html, attachments
|
gsuite_gmail/py.typed
ADDED
|
File without changes
|
gsuite_gmail/query.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Gmail query builder for intuitive searches."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Query:
|
|
8
|
+
"""
|
|
9
|
+
Gmail search query builder.
|
|
10
|
+
|
|
11
|
+
Supports combining queries with & (AND) and | (OR).
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
q = newer_than(days=7) & has_attachment() & from_("boss@company.com")
|
|
15
|
+
messages = gmail.search(q)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
_query: str
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
return self._query
|
|
22
|
+
|
|
23
|
+
def __and__(self, other: "Query") -> "Query":
|
|
24
|
+
"""Combine with AND."""
|
|
25
|
+
return Query(f"{self._query} {other._query}")
|
|
26
|
+
|
|
27
|
+
def __or__(self, other: "Query") -> "Query":
|
|
28
|
+
"""Combine with OR."""
|
|
29
|
+
return Query(f"({self._query}) OR ({other._query})")
|
|
30
|
+
|
|
31
|
+
def __invert__(self) -> "Query":
|
|
32
|
+
"""Negate query."""
|
|
33
|
+
return Query(f"-({self._query})")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def from_(address: str) -> Query:
|
|
37
|
+
"""Messages from a specific sender."""
|
|
38
|
+
return Query(f"from:{address}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def to(address: str) -> Query:
|
|
42
|
+
"""Messages to a specific recipient."""
|
|
43
|
+
return Query(f"to:{address}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def subject(text: str) -> Query:
|
|
47
|
+
"""Messages with subject containing text."""
|
|
48
|
+
if " " in text:
|
|
49
|
+
return Query(f'subject:"{text}"')
|
|
50
|
+
return Query(f"subject:{text}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def has_words(words: str) -> Query:
|
|
54
|
+
"""Messages containing specific words."""
|
|
55
|
+
return Query(words)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def has_attachment() -> Query:
|
|
59
|
+
"""Messages with attachments."""
|
|
60
|
+
return Query("has:attachment")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_unread() -> Query:
|
|
64
|
+
"""Unread messages."""
|
|
65
|
+
return Query("is:unread")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_read() -> Query:
|
|
69
|
+
"""Read messages."""
|
|
70
|
+
return Query("is:read")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_starred() -> Query:
|
|
74
|
+
"""Starred messages."""
|
|
75
|
+
return Query("is:starred")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_important() -> Query:
|
|
79
|
+
"""Important messages."""
|
|
80
|
+
return Query("is:important")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def in_inbox() -> Query:
|
|
84
|
+
"""Messages in inbox."""
|
|
85
|
+
return Query("in:inbox")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def in_sent() -> Query:
|
|
89
|
+
"""Sent messages."""
|
|
90
|
+
return Query("in:sent")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def in_drafts() -> Query:
|
|
94
|
+
"""Draft messages."""
|
|
95
|
+
return Query("in:drafts")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def in_trash() -> Query:
|
|
99
|
+
"""Trashed messages."""
|
|
100
|
+
return Query("in:trash")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def in_spam() -> Query:
|
|
104
|
+
"""Spam messages."""
|
|
105
|
+
return Query("in:spam")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def label(name: str) -> Query:
|
|
109
|
+
"""Messages with a specific label."""
|
|
110
|
+
if " " in name:
|
|
111
|
+
return Query(f'label:"{name}"')
|
|
112
|
+
return Query(f"label:{name}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def newer_than(days: int | None = None, months: int | None = None) -> Query:
|
|
116
|
+
"""Messages newer than a time period."""
|
|
117
|
+
if days:
|
|
118
|
+
return Query(f"newer_than:{days}d")
|
|
119
|
+
if months:
|
|
120
|
+
return Query(f"newer_than:{months}m")
|
|
121
|
+
raise ValueError("Specify days or months")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def older_than(days: int | None = None, months: int | None = None) -> Query:
|
|
125
|
+
"""Messages older than a time period."""
|
|
126
|
+
if days:
|
|
127
|
+
return Query(f"older_than:{days}d")
|
|
128
|
+
if months:
|
|
129
|
+
return Query(f"older_than:{months}m")
|
|
130
|
+
raise ValueError("Specify days or months")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def after(date: str) -> Query:
|
|
134
|
+
"""Messages after a date (YYYY/MM/DD)."""
|
|
135
|
+
return Query(f"after:{date}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def before(date: str) -> Query:
|
|
139
|
+
"""Messages before a date (YYYY/MM/DD)."""
|
|
140
|
+
return Query(f"before:{date}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def filename(name: str) -> Query:
|
|
144
|
+
"""Messages with attachment matching filename."""
|
|
145
|
+
return Query(f"filename:{name}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def size_larger(bytes: int) -> Query:
|
|
149
|
+
"""Messages larger than size in bytes."""
|
|
150
|
+
return Query(f"larger:{bytes}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def size_smaller(bytes: int) -> Query:
|
|
154
|
+
"""Messages smaller than size in bytes."""
|
|
155
|
+
return Query(f"smaller:{bytes}")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def category(name: str) -> Query:
|
|
159
|
+
"""Messages in a category (primary, social, promotions, updates, forums)."""
|
|
160
|
+
return Query(f"category:{name}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def raw(query_string: str) -> Query:
|
|
164
|
+
"""Raw Gmail query string."""
|
|
165
|
+
return Query(query_string)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Convenience function to build query from dict (like simplegmail)
|
|
169
|
+
def construct_query(
|
|
170
|
+
from_: str | None = None,
|
|
171
|
+
to: str | None = None,
|
|
172
|
+
subject: str | None = None,
|
|
173
|
+
unread: bool | None = None,
|
|
174
|
+
starred: bool | None = None,
|
|
175
|
+
newer_than: tuple[int, str] | None = None, # (2, "day")
|
|
176
|
+
older_than: tuple[int, str] | None = None,
|
|
177
|
+
labels: list[str] | None = None,
|
|
178
|
+
has_attachment: bool | None = None,
|
|
179
|
+
exclude_starred: bool | None = None,
|
|
180
|
+
) -> Query:
|
|
181
|
+
"""
|
|
182
|
+
Build query from parameters.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
from_: Sender email
|
|
186
|
+
to: Recipient email
|
|
187
|
+
subject: Subject contains
|
|
188
|
+
unread: True for unread only
|
|
189
|
+
starred: True for starred only
|
|
190
|
+
newer_than: Tuple of (amount, unit) e.g., (2, "day")
|
|
191
|
+
older_than: Tuple of (amount, unit)
|
|
192
|
+
labels: List of label names
|
|
193
|
+
has_attachment: True for messages with attachments
|
|
194
|
+
exclude_starred: True to exclude starred
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Query object
|
|
198
|
+
"""
|
|
199
|
+
parts = []
|
|
200
|
+
|
|
201
|
+
if from_:
|
|
202
|
+
parts.append(f"from:{from_}")
|
|
203
|
+
if to:
|
|
204
|
+
parts.append(f"to:{to}")
|
|
205
|
+
if subject:
|
|
206
|
+
parts.append(f'subject:"{subject}"' if " " in subject else f"subject:{subject}")
|
|
207
|
+
if unread:
|
|
208
|
+
parts.append("is:unread")
|
|
209
|
+
if starred:
|
|
210
|
+
parts.append("is:starred")
|
|
211
|
+
if exclude_starred:
|
|
212
|
+
parts.append("-is:starred")
|
|
213
|
+
if newer_than:
|
|
214
|
+
amount, unit = newer_than
|
|
215
|
+
unit_map = {"day": "d", "days": "d", "d": "d", "month": "m", "months": "m", "m": "m"}
|
|
216
|
+
parts.append(f"newer_than:{amount}{unit_map.get(unit, unit)}")
|
|
217
|
+
if older_than:
|
|
218
|
+
amount, unit = older_than
|
|
219
|
+
unit_map = {"day": "d", "days": "d", "d": "d", "month": "m", "months": "m", "m": "m"}
|
|
220
|
+
parts.append(f"older_than:{amount}{unit_map.get(unit, unit)}")
|
|
221
|
+
if labels:
|
|
222
|
+
for lbl in labels:
|
|
223
|
+
parts.append(f'label:"{lbl}"' if " " in lbl else f"label:{lbl}")
|
|
224
|
+
if has_attachment:
|
|
225
|
+
parts.append("has:attachment")
|
|
226
|
+
|
|
227
|
+
return Query(" ".join(parts))
|
gsuite_gmail/thread.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Gmail Thread entity."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from gsuite_gmail.message import Message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Thread:
|
|
12
|
+
"""
|
|
13
|
+
Gmail thread (conversation).
|
|
14
|
+
|
|
15
|
+
A thread is a collection of related messages.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
id: str
|
|
19
|
+
messages: list["Message"] = field(default_factory=list)
|
|
20
|
+
snippet: str = ""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def subject(self) -> str:
|
|
24
|
+
"""Get thread subject from first message."""
|
|
25
|
+
if self.messages:
|
|
26
|
+
return self.messages[0].subject
|
|
27
|
+
return ""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def message_count(self) -> int:
|
|
31
|
+
"""Get number of messages in thread."""
|
|
32
|
+
return len(self.messages)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def participants(self) -> set[str]:
|
|
36
|
+
"""Get all unique email addresses in thread."""
|
|
37
|
+
emails = set()
|
|
38
|
+
for msg in self.messages:
|
|
39
|
+
emails.add(msg.sender)
|
|
40
|
+
emails.add(msg.recipient)
|
|
41
|
+
return emails
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def has_unread(self) -> bool:
|
|
45
|
+
"""Check if thread has any unread messages."""
|
|
46
|
+
return any(msg.is_unread for msg in self.messages)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def labels(self) -> set[str]:
|
|
50
|
+
"""Get all unique labels from all messages."""
|
|
51
|
+
all_labels = set()
|
|
52
|
+
for msg in self.messages:
|
|
53
|
+
all_labels.update(msg.labels)
|
|
54
|
+
return all_labels
|