dhisana 0.0.1.dev210__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.
Files changed (115) hide show
  1. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/PKG-INFO +1 -1
  2. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/setup.py +1 -1
  3. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/google_oauth_tools.py +269 -72
  4. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana.egg-info/PKG-INFO +1 -1
  5. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/README.md +0 -0
  6. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/pyproject.toml +0 -0
  7. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/setup.cfg +0 -0
  8. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/__init__.py +0 -0
  9. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/cli/__init__.py +0 -0
  10. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/cli/cli.py +0 -0
  11. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/cli/datasets.py +0 -0
  12. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/cli/models.py +0 -0
  13. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/cli/predictions.py +0 -0
  14. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/schemas/__init__.py +0 -0
  15. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/schemas/common.py +0 -0
  16. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/schemas/sales.py +0 -0
  17. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/ui/__init__.py +0 -0
  18. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/ui/components.py +0 -0
  19. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/__init__.py +0 -0
  20. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/add_mapping.py +0 -0
  21. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/agent_tools.py +0 -0
  22. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/apollo_tools.py +0 -0
  23. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  24. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/built_with_api_tools.py +0 -0
  25. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/cache_output_tools.py +0 -0
  26. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/cache_output_tools_local.py +0 -0
  27. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/check_email_validity_tools.py +0 -0
  28. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/check_for_intent_signal.py +0 -0
  29. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  30. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/clay_tools.py +0 -0
  31. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/clean_properties.py +0 -0
  32. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/company_utils.py +0 -0
  33. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  34. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/compose_search_query.py +0 -0
  35. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
  36. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/composite_tools.py +0 -0
  37. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/dataframe_tools.py +0 -0
  38. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/domain_parser.py +0 -0
  39. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/email_parse_helpers.py +0 -0
  40. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/email_provider.py +0 -0
  41. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/enrich_lead_information.py +0 -0
  42. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
  43. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/fetch_openai_config.py +0 -0
  44. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/field_validators.py +0 -0
  45. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/g2_tools.py +0 -0
  46. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_content.py +0 -0
  47. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_email.py +0 -0
  48. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_email_response.py +0 -0
  49. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_flow.py +0 -0
  50. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
  51. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
  52. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
  53. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
  54. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/google_custom_search.py +0 -0
  55. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/google_workspace_tools.py +0 -0
  56. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  57. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
  58. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/instantly_tools.py +0 -0
  59. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/linkedin_crawler.py +0 -0
  60. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/lusha_tools.py +0 -0
  61. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/mailgun_tools.py +0 -0
  62. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/microsoft365_tools.py +0 -0
  63. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
  64. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/openai_helpers.py +0 -0
  65. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  66. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  67. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  68. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  69. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  70. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
  71. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/profile.py +0 -0
  72. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/proxy_curl_tools.py +0 -0
  73. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
  74. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/python_function_to_tools.py +0 -0
  75. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/research_lead.py +0 -0
  76. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  77. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  78. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/search_router.py +0 -0
  79. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/search_router_jobs.py +0 -0
  80. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/sendgrid_tools.py +0 -0
  81. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/serarch_router_local_business.py +0 -0
  82. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
  83. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
  84. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/serpapi_google_search.py +0 -0
  85. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
  86. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/serpapi_search_tools.py +0 -0
  87. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
  88. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/serperdev_local_business.py +0 -0
  89. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/serperdev_search.py +0 -0
  90. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/smtp_email_tools.py +0 -0
  91. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/test_connect.py +0 -0
  92. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/trasform_json.py +0 -0
  93. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  94. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/workflow_code_model.py +0 -0
  95. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/utils/zoominfo_tools.py +0 -0
  96. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/workflow/__init__.py +0 -0
  97. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/workflow/agent.py +0 -0
  98. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/workflow/flow.py +0 -0
  99. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/workflow/task.py +0 -0
  100. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana/workflow/test.py +0 -0
  101. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana.egg-info/SOURCES.txt +0 -0
  102. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana.egg-info/dependency_links.txt +0 -0
  103. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana.egg-info/entry_points.txt +0 -0
  104. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana.egg-info/requires.txt +0 -0
  105. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/src/dhisana.egg-info/top_level.txt +0 -0
  106. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/tests/test_agent_tools.py +0 -0
  107. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/tests/test_apollo_company_search.py +0 -0
  108. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/tests/test_connectivity.py +0 -0
  109. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/tests/test_google_document.py +0 -0
  110. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/tests/test_hubspot_call_logs.py +0 -0
  111. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/tests/test_linkedin_serper.py +0 -0
  112. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/tests/test_mcp_connectivity.py +0 -0
  113. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/tests/test_proxycurl_get_company_search_id.py +0 -0
  114. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/tests/test_proxycurl_job_count.py +0 -0
  115. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev211}/tests/test_structured_output_with_mcp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev210
3
+ Version: 0.0.1.dev211
4
4
  Summary: A Python SDK for Dhisana AI Platform
5
5
  Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
6
6
  Author: Admin
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='dhisana',
5
- version='0.0.1-dev210',
5
+ version='0.0.1-dev211',
6
6
  description='A Python SDK for Dhisana AI Platform',
7
7
  author='Admin',
8
8
  author_email='contact@dhisana.ai',
@@ -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
- resp = await client.post(url, headers=headers, json=payload)
95
- resp.raise_for_status()
96
- data = resp.json() or {}
97
- return data.get("id", "")
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
- list_resp = await client.get(base_url, headers=headers, params=params)
135
- list_resp.raise_for_status()
136
- list_data = list_resp.json() or {}
137
- for m in list_data.get("messages", []) or []:
138
- mid = m.get("id")
139
- tid = m.get("threadId")
140
- if not mid:
141
- continue
142
- get_url = f"{base_url}/{mid}"
143
- get_resp = await client.get(get_url, headers=headers)
144
- get_resp.raise_for_status()
145
- mdata = get_resp.json() or {}
146
-
147
- headers_list = (mdata.get("payload") or {}).get("headers", [])
148
- from_header = find_header(headers_list, "From") or ""
149
- subject_header = find_header(headers_list, "Subject") or ""
150
- date_header = find_header(headers_list, "Date") or ""
151
-
152
- iso_dt = convert_date_to_iso(date_header)
153
- s_name, s_email = parse_single_address(from_header)
154
- r_name, r_email = find_all_recipients_in_headers(headers_list)
155
-
156
- items.append(
157
- MessageItem(
158
- message_id=mdata.get("id", ""),
159
- thread_id=tid or "",
160
- sender_name=s_name,
161
- sender_email=s_email,
162
- receiver_name=r_name,
163
- receiver_email=r_email,
164
- iso_datetime=iso_dt,
165
- subject=subject_header,
166
- body=extract_email_body_in_plain_text(mdata),
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
- get_resp = await client.get(get_url, headers=headers, params=params)
193
- get_resp.raise_for_status()
194
- original = get_resp.json() or {}
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
- send_resp = await client.post(send_url, headers=headers, json=payload)
227
- send_resp.raise_for_status()
228
- sent = send_resp.json() or {}
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
- resp = await client.get(url, headers=headers, params=params)
293
- resp.raise_for_status()
294
- data = resp.json() or {}
295
- events = data.get("items", [])
296
- if not events:
297
- logging.info("No upcoming events found within the specified range (OAuth).")
298
- return events
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
- # Default range to first sheet title if not supplied
336
- if not range_name:
337
- meta_url = f"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheet_id}"
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
- meta_resp = await client.get(meta_url, headers=headers)
340
- meta_resp.raise_for_status()
341
- meta = meta_resp.json() or {}
342
- sheets = meta.get("sheets", [])
343
- if not sheets:
344
- return []
345
- range_name = (sheets[0].get("properties") or {}).get("title") or "Sheet1"
346
-
347
- values_url = f"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheet_id}/values/{range_name}"
348
- async with httpx.AsyncClient(timeout=30) as client:
349
- val_resp = await client.get(values_url, headers=headers)
350
- val_resp.raise_for_status()
351
- data = val_resp.json() or {}
352
- return data.get("values", [])
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
- resp = await client.get(url, headers=headers)
372
- resp.raise_for_status()
373
- doc = resp.json() or {}
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 search_google_places_using_google_oauth(
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev210
3
+ Version: 0.0.1.dev211
4
4
  Summary: A Python SDK for Dhisana AI Platform
5
5
  Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
6
6
  Author: Admin
File without changes
File without changes