dhisana 0.0.1.dev209__tar.gz → 0.0.1.dev211__tar.gz
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.
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/setup.py +1 -1
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/google_oauth_tools.py +269 -72
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana.egg-info/PKG-INFO +1 -1
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/README.md +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/setup.cfg +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/schemas/common.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/schemas/sales.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/add_mapping.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/apollo_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/cache_output_tools_local.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/clean_properties.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/compose_search_query.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/dataframe_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/email_parse_helpers.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/email_provider.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/enrich_lead_information.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/fetch_openai_config.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/field_validators.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_content.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_email.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_email_response.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/google_workspace_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/mailgun_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/microsoft365_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/profile.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/proxy_curl_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/search_router.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/search_router_jobs.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/sendgrid_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/serarch_router_local_business.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/serpapi_google_search.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/serpapi_search_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/serperdev_local_business.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/serperdev_search.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/smtp_email_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/test_connect.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana.egg-info/SOURCES.txt +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/tests/test_agent_tools.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/tests/test_apollo_company_search.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/tests/test_connectivity.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/tests/test_google_document.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/tests/test_hubspot_call_logs.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/tests/test_linkedin_serper.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/tests/test_mcp_connectivity.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/tests/test_proxycurl_get_company_search_id.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/tests/test_proxycurl_job_count.py +0 -0
- {dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/tests/test_structured_output_with_mcp.py +0 -0
|
@@ -24,6 +24,57 @@ from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
|
24
24
|
from dhisana.utils.cache_output_tools import retrieve_output, cache_output
|
|
25
25
|
from typing import Optional as _Optional # avoid name clash in wrappers
|
|
26
26
|
|
|
27
|
+
def _status_phrase(code: int) -> str:
|
|
28
|
+
mapping = {
|
|
29
|
+
400: "Bad Request",
|
|
30
|
+
401: "Unauthorized",
|
|
31
|
+
403: "Forbidden",
|
|
32
|
+
404: "Not Found",
|
|
33
|
+
405: "Method Not Allowed",
|
|
34
|
+
409: "Conflict",
|
|
35
|
+
412: "Precondition Failed",
|
|
36
|
+
415: "Unsupported Media Type",
|
|
37
|
+
429: "Too Many Requests",
|
|
38
|
+
500: "Internal Server Error",
|
|
39
|
+
502: "Bad Gateway",
|
|
40
|
+
503: "Service Unavailable",
|
|
41
|
+
504: "Gateway Timeout",
|
|
42
|
+
}
|
|
43
|
+
return mapping.get(code, "HTTP Error")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _extract_google_api_message(response: Optional[httpx.Response]) -> str:
|
|
47
|
+
"""Extract a concise message from Google-style error JSON responses."""
|
|
48
|
+
if not response:
|
|
49
|
+
return ""
|
|
50
|
+
try:
|
|
51
|
+
data = response.json()
|
|
52
|
+
except Exception:
|
|
53
|
+
text = getattr(response, "text", None)
|
|
54
|
+
return text or ""
|
|
55
|
+
|
|
56
|
+
msg = None
|
|
57
|
+
if isinstance(data, dict):
|
|
58
|
+
err = data.get("error")
|
|
59
|
+
if isinstance(err, dict):
|
|
60
|
+
msg = err.get("message") or err.get("status")
|
|
61
|
+
elif isinstance(err, str):
|
|
62
|
+
# Some endpoints return string error + error_description
|
|
63
|
+
msg = data.get("error_description") or err
|
|
64
|
+
if not msg:
|
|
65
|
+
msg = data.get("message") or data.get("text")
|
|
66
|
+
return msg or ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _rethrow_with_google_message(exc: httpx.HTTPStatusError, context: str) -> None:
|
|
70
|
+
resp = getattr(exc, "response", None)
|
|
71
|
+
code = getattr(resp, "status_code", None) or 0
|
|
72
|
+
phrase = _status_phrase(int(code))
|
|
73
|
+
api_msg = _extract_google_api_message(resp) or "Google API request failed."
|
|
74
|
+
raise httpx.HTTPStatusError(
|
|
75
|
+
f"{code} {phrase} ({context}). {api_msg}", request=exc.request, response=resp
|
|
76
|
+
)
|
|
77
|
+
|
|
27
78
|
|
|
28
79
|
def get_google_access_token(tool_config: Optional[List[Dict]] = None) -> str:
|
|
29
80
|
"""
|
|
@@ -91,10 +142,13 @@ async def send_email_using_google_oauth_async(
|
|
|
91
142
|
url = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
|
|
92
143
|
|
|
93
144
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
145
|
+
try:
|
|
146
|
+
resp = await client.post(url, headers=headers, json=payload)
|
|
147
|
+
resp.raise_for_status()
|
|
148
|
+
data = resp.json() or {}
|
|
149
|
+
return data.get("id", "")
|
|
150
|
+
except httpx.HTTPStatusError as exc:
|
|
151
|
+
_rethrow_with_google_message(exc, "Gmail Send OAuth")
|
|
98
152
|
|
|
99
153
|
|
|
100
154
|
async def list_emails_in_time_range_google_oauth_async(
|
|
@@ -131,41 +185,44 @@ async def list_emails_in_time_range_google_oauth_async(
|
|
|
131
185
|
|
|
132
186
|
items: List[MessageItem] = []
|
|
133
187
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
try:
|
|
189
|
+
list_resp = await client.get(base_url, headers=headers, params=params)
|
|
190
|
+
list_resp.raise_for_status()
|
|
191
|
+
list_data = list_resp.json() or {}
|
|
192
|
+
for m in list_data.get("messages", []) or []:
|
|
193
|
+
mid = m.get("id")
|
|
194
|
+
tid = m.get("threadId")
|
|
195
|
+
if not mid:
|
|
196
|
+
continue
|
|
197
|
+
get_url = f"{base_url}/{mid}"
|
|
198
|
+
get_resp = await client.get(get_url, headers=headers)
|
|
199
|
+
get_resp.raise_for_status()
|
|
200
|
+
mdata = get_resp.json() or {}
|
|
201
|
+
|
|
202
|
+
headers_list = (mdata.get("payload") or {}).get("headers", [])
|
|
203
|
+
from_header = find_header(headers_list, "From") or ""
|
|
204
|
+
subject_header = find_header(headers_list, "Subject") or ""
|
|
205
|
+
date_header = find_header(headers_list, "Date") or ""
|
|
206
|
+
|
|
207
|
+
iso_dt = convert_date_to_iso(date_header)
|
|
208
|
+
s_name, s_email = parse_single_address(from_header)
|
|
209
|
+
r_name, r_email = find_all_recipients_in_headers(headers_list)
|
|
210
|
+
|
|
211
|
+
items.append(
|
|
212
|
+
MessageItem(
|
|
213
|
+
message_id=mdata.get("id", ""),
|
|
214
|
+
thread_id=tid or "",
|
|
215
|
+
sender_name=s_name,
|
|
216
|
+
sender_email=s_email,
|
|
217
|
+
receiver_name=r_name,
|
|
218
|
+
receiver_email=r_email,
|
|
219
|
+
iso_datetime=iso_dt,
|
|
220
|
+
subject=subject_header,
|
|
221
|
+
body=extract_email_body_in_plain_text(mdata),
|
|
222
|
+
)
|
|
167
223
|
)
|
|
168
|
-
|
|
224
|
+
except httpx.HTTPStatusError as exc:
|
|
225
|
+
_rethrow_with_google_message(exc, "Gmail List OAuth")
|
|
169
226
|
|
|
170
227
|
return items
|
|
171
228
|
|
|
@@ -189,9 +246,12 @@ async def reply_to_email_google_oauth_async(
|
|
|
189
246
|
get_url = f"{base}/messages/{reply_email_context.message_id}"
|
|
190
247
|
params = {"format": "full"}
|
|
191
248
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
249
|
+
try:
|
|
250
|
+
get_resp = await client.get(get_url, headers=headers, params=params)
|
|
251
|
+
get_resp.raise_for_status()
|
|
252
|
+
original = get_resp.json() or {}
|
|
253
|
+
except httpx.HTTPStatusError as exc:
|
|
254
|
+
_rethrow_with_google_message(exc, "Gmail Fetch Message OAuth")
|
|
195
255
|
|
|
196
256
|
headers_list = (original.get("payload") or {}).get("headers", [])
|
|
197
257
|
headers_map = {h.get("name"): h.get("value") for h in headers_list if isinstance(h, dict)}
|
|
@@ -223,9 +283,12 @@ async def reply_to_email_google_oauth_async(
|
|
|
223
283
|
# 3) Send the reply
|
|
224
284
|
send_url = f"{base}/messages/send"
|
|
225
285
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
286
|
+
try:
|
|
287
|
+
send_resp = await client.post(send_url, headers=headers, json=payload)
|
|
288
|
+
send_resp.raise_for_status()
|
|
289
|
+
sent = send_resp.json() or {}
|
|
290
|
+
except httpx.HTTPStatusError as exc:
|
|
291
|
+
_rethrow_with_google_message(exc, "Gmail Send Reply OAuth")
|
|
229
292
|
|
|
230
293
|
# 4) Optional: mark as read
|
|
231
294
|
if str(reply_email_context.mark_as_read).lower() == "true" and thread_id:
|
|
@@ -289,13 +352,16 @@ async def get_calendar_events_using_google_oauth_async(
|
|
|
289
352
|
}
|
|
290
353
|
|
|
291
354
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
355
|
+
try:
|
|
356
|
+
resp = await client.get(url, headers=headers, params=params)
|
|
357
|
+
resp.raise_for_status()
|
|
358
|
+
data = resp.json() or {}
|
|
359
|
+
events = data.get("items", [])
|
|
360
|
+
if not events:
|
|
361
|
+
logging.info("No upcoming events found within the specified range (OAuth).")
|
|
362
|
+
return events
|
|
363
|
+
except httpx.HTTPStatusError as exc:
|
|
364
|
+
_rethrow_with_google_message(exc, "Calendar OAuth")
|
|
299
365
|
|
|
300
366
|
|
|
301
367
|
# ---------------------------------------------------------------------------
|
|
@@ -330,26 +396,68 @@ async def read_google_sheet_using_google_oauth(
|
|
|
330
396
|
token = get_google_access_token(tool_config)
|
|
331
397
|
headers = {"Authorization": f"Bearer {token}"}
|
|
332
398
|
|
|
399
|
+
# If the GCP project requires a quota/billing project with OAuth, allow an optional header
|
|
400
|
+
def _quota_project(cfg: _Optional[List[Dict]]) -> _Optional[str]:
|
|
401
|
+
try:
|
|
402
|
+
g_cfg = next((c for c in (cfg or []) if c.get("name") == "google"), None)
|
|
403
|
+
if not g_cfg:
|
|
404
|
+
return None
|
|
405
|
+
cmap = {f["name"]: f.get("value") for f in g_cfg.get("configuration", []) if f}
|
|
406
|
+
return (
|
|
407
|
+
cmap.get("quota_project")
|
|
408
|
+
or cmap.get("quotaProjectId")
|
|
409
|
+
or cmap.get("project_id")
|
|
410
|
+
or cmap.get("x_goog_user_project")
|
|
411
|
+
or cmap.get("google_cloud_project")
|
|
412
|
+
)
|
|
413
|
+
except Exception:
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
qp = _quota_project(tool_config)
|
|
417
|
+
if qp:
|
|
418
|
+
headers["X-Goog-User-Project"] = qp
|
|
419
|
+
|
|
333
420
|
spreadsheet_id = _get_sheet_id_from_url(sheet_url)
|
|
334
421
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
422
|
+
async def _oauth_fetch() -> List[List[str]]:
|
|
423
|
+
nonlocal range_name
|
|
424
|
+
# Default range to first sheet title if not supplied
|
|
425
|
+
if not range_name:
|
|
426
|
+
meta_url = f"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheet_id}"
|
|
427
|
+
params = {"fields": "sheets(properties(title))"}
|
|
428
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
429
|
+
meta_resp = await client.get(meta_url, headers=headers, params=params)
|
|
430
|
+
meta_resp.raise_for_status()
|
|
431
|
+
meta = meta_resp.json() or {}
|
|
432
|
+
sheets = meta.get("sheets", [])
|
|
433
|
+
if not sheets:
|
|
434
|
+
return []
|
|
435
|
+
range_name = (sheets[0].get("properties") or {}).get("title") or "Sheet1"
|
|
436
|
+
|
|
437
|
+
values_url = f"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheet_id}/values/{range_name}"
|
|
338
438
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
439
|
+
val_resp = await client.get(values_url, headers=headers)
|
|
440
|
+
val_resp.raise_for_status()
|
|
441
|
+
data = val_resp.json() or {}
|
|
442
|
+
return data.get("values", [])
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
return await _oauth_fetch()
|
|
446
|
+
except httpx.HTTPStatusError as exc:
|
|
447
|
+
# If OAuth fails with 403 (likely insufficient scope or access), fail with clear guidance
|
|
448
|
+
status = getattr(getattr(exc, "response", None), "status_code", None)
|
|
449
|
+
if status == 403:
|
|
450
|
+
api_msg = _extract_google_api_message(exc.response) or "Access forbidden by Google API (403)."
|
|
451
|
+
guidance = (
|
|
452
|
+
"Google Sheets access denied with OAuth. Ensure the connected Google account can access the spreadsheet "
|
|
453
|
+
"(share with the account if private) and that the OAuth token includes the Sheets scope "
|
|
454
|
+
"('https://www.googleapis.com/auth/spreadsheets.readonly' or 'https://www.googleapis.com/auth/spreadsheets')."
|
|
455
|
+
)
|
|
456
|
+
raise httpx.HTTPStatusError(
|
|
457
|
+
f"403 Forbidden (Sheets OAuth). {api_msg} {guidance}", request=exc.request, response=exc.response
|
|
458
|
+
)
|
|
459
|
+
# For other statuses, rethrow with Google's message
|
|
460
|
+
_rethrow_with_google_message(exc, "Sheets OAuth")
|
|
353
461
|
|
|
354
462
|
|
|
355
463
|
@assistant_tool
|
|
@@ -368,9 +476,12 @@ async def read_google_document_using_google_oauth(
|
|
|
368
476
|
url = f"https://docs.googleapis.com/v1/documents/{document_id}"
|
|
369
477
|
|
|
370
478
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
479
|
+
try:
|
|
480
|
+
resp = await client.get(url, headers=headers)
|
|
481
|
+
resp.raise_for_status()
|
|
482
|
+
doc = resp.json() or {}
|
|
483
|
+
except httpx.HTTPStatusError as exc:
|
|
484
|
+
_rethrow_with_google_message(exc, "Docs OAuth")
|
|
374
485
|
|
|
375
486
|
content = (doc.get("body") or {}).get("content", [])
|
|
376
487
|
parts: List[str] = []
|
|
@@ -387,7 +498,93 @@ async def read_google_document_using_google_oauth(
|
|
|
387
498
|
|
|
388
499
|
|
|
389
500
|
@assistant_tool
|
|
390
|
-
async def
|
|
501
|
+
async def search_google_custom_search(
|
|
502
|
+
query: str,
|
|
503
|
+
number_of_results: int = 10,
|
|
504
|
+
offset: int = 0,
|
|
505
|
+
tool_config: _Optional[List[Dict]] = None,
|
|
506
|
+
as_oq: _Optional[str] = None,
|
|
507
|
+
) -> List[str]:
|
|
508
|
+
"""
|
|
509
|
+
Search Google using the Custom Search JSON API with a per-user OAuth token.
|
|
510
|
+
|
|
511
|
+
Requires a Programmable Search Engine ID (cx) from the 'google_custom_search' integration
|
|
512
|
+
or env var 'GOOGLE_SEARCH_CX'. Returns a list of JSON strings with
|
|
513
|
+
{ position, title, link, snippet } items.
|
|
514
|
+
"""
|
|
515
|
+
# Final query composition
|
|
516
|
+
full_query = query if not as_oq else f"{query} {as_oq}"
|
|
517
|
+
|
|
518
|
+
# Acquire OAuth token and CX id
|
|
519
|
+
token = get_google_access_token(tool_config)
|
|
520
|
+
|
|
521
|
+
cx: Optional[str] = None
|
|
522
|
+
if tool_config:
|
|
523
|
+
gcs_cfg = next((c for c in tool_config if c.get("name") == "google_custom_search"), None)
|
|
524
|
+
if gcs_cfg:
|
|
525
|
+
cfg_map = {f["name"]: f.get("value") for f in gcs_cfg.get("configuration", []) if f}
|
|
526
|
+
cx = cfg_map.get("cx")
|
|
527
|
+
if not cx:
|
|
528
|
+
import os as _os
|
|
529
|
+
cx = _os.environ.get("GOOGLE_SEARCH_CX")
|
|
530
|
+
if not cx:
|
|
531
|
+
err = (
|
|
532
|
+
"Google Custom Search CX is not configured. Please add 'google_custom_search' integration with 'cx',"
|
|
533
|
+
" or set GOOGLE_SEARCH_CX."
|
|
534
|
+
)
|
|
535
|
+
logging.error(err)
|
|
536
|
+
return [json.dumps({"error": err})]
|
|
537
|
+
|
|
538
|
+
# Pagination: start=1-based index
|
|
539
|
+
start_index = max(1, int(offset) + 1)
|
|
540
|
+
|
|
541
|
+
url = "https://www.googleapis.com/customsearch/v1"
|
|
542
|
+
params = {
|
|
543
|
+
"q": full_query,
|
|
544
|
+
"num": number_of_results,
|
|
545
|
+
"start": start_index,
|
|
546
|
+
"cx": cx,
|
|
547
|
+
}
|
|
548
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
549
|
+
|
|
550
|
+
cache_key = f"oauth_cse:{full_query}:{number_of_results}:{offset}:{cx}"
|
|
551
|
+
cached = retrieve_output("search_google_custom_search_oauth", cache_key)
|
|
552
|
+
if cached is not None:
|
|
553
|
+
return cached
|
|
554
|
+
|
|
555
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
556
|
+
try:
|
|
557
|
+
resp = await client.get(url, headers=headers, params=params)
|
|
558
|
+
if resp.status_code == 429:
|
|
559
|
+
return [json.dumps({"error": "Rate limit exceeded (429)"})]
|
|
560
|
+
resp.raise_for_status()
|
|
561
|
+
data = resp.json() or {}
|
|
562
|
+
|
|
563
|
+
items = data.get("items", []) or []
|
|
564
|
+
norm: List[Dict[str, Any]] = []
|
|
565
|
+
for i, item in enumerate(items):
|
|
566
|
+
norm.append({
|
|
567
|
+
"position": i + 1,
|
|
568
|
+
"title": item.get("title", ""),
|
|
569
|
+
"link": item.get("link", ""),
|
|
570
|
+
"snippet": item.get("snippet", ""),
|
|
571
|
+
})
|
|
572
|
+
out = [json.dumps(o) for o in norm]
|
|
573
|
+
cache_output("search_google_custom_search_oauth", cache_key, out)
|
|
574
|
+
return out
|
|
575
|
+
except httpx.HTTPStatusError as exc:
|
|
576
|
+
try:
|
|
577
|
+
err_json = exc.response.json()
|
|
578
|
+
except Exception:
|
|
579
|
+
err_json = {"status": exc.response.status_code, "text": exc.response.text}
|
|
580
|
+
logging.warning(f"CSE OAuth request failed: {err_json}")
|
|
581
|
+
return [json.dumps({"error": err_json})]
|
|
582
|
+
except Exception as e:
|
|
583
|
+
logging.exception("CSE OAuth request failed")
|
|
584
|
+
return [json.dumps({"error": str(e)})]
|
|
585
|
+
|
|
586
|
+
@assistant_tool
|
|
587
|
+
async def search_google_places(
|
|
391
588
|
query: str,
|
|
392
589
|
location_bias: dict = None,
|
|
393
590
|
number_of_results: int = 3,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/check_email_validity_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/check_linkedin_url_validity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/compose_three_step_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/extract_email_content_for_llm.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_linkedin_connect_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/openai_assistant_and_file_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/openapi_tool/openapi_tool.py
RENAMED
|
File without changes
|
{dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/parse_linkedin_messages_txt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/serarch_router_local_business.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev209 → dhisana-0.0.1.dev211}/src/dhisana/utils/serpapi_local_business_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|