dhisana 0.0.1.dev210__tar.gz → 0.0.1.dev212__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.dev212}/PKG-INFO +1 -1
  2. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/setup.py +1 -1
  3. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/google_oauth_tools.py +269 -72
  4. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/smtp_email_tools.py +36 -4
  5. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana.egg-info/PKG-INFO +1 -1
  6. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/README.md +0 -0
  7. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/pyproject.toml +0 -0
  8. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/setup.cfg +0 -0
  9. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/__init__.py +0 -0
  10. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/cli/__init__.py +0 -0
  11. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/cli/cli.py +0 -0
  12. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/cli/datasets.py +0 -0
  13. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/cli/models.py +0 -0
  14. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/cli/predictions.py +0 -0
  15. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/schemas/__init__.py +0 -0
  16. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/schemas/common.py +0 -0
  17. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/schemas/sales.py +0 -0
  18. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/ui/__init__.py +0 -0
  19. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/ui/components.py +0 -0
  20. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/__init__.py +0 -0
  21. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/add_mapping.py +0 -0
  22. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/agent_tools.py +0 -0
  23. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/apollo_tools.py +0 -0
  24. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  25. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/built_with_api_tools.py +0 -0
  26. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/cache_output_tools.py +0 -0
  27. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/cache_output_tools_local.py +0 -0
  28. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/check_email_validity_tools.py +0 -0
  29. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/check_for_intent_signal.py +0 -0
  30. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  31. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/clay_tools.py +0 -0
  32. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/clean_properties.py +0 -0
  33. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/company_utils.py +0 -0
  34. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  35. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/compose_search_query.py +0 -0
  36. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
  37. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/composite_tools.py +0 -0
  38. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/dataframe_tools.py +0 -0
  39. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/domain_parser.py +0 -0
  40. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/email_parse_helpers.py +0 -0
  41. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/email_provider.py +0 -0
  42. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/enrich_lead_information.py +0 -0
  43. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
  44. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/fetch_openai_config.py +0 -0
  45. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/field_validators.py +0 -0
  46. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/g2_tools.py +0 -0
  47. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/generate_content.py +0 -0
  48. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/generate_email.py +0 -0
  49. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/generate_email_response.py +0 -0
  50. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/generate_flow.py +0 -0
  51. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
  52. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
  53. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
  54. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
  55. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/google_custom_search.py +0 -0
  56. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/google_workspace_tools.py +0 -0
  57. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  58. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
  59. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/instantly_tools.py +0 -0
  60. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/linkedin_crawler.py +0 -0
  61. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/lusha_tools.py +0 -0
  62. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/mailgun_tools.py +0 -0
  63. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/microsoft365_tools.py +0 -0
  64. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
  65. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/openai_helpers.py +0 -0
  66. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  67. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  68. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  69. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  70. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  71. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
  72. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/profile.py +0 -0
  73. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/proxy_curl_tools.py +0 -0
  74. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
  75. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/python_function_to_tools.py +0 -0
  76. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/research_lead.py +0 -0
  77. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  78. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  79. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/search_router.py +0 -0
  80. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/search_router_jobs.py +0 -0
  81. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/sendgrid_tools.py +0 -0
  82. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/serarch_router_local_business.py +0 -0
  83. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
  84. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
  85. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/serpapi_google_search.py +0 -0
  86. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
  87. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/serpapi_search_tools.py +0 -0
  88. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
  89. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/serperdev_local_business.py +0 -0
  90. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/serperdev_search.py +0 -0
  91. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/test_connect.py +0 -0
  92. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/trasform_json.py +0 -0
  93. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  94. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/workflow_code_model.py +0 -0
  95. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/utils/zoominfo_tools.py +0 -0
  96. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/workflow/__init__.py +0 -0
  97. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/workflow/agent.py +0 -0
  98. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/workflow/flow.py +0 -0
  99. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/workflow/task.py +0 -0
  100. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana/workflow/test.py +0 -0
  101. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana.egg-info/SOURCES.txt +0 -0
  102. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana.egg-info/dependency_links.txt +0 -0
  103. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana.egg-info/entry_points.txt +0 -0
  104. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana.egg-info/requires.txt +0 -0
  105. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/src/dhisana.egg-info/top_level.txt +0 -0
  106. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/tests/test_agent_tools.py +0 -0
  107. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/tests/test_apollo_company_search.py +0 -0
  108. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/tests/test_connectivity.py +0 -0
  109. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/tests/test_google_document.py +0 -0
  110. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/tests/test_hubspot_call_logs.py +0 -0
  111. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/tests/test_linkedin_serper.py +0 -0
  112. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/tests/test_mcp_connectivity.py +0 -0
  113. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/tests/test_proxycurl_get_company_search_id.py +0 -0
  114. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/tests/test_proxycurl_job_count.py +0 -0
  115. {dhisana-0.0.1.dev210 → dhisana-0.0.1.dev212}/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.dev212
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-dev212',
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,
@@ -11,6 +11,8 @@ import imaplib
11
11
  import logging
12
12
  import re
13
13
  import uuid
14
+ from email.errors import HeaderParseError
15
+ from email.header import Header, decode_header, make_header
14
16
  from email.mime.text import MIMEText
15
17
  from datetime import datetime, timedelta, timezone
16
18
  from typing import Any, Dict, List, Optional, Union
@@ -35,6 +37,32 @@ from dhisana.utils.google_workspace_tools import (
35
37
  # Helper / Utility
36
38
  # --------------------------------------------------------------------------- #
37
39
 
40
+
41
+ def _decode_header_value(value: Any) -> str:
42
+ """Return a unicode string for an e-mail header field."""
43
+
44
+ if value is None:
45
+ return ""
46
+
47
+ if isinstance(value, Header):
48
+ value = str(value)
49
+
50
+ if isinstance(value, bytes):
51
+ try:
52
+ return value.decode("utf-8")
53
+ except UnicodeDecodeError:
54
+ return value.decode("latin-1", errors="replace")
55
+
56
+ if isinstance(value, str):
57
+ try:
58
+ decoded = make_header(decode_header(value))
59
+ return str(decoded)
60
+ except (HeaderParseError, UnicodeDecodeError, LookupError):
61
+ return value
62
+
63
+ return str(value)
64
+
65
+
38
66
  def _imap_date(iso_dt: Union[str, datetime]) -> str:
39
67
  """
40
68
  Convert an ISO 8601 datetime or datetime object into IMAP date format: DD-Mmm-YYYY.
@@ -150,7 +178,7 @@ def _parse_email_msg(raw_bytes: bytes) -> MessageItem:
150
178
  msg = email.message_from_bytes(raw_bytes)
151
179
 
152
180
  # Helper for reading headers
153
- hdr = lambda h: msg.get(h, "")
181
+ hdr = lambda h: _decode_header_value(msg.get(h))
154
182
 
155
183
  sender_name, sender_email = email.utils.parseaddr(hdr("From"))
156
184
  receiver_name, receiver_email = email.utils.parseaddr(hdr("To"))
@@ -163,10 +191,14 @@ def _parse_email_msg(raw_bytes: bytes) -> MessageItem:
163
191
  part.get_content_type() == "text/plain"
164
192
  and "attachment" not in str(part.get("Content-Disposition", ""))
165
193
  ):
166
- body = part.get_payload(decode=True).decode(errors="ignore")
167
- break
194
+ payload = part.get_payload(decode=True)
195
+ if payload is not None:
196
+ body = payload.decode(errors="ignore")
197
+ break
168
198
  else:
169
- body = msg.get_payload(decode=True).decode(errors="ignore")
199
+ payload = msg.get_payload(decode=True)
200
+ if payload is not None:
201
+ body = payload.decode(errors="ignore")
170
202
 
171
203
  # Parse the Date header to get a timezone-aware datetime
172
204
  try:
@@ -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.dev212
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