dhisana 0.0.1.dev207__py3-none-any.whl → 0.0.1.dev209__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.
@@ -17,6 +17,11 @@ from dhisana.utils.google_workspace_tools import (
17
17
  list_emails_in_time_range_async,
18
18
  reply_to_email_async as gw_reply_to_email_async,
19
19
  )
20
+ from dhisana.utils.google_oauth_tools import (
21
+ send_email_using_google_oauth_async,
22
+ list_emails_in_time_range_google_oauth_async,
23
+ reply_to_email_google_oauth_async,
24
+ )
20
25
  from dhisana.utils.microsoft365_tools import (
21
26
  send_email_using_microsoft_graph_async,
22
27
  list_emails_in_time_range_m365_async,
@@ -105,7 +110,14 @@ async def send_email_async(
105
110
  send_email_context: SendEmailContext,
106
111
  tool_config: Optional[List[Dict]] = None,
107
112
  *,
108
- provider_order: Sequence[str] = ("mailgun", "sendgrid", "smtpEmail", "googleworkspace", "microsoft365"),
113
+ provider_order: Sequence[str] = (
114
+ "mailgun",
115
+ "sendgrid",
116
+ "google", # Google OAuth (per-user token)
117
+ "smtpEmail",
118
+ "googleworkspace", # Google Workspace service account (DWD)
119
+ "microsoft365",
120
+ ),
109
121
  ):
110
122
  """
111
123
  Send an e-mail using the first *configured* provider in *provider_order*.
@@ -154,15 +166,12 @@ async def send_email_async(
154
166
  continue
155
167
  return await send_email_using_sendgrid_async(send_email_context, tool_config)
156
168
 
157
- # 1d) Microsoft 365 (Graph API)
158
- elif provider == "microsoft365":
159
- ms_cfg = _find_provider_cfg(tool_config, "microsoft365")
160
- if not ms_cfg:
169
+ # 1d) Google (Gmail API via per-user OAuth)
170
+ elif provider == "google":
171
+ g_cfg = _find_provider_cfg(tool_config, "google")
172
+ if not g_cfg:
161
173
  continue
162
-
163
- return await send_email_using_microsoft_graph_async(
164
- send_email_context, tool_config
165
- )
174
+ return await send_email_using_google_oauth_async(send_email_context, tool_config)
166
175
 
167
176
  # 1e) Google Workspace
168
177
  elif provider == "googleworkspace":
@@ -174,6 +183,16 @@ async def send_email_async(
174
183
  send_email_context, tool_config
175
184
  )
176
185
 
186
+ # 1f) Microsoft 365 (Graph API)
187
+ elif provider == "microsoft365":
188
+ ms_cfg = _find_provider_cfg(tool_config, "microsoft365")
189
+ if not ms_cfg:
190
+ continue
191
+
192
+ return await send_email_using_microsoft_graph_async(
193
+ send_email_context, tool_config
194
+ )
195
+
177
196
  # -- future providers slot --------------------------------------
178
197
 
179
198
  # ------------------------------------------------------------------ #
@@ -230,7 +249,7 @@ async def list_emails_async(
230
249
  query_email_context: QueryEmailContext,
231
250
  tool_config: Optional[List[Dict]] = None,
232
251
  *,
233
- provider_order: Sequence[str] = ("smtpEmail", "googleworkspace", "microsoft365"),
252
+ provider_order: Sequence[str] = ("google", "smtpEmail", "googleworkspace", "microsoft365"),
234
253
  ) -> List[MessageItem]:
235
254
  """
236
255
  List e-mails (see ``QueryEmailContext``) using the first configured provider.
@@ -255,6 +274,12 @@ async def list_emails_async(
255
274
  password=creds["password"],
256
275
  )
257
276
 
277
+ elif provider == "google":
278
+ g_cfg = _find_provider_cfg(tool_config, "google")
279
+ if not g_cfg:
280
+ continue
281
+ return await list_emails_in_time_range_google_oauth_async(query_email_context, tool_config)
282
+
258
283
  elif provider == "googleworkspace":
259
284
  gw_cfg = _find_provider_cfg(tool_config, "googleworkspace")
260
285
  if not gw_cfg:
@@ -279,7 +304,7 @@ async def reply_email_async(
279
304
  reply_email_context: ReplyEmailContext,
280
305
  tool_config: Optional[List[Dict]] = None,
281
306
  *,
282
- provider_order: Sequence[str] = ("smtpEmail", "googleworkspace", "microsoft365"),
307
+ provider_order: Sequence[str] = ("google", "smtpEmail", "googleworkspace", "microsoft365"),
283
308
  ) -> Dict[str, Any]:
284
309
  """
285
310
  Reply (reply-all) to an e-mail using the first *configured* provider
@@ -312,7 +337,17 @@ async def reply_email_async(
312
337
  )
313
338
 
314
339
  # ------------------------------------------------------------------
315
- # 2) Google Workspace service-account
340
+ # 2) Google OAuth (per-user)
341
+ # ------------------------------------------------------------------
342
+ elif provider == "google":
343
+ g_cfg = _find_provider_cfg(tool_config, "google")
344
+ if not g_cfg:
345
+ continue
346
+
347
+ return await reply_to_email_google_oauth_async(reply_email_context, tool_config)
348
+
349
+ # ------------------------------------------------------------------
350
+ # 3) Google Workspace service-account
316
351
  # ------------------------------------------------------------------
317
352
  elif provider == "googleworkspace":
318
353
  gw_cfg = _find_provider_cfg(tool_config, "googleworkspace")
@@ -322,7 +357,7 @@ async def reply_email_async(
322
357
  return await gw_reply_to_email_async(reply_email_context, tool_config)
323
358
 
324
359
  # ------------------------------------------------------------------
325
- # 3) Microsoft 365 (Graph)
360
+ # 4) Microsoft 365 (Graph)
326
361
  # ------------------------------------------------------------------
327
362
  elif provider == "microsoft365":
328
363
  ms_cfg = _find_provider_cfg(tool_config, "microsoft365")
@@ -0,0 +1,455 @@
1
+ import base64
2
+ import json
3
+ import logging
4
+ import re
5
+ from email.mime.text import MIMEText
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ import httpx
9
+
10
+ from dhisana.schemas.common import (
11
+ SendEmailContext,
12
+ QueryEmailContext,
13
+ ReplyEmailContext,
14
+ )
15
+ from dhisana.schemas.sales import MessageItem
16
+ from dhisana.utils.email_parse_helpers import (
17
+ find_header,
18
+ parse_single_address,
19
+ find_all_recipients_in_headers,
20
+ convert_date_to_iso,
21
+ extract_email_body_in_plain_text,
22
+ )
23
+ from dhisana.utils.assistant_tool_tag import assistant_tool
24
+ from dhisana.utils.cache_output_tools import retrieve_output, cache_output
25
+ from typing import Optional as _Optional # avoid name clash in wrappers
26
+
27
+
28
+ def get_google_access_token(tool_config: Optional[List[Dict]] = None) -> str:
29
+ """
30
+ Retrieve a Google OAuth2 access token from the 'google' integration config.
31
+
32
+ Expected tool_config shape:
33
+ {
34
+ "name": "google",
35
+ "configuration": [
36
+ {"name": "oauth_tokens", "value": {"access_token": "..."} }
37
+ # or {"name": "access_token", "value": "..."}
38
+ ]
39
+ }
40
+
41
+ If provided as a JSON string under oauth_tokens, it is parsed.
42
+ """
43
+ access_token: Optional[str] = None
44
+
45
+ if tool_config:
46
+ g_cfg = next((c for c in tool_config if c.get("name") == "google"), None)
47
+ if g_cfg:
48
+ cfg_map = {f["name"]: f.get("value") for f in g_cfg.get("configuration", []) if f}
49
+ raw_oauth = cfg_map.get("oauth_tokens")
50
+ # oauth_tokens might be a JSON string or a dict
51
+ if isinstance(raw_oauth, str):
52
+ try:
53
+ raw_oauth = json.loads(raw_oauth)
54
+ except Exception:
55
+ raw_oauth = None
56
+ if isinstance(raw_oauth, dict):
57
+ access_token = raw_oauth.get("access_token") or raw_oauth.get("token")
58
+ if not access_token:
59
+ access_token = cfg_map.get("access_token")
60
+
61
+ if not access_token:
62
+ raise ValueError(
63
+ "Google integration is not configured. Please connect Google and supply an OAuth access token."
64
+ )
65
+ return access_token
66
+
67
+
68
+ async def send_email_using_google_oauth_async(
69
+ send_email_context: SendEmailContext,
70
+ tool_config: Optional[List[Dict]] = None,
71
+ ) -> str:
72
+ """
73
+ Send an email using Gmail API with a per-user OAuth2 token.
74
+
75
+ Returns the Gmail message id of the sent message when available.
76
+ """
77
+ token = get_google_access_token(tool_config)
78
+
79
+ message = MIMEText(send_email_context.body, _subtype="html")
80
+ message["to"] = send_email_context.recipient
81
+ message["from"] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
82
+ message["subject"] = send_email_context.subject
83
+
84
+ raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
85
+
86
+ payload: Dict[str, Any] = {"raw": raw_message}
87
+ if send_email_context.labels:
88
+ payload["labelIds"] = send_email_context.labels
89
+
90
+ headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
91
+ url = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
92
+
93
+ 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", "")
98
+
99
+
100
+ async def list_emails_in_time_range_google_oauth_async(
101
+ context: QueryEmailContext,
102
+ tool_config: Optional[List[Dict]] = None,
103
+ ) -> List[MessageItem]:
104
+ """
105
+ List Gmail messages for the connected user in a time range using OAuth2.
106
+ Returns a list of MessageItem.
107
+ """
108
+ if context.labels is None:
109
+ context.labels = []
110
+
111
+ token = get_google_access_token(tool_config)
112
+ base_url = "https://gmail.googleapis.com/gmail/v1/users/me/messages"
113
+ headers = {"Authorization": f"Bearer {token}"}
114
+
115
+ # Convert RFC3339 times to unix timestamps for Gmail search query
116
+ # Expecting context.start_time and context.end_time as ISO 8601; Gmail q uses epoch seconds
117
+ from datetime import datetime
118
+ start_dt = datetime.fromisoformat(context.start_time.replace("Z", "+00:00"))
119
+ end_dt = datetime.fromisoformat(context.end_time.replace("Z", "+00:00"))
120
+ after_ts = int(start_dt.timestamp())
121
+ before_ts = int(end_dt.timestamp())
122
+
123
+ q_parts: List[str] = [f"after:{after_ts}", f"before:{before_ts}"]
124
+ if context.unread_only:
125
+ q_parts.append("is:unread")
126
+ if context.labels:
127
+ q_parts.extend([f"label:{lbl}" for lbl in context.labels])
128
+ query = " ".join(q_parts)
129
+
130
+ params = {"q": query}
131
+
132
+ items: List[MessageItem] = []
133
+ 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),
167
+ )
168
+ )
169
+
170
+ return items
171
+
172
+
173
+ async def reply_to_email_google_oauth_async(
174
+ reply_email_context: ReplyEmailContext,
175
+ tool_config: Optional[List[Dict]] = None,
176
+ ) -> Dict[str, Any]:
177
+ """
178
+ Reply-all to a Gmail message for the connected user using OAuth2.
179
+ Returns a metadata dictionary similar to other providers.
180
+ """
181
+ if reply_email_context.add_labels is None:
182
+ reply_email_context.add_labels = []
183
+
184
+ token = get_google_access_token(tool_config)
185
+ headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
186
+ base = "https://gmail.googleapis.com/gmail/v1/users/me"
187
+
188
+ # 1) Fetch original message
189
+ get_url = f"{base}/messages/{reply_email_context.message_id}"
190
+ params = {"format": "full"}
191
+ 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 {}
195
+
196
+ headers_list = (original.get("payload") or {}).get("headers", [])
197
+ headers_map = {h.get("name"): h.get("value") for h in headers_list if isinstance(h, dict)}
198
+ thread_id = original.get("threadId")
199
+
200
+ subject = headers_map.get("Subject", "") or ""
201
+ if not subject.startswith("Re:"):
202
+ subject = f"Re: {subject}"
203
+ to_addresses = headers_map.get("From", "") or ""
204
+ cc_addresses = headers_map.get("Cc", "") or ""
205
+ message_id_header = headers_map.get("Message-ID", "") or ""
206
+
207
+ # 2) Build reply MIME
208
+ msg = MIMEText(reply_email_context.reply_body, _subtype="html")
209
+ msg["To"] = to_addresses
210
+ if cc_addresses:
211
+ msg["Cc"] = cc_addresses
212
+ msg["From"] = f"{reply_email_context.sender_name} <{reply_email_context.sender_email}>"
213
+ msg["Subject"] = subject
214
+ if message_id_header:
215
+ msg["In-Reply-To"] = message_id_header
216
+ msg["References"] = message_id_header
217
+
218
+ raw_message = base64.urlsafe_b64encode(msg.as_bytes()).decode()
219
+ payload = {"raw": raw_message}
220
+ if thread_id:
221
+ payload["threadId"] = thread_id
222
+
223
+ # 3) Send the reply
224
+ send_url = f"{base}/messages/send"
225
+ 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 {}
229
+
230
+ # 4) Optional: mark as read
231
+ if str(reply_email_context.mark_as_read).lower() == "true" and thread_id:
232
+ modify_url = f"{base}/threads/{thread_id}/modify"
233
+ modify_payload = {"removeLabelIds": ["UNREAD"]}
234
+ try:
235
+ async with httpx.AsyncClient(timeout=30) as client:
236
+ await client.post(modify_url, headers=headers, json=modify_payload)
237
+ except Exception:
238
+ logging.exception("Gmail: failed to mark thread as read (best-effort)")
239
+
240
+ # 5) Optional: add labels
241
+ if reply_email_context.add_labels and thread_id:
242
+ modify_url = f"{base}/threads/{thread_id}/modify"
243
+ modify_payload = {"addLabelIds": reply_email_context.add_labels}
244
+ try:
245
+ async with httpx.AsyncClient(timeout=30) as client:
246
+ await client.post(modify_url, headers=headers, json=modify_payload)
247
+ except Exception:
248
+ logging.exception("Gmail: failed to add labels to thread (best-effort)")
249
+
250
+ return {
251
+ "mailbox_email_id": sent.get("id"),
252
+ "message_id": (sent.get("threadId") or thread_id or ""),
253
+ "email_subject": subject,
254
+ "email_sender": reply_email_context.sender_email,
255
+ "email_recipients": [to_addresses] + ([cc_addresses] if cc_addresses else []),
256
+ "read_email_status": "READ" if str(reply_email_context.mark_as_read).lower() == "true" else "UNREAD",
257
+ "email_labels": sent.get("labelIds", []),
258
+ }
259
+
260
+
261
+ # ---------------------------------------------------------------------------
262
+ # Google Calendar (OAuth per-user)
263
+ # ---------------------------------------------------------------------------
264
+
265
+ @assistant_tool
266
+ async def get_calendar_events_using_google_oauth_async(
267
+ start_date: str,
268
+ end_date: str,
269
+ tool_config: Optional[List[Dict]] = None,
270
+ ) -> List[Dict[str, Any]]:
271
+ """
272
+ Retrieve events from the user's primary Google Calendar using a per-user OAuth token.
273
+
274
+ start_date, end_date: 'YYYY-MM-DD' strings (inclusive start, inclusive end day as 23:59:59Z).
275
+ Returns a list of event dicts from the Calendar API.
276
+ """
277
+ token = get_google_access_token(tool_config)
278
+ headers = {"Authorization": f"Bearer {token}"}
279
+ url = "https://www.googleapis.com/calendar/v3/calendars/primary/events"
280
+
281
+ time_min = f"{start_date}T00:00:00Z"
282
+ time_max = f"{end_date}T23:59:59Z"
283
+ params = {
284
+ "timeMin": time_min,
285
+ "timeMax": time_max,
286
+ "maxResults": 10,
287
+ "singleEvents": True,
288
+ "orderBy": "startTime",
289
+ }
290
+
291
+ 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
299
+
300
+
301
+ # ---------------------------------------------------------------------------
302
+ # Google Sheets and Docs (OAuth per-user)
303
+ # ---------------------------------------------------------------------------
304
+
305
+ def _get_sheet_id_from_url(sheet_url: str) -> str:
306
+ match = re.search(r"/d/([a-zA-Z0-9-_]+)/", sheet_url)
307
+ if not match:
308
+ raise ValueError("Could not extract spreadsheet ID from the provided URL.")
309
+ return match.group(1)
310
+
311
+
312
+ def _get_document_id_from_url(doc_url: str) -> str:
313
+ match = re.search(r"/d/([a-zA-Z0-9-_]+)/", doc_url)
314
+ if not match:
315
+ raise ValueError("Could not extract document ID from the provided URL.")
316
+ return match.group(1)
317
+
318
+
319
+ @assistant_tool
320
+ async def read_google_sheet_using_google_oauth(
321
+ sheet_url: str,
322
+ range_name: str,
323
+ tool_config: Optional[List[Dict]] = None,
324
+ ) -> List[List[str]]:
325
+ """
326
+ Read data from a Google Sheet using the connected user's OAuth token.
327
+
328
+ If range_name is empty, reads the first sheet tab by fetching spreadsheet metadata.
329
+ """
330
+ token = get_google_access_token(tool_config)
331
+ headers = {"Authorization": f"Bearer {token}"}
332
+
333
+ spreadsheet_id = _get_sheet_id_from_url(sheet_url)
334
+
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}"
338
+ 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", [])
353
+
354
+
355
+ @assistant_tool
356
+ async def read_google_document_using_google_oauth(
357
+ doc_url: str,
358
+ tool_config: Optional[List[Dict]] = None,
359
+ ) -> str:
360
+ """
361
+ Read text content from a Google Doc using the connected user's OAuth token.
362
+ Concatenates all text runs in the document body.
363
+ """
364
+ token = get_google_access_token(tool_config)
365
+ headers = {"Authorization": f"Bearer {token}"}
366
+
367
+ document_id = _get_document_id_from_url(doc_url)
368
+ url = f"https://docs.googleapis.com/v1/documents/{document_id}"
369
+
370
+ 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 {}
374
+
375
+ content = (doc.get("body") or {}).get("content", [])
376
+ parts: List[str] = []
377
+ for element in content:
378
+ paragraph = element.get("paragraph")
379
+ if not paragraph:
380
+ continue
381
+ for elem in paragraph.get("elements", []) or []:
382
+ text_run = elem.get("textRun")
383
+ if text_run:
384
+ parts.append(text_run.get("content", ""))
385
+
386
+ return "".join(parts)
387
+
388
+
389
+ @assistant_tool
390
+ async def search_google_places_using_google_oauth(
391
+ query: str,
392
+ location_bias: dict = None,
393
+ number_of_results: int = 3,
394
+ tool_config: _Optional[List[Dict]] = None,
395
+ ) -> List[str]:
396
+ """
397
+ Search Google Places (New) with a per-user OAuth token.
398
+
399
+ - Requires that the OAuth token has Maps/Places access enabled for the project.
400
+ - Returns a list of JSON strings, each being a place object.
401
+ """
402
+
403
+
404
+ token = get_google_access_token(tool_config)
405
+ url = "https://places.googleapis.com/v1/places:searchText"
406
+ headers = {
407
+ "Content-Type": "application/json",
408
+ "Authorization": f"Bearer {token}",
409
+ # Field mask is required to limit returned fields
410
+ "X-Goog-FieldMask": (
411
+ "places.displayName,places.formattedAddress,places.location,"
412
+ "places.websiteUri,places.rating,places.reviews"
413
+ ),
414
+ }
415
+
416
+ body: Dict[str, Any] = {"textQuery": query}
417
+ if location_bias:
418
+ body["locationBias"] = {
419
+ "circle": {
420
+ "center": {
421
+ "latitude": location_bias.get("latitude"),
422
+ "longitude": location_bias.get("longitude"),
423
+ },
424
+ "radius": location_bias.get("radius", 5000),
425
+ }
426
+ }
427
+
428
+ # Cache key based on query, count and bias
429
+ bias_str = json.dumps(location_bias, sort_keys=True) if location_bias else "None"
430
+ cache_key = f"oauth_places:{query}:{number_of_results}:{bias_str}"
431
+ cached = retrieve_output("search_google_places_oauth", cache_key)
432
+ if cached is not None:
433
+ return cached
434
+
435
+ async with httpx.AsyncClient(timeout=30) as client:
436
+ try:
437
+ resp = await client.post(url, headers=headers, json=body)
438
+ if resp.status_code == 429:
439
+ return [json.dumps({"error": "Rate limit exceeded (429)"})]
440
+ resp.raise_for_status()
441
+ data = resp.json() or {}
442
+ places = (data.get("places") or [])[: max(0, int(number_of_results))]
443
+ out = [json.dumps(p) for p in places]
444
+ cache_output("search_google_places_oauth", cache_key, out)
445
+ return out
446
+ except httpx.HTTPStatusError as exc:
447
+ try:
448
+ err_json = exc.response.json()
449
+ except Exception:
450
+ err_json = {"status": exc.response.status_code, "text": exc.response.text}
451
+ logging.warning(f"Places OAuth request failed: {err_json}")
452
+ return [json.dumps({"error": err_json})]
453
+ except Exception as e:
454
+ logging.exception("Places OAuth request failed")
455
+ return [json.dumps({"error": str(e)})]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev207
3
+ Version: 0.0.1.dev209
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
@@ -30,7 +30,7 @@ dhisana/utils/composite_tools.py,sha256=ZlwHCp7PXjYFUWUEeR_fTF0Z4Wg-4F6eBi1reE3F
30
30
  dhisana/utils/dataframe_tools.py,sha256=jxyvyXAMKxccST_W6o6FnBqAsvp7mGNOD6HV5V6xgeA,9242
31
31
  dhisana/utils/domain_parser.py,sha256=Kw5MPP06wK2azWQzuSiOE-DffOezLqDyF-L9JEBsMSU,1206
32
32
  dhisana/utils/email_parse_helpers.py,sha256=LIdm1B1IyGSW50y8EkxOk6YRjvxO2SJTgTKPLxYls_o,4613
33
- dhisana/utils/email_provider.py,sha256=8V-hqBDStRl0Ui73pOYxPxtkJ2JzvpHSZ88Y21QC2xM,12936
33
+ dhisana/utils/email_provider.py,sha256=oYjk-9yIgRzcPoPwnYjPvChPn0J78T5p6CoqXFPI-zk,14264
34
34
  dhisana/utils/enrich_lead_information.py,sha256=hZxSstIErqxXG40j5YyzTYTCSsTEf8YxRF25DU5-s2k,37302
35
35
  dhisana/utils/extract_email_content_for_llm.py,sha256=SQmMZ3YJtm3ZI44XiWEVAItcAwrsSSy1QzDne7LTu_Q,3713
36
36
  dhisana/utils/fetch_openai_config.py,sha256=LjWdFuUeTNeAW106pb7DLXZNElos2PlmXRe6bHZJ2hw,5159
@@ -45,6 +45,7 @@ dhisana/utils/generate_linkedin_connect_message.py,sha256=eL4RV2B1ByyMnjoGohdTA0
45
45
  dhisana/utils/generate_linkedin_response_message.py,sha256=udAt4V_vNuieyyfhrtTFWA1CiBx63jfac9E35wunS5k,14404
46
46
  dhisana/utils/generate_structured_output_internal.py,sha256=83SaThDAa_fANJEZ5CSCMcPpD_MN5zMI9NU1uEtQO2E,20705
47
47
  dhisana/utils/google_custom_search.py,sha256=5rQ4uAF-hjFpd9ooJkd6CjRvSmhZHhqM0jfHItsbpzk,10071
48
+ dhisana/utils/google_oauth_tools.py,sha256=KoiNiC8iLh2CGslgQi5drHgjjO-lswLkZmkPfmtJL2U,17238
48
49
  dhisana/utils/google_workspace_tools.py,sha256=22mmJ2KyJAx4Ewo_AK2qDLF5veYcB7sWcb00vw21qhM,44727
49
50
  dhisana/utils/hubspot_clearbit.py,sha256=keNX1F_RnDl9AOPxYEOTMdukV_A9g8v9j1fZyT4tuP4,3440
50
51
  dhisana/utils/hubspot_crm_tools.py,sha256=lbXFCeq690_TDLjDG8Gm5E-2f1P5EuDqNf5j8PYpMm8,99298
@@ -91,8 +92,8 @@ dhisana/workflow/agent.py,sha256=esv7_i_XuMkV2j1nz_UlsHov_m6X5WZZiZm_tG4OBHU,565
91
92
  dhisana/workflow/flow.py,sha256=xWE3qQbM7j2B3FH8XnY3zOL_QXX4LbTW4ArndnEYJE0,1638
92
93
  dhisana/workflow/task.py,sha256=HlWz9mtrwLYByoSnePOemBUBrMEcj7KbgNjEE1oF5wo,1830
93
94
  dhisana/workflow/test.py,sha256=kwW8jWqSBNcRmoyaxlTuZCMOpGJpTbJQgHI7gSjwdzM,3399
94
- dhisana-0.0.1.dev207.dist-info/METADATA,sha256=-mLeUCfVXuCVO85-0ixPwHSWJlbOYpW-NVu780s3A2w,1190
95
- dhisana-0.0.1.dev207.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
96
- dhisana-0.0.1.dev207.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
97
- dhisana-0.0.1.dev207.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
98
- dhisana-0.0.1.dev207.dist-info/RECORD,,
95
+ dhisana-0.0.1.dev209.dist-info/METADATA,sha256=RLMggAC3XyqdtE58kUr7gC1pBpv2TEN_RGTXfOjRUh4,1190
96
+ dhisana-0.0.1.dev209.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
+ dhisana-0.0.1.dev209.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
98
+ dhisana-0.0.1.dev209.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
99
+ dhisana-0.0.1.dev209.dist-info/RECORD,,