arcade-google 0.1.6__py3-none-any.whl → 2.0.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.
- arcade_google/constants.py +24 -0
- arcade_google/critics.py +41 -0
- arcade_google/doc_to_html.py +99 -0
- arcade_google/doc_to_markdown.py +64 -0
- arcade_google/enums.py +0 -0
- arcade_google/exceptions.py +70 -0
- arcade_google/models.py +654 -0
- arcade_google/tools/__init__.py +96 -1
- arcade_google/tools/calendar.py +236 -32
- arcade_google/tools/contacts.py +96 -0
- arcade_google/tools/docs.py +24 -14
- arcade_google/tools/drive.py +256 -48
- arcade_google/tools/file_picker.py +54 -0
- arcade_google/tools/gmail.py +336 -116
- arcade_google/tools/sheets.py +144 -0
- arcade_google/utils.py +1564 -0
- arcade_google-2.0.0.dist-info/METADATA +27 -0
- arcade_google-2.0.0.dist-info/RECORD +21 -0
- {arcade_google-0.1.6.dist-info → arcade_google-2.0.0.dist-info}/WHEEL +1 -1
- arcade_google-2.0.0.dist-info/licenses/LICENSE +21 -0
- arcade_google/tools/models.py +0 -296
- arcade_google/tools/utils.py +0 -282
- arcade_google-0.1.6.dist-info/METADATA +0 -20
- arcade_google-0.1.6.dist-info/RECORD +0 -11
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from arcade_google.models import GmailReplyToWhom
|
|
4
|
+
|
|
5
|
+
# The default reply in Gmail is to only the sender. Since Gmail also offers the possibility of
|
|
6
|
+
# changing the default to 'reply to all', we support both options through an env variable.
|
|
7
|
+
# https://support.google.com/mail/answer/6585?hl=en&sjid=15399867888091633568-SA#null
|
|
8
|
+
try:
|
|
9
|
+
GMAIL_DEFAULT_REPLY_TO = GmailReplyToWhom(
|
|
10
|
+
# Values accepted are defined in the arcade_google.tools.models.GmailReplyToWhom Enum
|
|
11
|
+
os.getenv("ARCADE_GMAIL_DEFAULT_REPLY_TO", GmailReplyToWhom.ONLY_THE_SENDER.value).lower()
|
|
12
|
+
)
|
|
13
|
+
except ValueError as e:
|
|
14
|
+
raise ValueError(
|
|
15
|
+
"Invalid value for ARCADE_GMAIL_DEFAULT_REPLY_TO: "
|
|
16
|
+
f"'{os.getenv('ARCADE_GMAIL_DEFAULT_REPLY_TO')}'. Expected one of "
|
|
17
|
+
f"{list(GmailReplyToWhom.__members__.keys())}"
|
|
18
|
+
) from e
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_SEARCH_CONTACTS_LIMIT = 30
|
|
22
|
+
|
|
23
|
+
DEFAULT_SHEET_ROW_COUNT = 1000
|
|
24
|
+
DEFAULT_SHEET_COLUMN_COUNT = 26
|
arcade_google/critics.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from collections.abc import Collection
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from arcade_evals import DatetimeCritic
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DatetimeOrNoneCritic(DatetimeCritic):
|
|
8
|
+
"""
|
|
9
|
+
A critic that evaluates the closeness of datetime values within a specified tolerance or whether
|
|
10
|
+
it's a None value.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
tolerance: Acceptable timedelta between expected and actual datetimes.
|
|
14
|
+
max_difference: Maximum timedelta for a partial score.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def evaluate(self, expected: Any, actual: Any) -> dict[str, Any]:
|
|
18
|
+
if actual is None:
|
|
19
|
+
return {"match": True, "score": self.weight}
|
|
20
|
+
return super().evaluate(expected, actual)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AnyDatetimeCritic(DatetimeCritic):
|
|
24
|
+
"""
|
|
25
|
+
A critic that evaluates the closeness of datetime values within a list of expected values.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def evaluate(self, expected: Any, actual: Any) -> dict[str, Any]:
|
|
29
|
+
if not isinstance(expected, Collection):
|
|
30
|
+
expected = [expected]
|
|
31
|
+
for expected_value in expected:
|
|
32
|
+
critic = DatetimeCritic(
|
|
33
|
+
critic_field=self.critic_field,
|
|
34
|
+
weight=self.weight,
|
|
35
|
+
tolerance=self.tolerance,
|
|
36
|
+
max_difference=self.max_difference,
|
|
37
|
+
)
|
|
38
|
+
result = critic.evaluate(expected_value, actual)
|
|
39
|
+
if result["match"]:
|
|
40
|
+
return result
|
|
41
|
+
return {"match": False, "score": 0}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
def convert_document_to_html(document: dict) -> str:
|
|
2
|
+
html = (
|
|
3
|
+
"<html><head>"
|
|
4
|
+
f"<title>{document['title']}</title>"
|
|
5
|
+
f'<meta name="documentId" content="{document["documentId"]}">'
|
|
6
|
+
"</head><body>"
|
|
7
|
+
)
|
|
8
|
+
for element in document["body"]["content"]:
|
|
9
|
+
html += convert_structural_element(element)
|
|
10
|
+
html += "</body></html>"
|
|
11
|
+
return html
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def convert_structural_element(element: dict, wrap_paragraphs: bool = True) -> str:
|
|
15
|
+
if "sectionBreak" in element or "tableOfContents" in element:
|
|
16
|
+
return ""
|
|
17
|
+
|
|
18
|
+
elif "paragraph" in element:
|
|
19
|
+
paragraph_content = ""
|
|
20
|
+
|
|
21
|
+
prepend, append = get_paragraph_style_tags(
|
|
22
|
+
style=element["paragraph"]["paragraphStyle"],
|
|
23
|
+
wrap_paragraphs=wrap_paragraphs,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
for item in element["paragraph"]["elements"]:
|
|
27
|
+
if "textRun" not in item:
|
|
28
|
+
continue
|
|
29
|
+
paragraph_content += extract_paragraph_content(item["textRun"])
|
|
30
|
+
|
|
31
|
+
if not paragraph_content:
|
|
32
|
+
return ""
|
|
33
|
+
|
|
34
|
+
return f"{prepend}{paragraph_content.strip()}{append}"
|
|
35
|
+
|
|
36
|
+
elif "table" in element:
|
|
37
|
+
table = [
|
|
38
|
+
[
|
|
39
|
+
"".join([
|
|
40
|
+
convert_structural_element(element=cell_element, wrap_paragraphs=False)
|
|
41
|
+
for cell_element in cell["content"]
|
|
42
|
+
])
|
|
43
|
+
for cell in row["tableCells"]
|
|
44
|
+
]
|
|
45
|
+
for row in element["table"]["tableRows"]
|
|
46
|
+
]
|
|
47
|
+
return table_list_to_html(table)
|
|
48
|
+
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError(f"Unknown document body element type: {element}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_paragraph_content(text_run: dict) -> str:
|
|
54
|
+
content = text_run["content"]
|
|
55
|
+
style = text_run["textStyle"]
|
|
56
|
+
return apply_text_style(content, style)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def apply_text_style(content: str, style: dict) -> str:
|
|
60
|
+
content = content.rstrip("\n")
|
|
61
|
+
content = content.replace("\n", "<br>")
|
|
62
|
+
italic = style.get("italic", False)
|
|
63
|
+
bold = style.get("bold", False)
|
|
64
|
+
if italic:
|
|
65
|
+
content = f"<i>{content}</i>"
|
|
66
|
+
if bold:
|
|
67
|
+
content = f"<b>{content}</b>"
|
|
68
|
+
return content
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_paragraph_style_tags(style: dict, wrap_paragraphs: bool = True) -> tuple[str, str]:
|
|
72
|
+
named_style = style["namedStyleType"]
|
|
73
|
+
if named_style == "NORMAL_TEXT":
|
|
74
|
+
return ("<p>", "</p>") if wrap_paragraphs else ("", "")
|
|
75
|
+
elif named_style == "TITLE":
|
|
76
|
+
return "<h1>", "</h1>"
|
|
77
|
+
elif named_style == "SUBTITLE":
|
|
78
|
+
return "<h2>", "</h2>"
|
|
79
|
+
elif named_style.startswith("HEADING_"):
|
|
80
|
+
try:
|
|
81
|
+
heading_level = int(named_style.split("_")[1])
|
|
82
|
+
except ValueError:
|
|
83
|
+
return ("<p>", "</p>") if wrap_paragraphs else ("", "")
|
|
84
|
+
else:
|
|
85
|
+
return f"<h{heading_level}>", f"</h{heading_level}>"
|
|
86
|
+
return ("<p>", "</p>") if wrap_paragraphs else ("", "")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def table_list_to_html(table: list[list[str]]) -> str:
|
|
90
|
+
html = "<table>"
|
|
91
|
+
for row in table:
|
|
92
|
+
html += "<tr>"
|
|
93
|
+
for cell in row:
|
|
94
|
+
if cell.endswith("<br>"):
|
|
95
|
+
cell = cell[:-4]
|
|
96
|
+
html += f"<td>{cell}</td>"
|
|
97
|
+
html += "</tr>"
|
|
98
|
+
html += "</table>"
|
|
99
|
+
return html
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import arcade_google.doc_to_html as doc_to_html
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def convert_document_to_markdown(document: dict) -> str:
|
|
5
|
+
md = f"---\ntitle: {document['title']}\ndocumentId: {document['documentId']}\n---\n"
|
|
6
|
+
for element in document["body"]["content"]:
|
|
7
|
+
md += convert_structural_element(element)
|
|
8
|
+
return md
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def convert_structural_element(element: dict) -> str:
|
|
12
|
+
if "sectionBreak" in element or "tableOfContents" in element:
|
|
13
|
+
return ""
|
|
14
|
+
|
|
15
|
+
elif "paragraph" in element:
|
|
16
|
+
md = ""
|
|
17
|
+
prepend = get_paragraph_style_prepend_str(element["paragraph"]["paragraphStyle"])
|
|
18
|
+
for item in element["paragraph"]["elements"]:
|
|
19
|
+
if "textRun" not in item:
|
|
20
|
+
continue
|
|
21
|
+
content = extract_paragraph_content(item["textRun"])
|
|
22
|
+
md += f"{prepend}{content}"
|
|
23
|
+
return md
|
|
24
|
+
|
|
25
|
+
elif "table" in element:
|
|
26
|
+
return doc_to_html.convert_structural_element(element)
|
|
27
|
+
|
|
28
|
+
else:
|
|
29
|
+
raise ValueError(f"Unknown document body element type: {element}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_paragraph_content(text_run: dict) -> str:
|
|
33
|
+
content = text_run["content"]
|
|
34
|
+
style = text_run["textStyle"]
|
|
35
|
+
return apply_text_style(content, style)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def apply_text_style(content: str, style: dict) -> str:
|
|
39
|
+
append = "\n" if content.endswith("\n") else ""
|
|
40
|
+
content = content.rstrip("\n")
|
|
41
|
+
italic = style.get("italic", False)
|
|
42
|
+
bold = style.get("bold", False)
|
|
43
|
+
if italic:
|
|
44
|
+
content = f"_{content}_"
|
|
45
|
+
if bold:
|
|
46
|
+
content = f"**{content}**"
|
|
47
|
+
return f"{content}{append}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_paragraph_style_prepend_str(style: dict) -> str:
|
|
51
|
+
named_style = style["namedStyleType"]
|
|
52
|
+
if named_style == "NORMAL_TEXT":
|
|
53
|
+
return ""
|
|
54
|
+
elif named_style == "TITLE":
|
|
55
|
+
return "# "
|
|
56
|
+
elif named_style == "SUBTITLE":
|
|
57
|
+
return "## "
|
|
58
|
+
elif named_style.startswith("HEADING_"):
|
|
59
|
+
try:
|
|
60
|
+
heading_level = int(named_style.split("_")[1])
|
|
61
|
+
return f"{'#' * heading_level} "
|
|
62
|
+
except ValueError:
|
|
63
|
+
return ""
|
|
64
|
+
return ""
|
arcade_google/enums.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from zoneinfo import available_timezones
|
|
2
|
+
|
|
3
|
+
from arcade_tdk.errors import RetryableToolError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GoogleToolError(Exception):
|
|
7
|
+
"""Base exception for Google tool errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, developer_message: str | None = None):
|
|
10
|
+
self.message = message
|
|
11
|
+
self.developer_message = developer_message
|
|
12
|
+
super().__init__(self.message)
|
|
13
|
+
|
|
14
|
+
def __str__(self) -> str:
|
|
15
|
+
base_message = self.message
|
|
16
|
+
if self.developer_message:
|
|
17
|
+
return f"{base_message} (Developer: {self.developer_message})"
|
|
18
|
+
return base_message
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RetryableGoogleToolError(RetryableToolError):
|
|
22
|
+
"""Raised when there's an error in a Google tool that can be retried."""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GoogleServiceError(GoogleToolError):
|
|
28
|
+
"""Raised when there's an error building or using the Google service."""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GmailToolError(GoogleToolError):
|
|
34
|
+
"""Raised when there's an error in the Gmail tools."""
|
|
35
|
+
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GoogleCalendarToolError(GoogleToolError):
|
|
40
|
+
"""Raised when there's an error in the Google Calendar tools."""
|
|
41
|
+
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class InvalidTimezoneError(RetryableGoogleToolError):
|
|
46
|
+
"""Raised when a timezone is provided that is not supported by Python's zoneinfo."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, timezone_str: str):
|
|
49
|
+
self.timezone_str = timezone_str
|
|
50
|
+
available_timezones_msg = (
|
|
51
|
+
"Here is a list of valid timezones (from Python's zoneinfo.available_timezones()): "
|
|
52
|
+
f"{available_timezones()}"
|
|
53
|
+
)
|
|
54
|
+
super().__init__(
|
|
55
|
+
f"Invalid timezone: '{timezone_str}'",
|
|
56
|
+
developer_message=available_timezones_msg,
|
|
57
|
+
additional_prompt_content=available_timezones_msg,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class GoogleDriveToolError(GoogleToolError):
|
|
62
|
+
"""Raised when there's an error in the Google Drive tools."""
|
|
63
|
+
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class GoogleDocsToolError(GoogleToolError):
|
|
68
|
+
"""Raised when there's an error in the Google Docs tools."""
|
|
69
|
+
|
|
70
|
+
pass
|