arcade-google 0.1.6__py3-none-any.whl → 1.2.4__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,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
@@ -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