dhisana 0.0.1.dev225__py3-none-any.whl → 0.0.1.dev227__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.
- dhisana/utils/google_workspace_tools.py +34 -3
- dhisana/utils/smtp_email_tools.py +34 -1
- dhisana/utils/test_connect.py +93 -0
- {dhisana-0.0.1.dev225.dist-info → dhisana-0.0.1.dev227.dist-info}/METADATA +1 -1
- {dhisana-0.0.1.dev225.dist-info → dhisana-0.0.1.dev227.dist-info}/RECORD +8 -8
- {dhisana-0.0.1.dev225.dist-info → dhisana-0.0.1.dev227.dist-info}/WHEEL +0 -0
- {dhisana-0.0.1.dev225.dist-info → dhisana-0.0.1.dev227.dist-info}/entry_points.txt +0 -0
- {dhisana-0.0.1.dev225.dist-info → dhisana-0.0.1.dev227.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import csv
|
|
3
3
|
import datetime
|
|
4
|
+
import html as html_lib
|
|
4
5
|
import io
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
8
|
import os
|
|
8
9
|
import re
|
|
9
10
|
import uuid
|
|
11
|
+
from email.mime.multipart import MIMEMultipart
|
|
10
12
|
from email.mime.text import MIMEText
|
|
11
13
|
from typing import Any, Dict, List, Optional
|
|
12
14
|
|
|
@@ -109,6 +111,28 @@ def get_google_credentials(
|
|
|
109
111
|
return credentials
|
|
110
112
|
|
|
111
113
|
|
|
114
|
+
def _looks_like_html(text: str) -> bool:
|
|
115
|
+
"""Heuristically determine whether the body contains HTML markup."""
|
|
116
|
+
return bool(text and re.search(r"<[a-zA-Z][^>]*>", text))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _html_to_plain_text(html: str) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Produce a very lightweight plain-text version of an HTML fragment.
|
|
122
|
+
This keeps newlines on block boundaries and strips tags.
|
|
123
|
+
"""
|
|
124
|
+
if not html:
|
|
125
|
+
return ""
|
|
126
|
+
text = re.sub(r"(?is)<(script|style).*?>.*?</\1>", " ", html)
|
|
127
|
+
text = re.sub(r"(?i)<br\s*/?>", "\n", text)
|
|
128
|
+
text = re.sub(r"(?i)</(p|div|li|h[1-6])\s*>", "\n", text)
|
|
129
|
+
text = re.sub(r"(?is)<.*?>", "", text)
|
|
130
|
+
text = html_lib.unescape(text)
|
|
131
|
+
text = re.sub(r"\s+\n", "\n", text)
|
|
132
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
133
|
+
return text.strip()
|
|
134
|
+
|
|
135
|
+
|
|
112
136
|
|
|
113
137
|
@assistant_tool
|
|
114
138
|
async def send_email_using_service_account_async(
|
|
@@ -137,8 +161,16 @@ async def send_email_using_service_account_async(
|
|
|
137
161
|
|
|
138
162
|
gmail_api_url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'
|
|
139
163
|
|
|
140
|
-
|
|
141
|
-
|
|
164
|
+
body = send_email_context.body or ""
|
|
165
|
+
|
|
166
|
+
if _looks_like_html(body):
|
|
167
|
+
# Gmail prefers multipart/alternative when HTML is present.
|
|
168
|
+
message = MIMEMultipart("alternative")
|
|
169
|
+
message.attach(MIMEText(_html_to_plain_text(body), "plain", _charset="utf-8"))
|
|
170
|
+
message.attach(MIMEText(body, "html", _charset="utf-8"))
|
|
171
|
+
else:
|
|
172
|
+
message = MIMEText(body, _subtype="plain", _charset="utf-8")
|
|
173
|
+
|
|
142
174
|
message['to'] = send_email_context.recipient
|
|
143
175
|
message['from'] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
144
176
|
message['subject'] = send_email_context.subject
|
|
@@ -1186,4 +1218,3 @@ def save_values_to_csv(values: List[List[str]], output_filename: str) -> str:
|
|
|
1186
1218
|
writer.writerows(values)
|
|
1187
1219
|
|
|
1188
1220
|
return local_file_path
|
|
1189
|
-
|
|
@@ -7,12 +7,14 @@ import datetime
|
|
|
7
7
|
import email
|
|
8
8
|
import email.utils
|
|
9
9
|
import hashlib
|
|
10
|
+
import html as html_lib
|
|
10
11
|
import imaplib
|
|
11
12
|
import logging
|
|
12
13
|
import re
|
|
13
14
|
import uuid
|
|
14
15
|
from email.errors import HeaderParseError
|
|
15
16
|
from email.header import Header, decode_header, make_header
|
|
17
|
+
from email.mime.multipart import MIMEMultipart
|
|
16
18
|
from email.mime.text import MIMEText
|
|
17
19
|
from datetime import datetime, timedelta, timezone
|
|
18
20
|
from typing import Any, Dict, List, Optional, Union
|
|
@@ -102,6 +104,28 @@ def _to_datetime(val: Union[datetime, str]) -> datetime:
|
|
|
102
104
|
) from exc
|
|
103
105
|
|
|
104
106
|
|
|
107
|
+
def _looks_like_html(text: str) -> bool:
|
|
108
|
+
"""Heuristically determine whether the body contains HTML markup."""
|
|
109
|
+
return bool(text and re.search(r"<[a-zA-Z][^>]*>", text))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _html_to_plain_text(html: str) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Produce a very lightweight plain-text version of an HTML fragment.
|
|
115
|
+
This keeps newlines on block boundaries and strips tags.
|
|
116
|
+
"""
|
|
117
|
+
if not html:
|
|
118
|
+
return ""
|
|
119
|
+
text = re.sub(r"(?is)<(script|style).*?>.*?</\1>", " ", html)
|
|
120
|
+
text = re.sub(r"(?i)<br\s*/?>", "\n", text)
|
|
121
|
+
text = re.sub(r"(?i)</(p|div|li|h[1-6])\s*>", "\n", text)
|
|
122
|
+
text = re.sub(r"(?is)<.*?>", "", text)
|
|
123
|
+
text = html_lib.unescape(text)
|
|
124
|
+
text = re.sub(r"\s+\n", "\n", text)
|
|
125
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
126
|
+
return text.strip()
|
|
127
|
+
|
|
128
|
+
|
|
105
129
|
# --------------------------------------------------------------------------- #
|
|
106
130
|
# Outbound -- SMTP
|
|
107
131
|
# --------------------------------------------------------------------------- #
|
|
@@ -127,7 +151,16 @@ async def send_email_via_smtp_async(
|
|
|
127
151
|
str
|
|
128
152
|
The Message-ID of the sent message (e.g., "<uuid@yourdomain.com>").
|
|
129
153
|
"""
|
|
130
|
-
|
|
154
|
+
body = ctx.body or ""
|
|
155
|
+
|
|
156
|
+
if _looks_like_html(body):
|
|
157
|
+
# Build multipart/alternative so HTML-capable clients see rich content.
|
|
158
|
+
msg = MIMEMultipart("alternative")
|
|
159
|
+
msg.attach(MIMEText(_html_to_plain_text(body), "plain", _charset="utf-8"))
|
|
160
|
+
msg.attach(MIMEText(body, "html", _charset="utf-8"))
|
|
161
|
+
else:
|
|
162
|
+
msg = MIMEText(body, _subtype="plain", _charset="utf-8")
|
|
163
|
+
|
|
131
164
|
msg["From"] = f"{ctx.sender_name} <{ctx.sender_email}>"
|
|
132
165
|
msg["To"] = ctx.recipient
|
|
133
166
|
msg["Subject"] = ctx.subject
|
dhisana/utils/test_connect.py
CHANGED
|
@@ -810,6 +810,76 @@ async def test_clay(api_key: str, webhook: str) -> Dict[str, Any]:
|
|
|
810
810
|
logger.error(f"Clay test failed: {exc}")
|
|
811
811
|
return {"success": False, "status_code": 0, "error_message": str(exc)}
|
|
812
812
|
|
|
813
|
+
###############################################################################
|
|
814
|
+
# POSTHOG CONNECTIVITY TEST
|
|
815
|
+
###############################################################################
|
|
816
|
+
|
|
817
|
+
async def test_posthog(
|
|
818
|
+
api_host: str,
|
|
819
|
+
project_id: str,
|
|
820
|
+
personal_api_key: str,
|
|
821
|
+
) -> Dict[str, Any]:
|
|
822
|
+
"""
|
|
823
|
+
Validate PostHog connectivity by issuing a lightweight HogQL query.
|
|
824
|
+
|
|
825
|
+
Requires:
|
|
826
|
+
• api_host (e.g. https://app.posthog.com or self-hosted URL)
|
|
827
|
+
• project_id (numeric or string project identifier)
|
|
828
|
+
• personal_api_key (token with query access)
|
|
829
|
+
"""
|
|
830
|
+
base_url = (api_host or "").rstrip("/")
|
|
831
|
+
if not base_url:
|
|
832
|
+
return {
|
|
833
|
+
"success": False,
|
|
834
|
+
"status_code": 0,
|
|
835
|
+
"error_message": "Missing api_host for PostHog connectivity test.",
|
|
836
|
+
}
|
|
837
|
+
if not project_id:
|
|
838
|
+
return {
|
|
839
|
+
"success": False,
|
|
840
|
+
"status_code": 0,
|
|
841
|
+
"error_message": "Missing project_id for PostHog connectivity test.",
|
|
842
|
+
}
|
|
843
|
+
if not personal_api_key:
|
|
844
|
+
return {
|
|
845
|
+
"success": False,
|
|
846
|
+
"status_code": 0,
|
|
847
|
+
"error_message": "Missing personal_api_key for PostHog connectivity test.",
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
url = f"{base_url}/api/projects/{project_id}/query/"
|
|
851
|
+
headers = {
|
|
852
|
+
"Authorization": f"Bearer {personal_api_key}",
|
|
853
|
+
"Content-Type": "application/json",
|
|
854
|
+
}
|
|
855
|
+
payload = {"query": {"kind": "HogQLQuery", "query": "SELECT 1"}}
|
|
856
|
+
|
|
857
|
+
try:
|
|
858
|
+
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
|
|
859
|
+
async with session.post(url, headers=headers, json=payload) as response:
|
|
860
|
+
status = response.status
|
|
861
|
+
data = await safe_json(response)
|
|
862
|
+
|
|
863
|
+
if status == 200:
|
|
864
|
+
return {"success": True, "status_code": status, "error_message": None}
|
|
865
|
+
|
|
866
|
+
detail = None
|
|
867
|
+
if isinstance(data, dict):
|
|
868
|
+
detail = (
|
|
869
|
+
data.get("message")
|
|
870
|
+
or data.get("detail")
|
|
871
|
+
or data.get("error")
|
|
872
|
+
or data.get("code")
|
|
873
|
+
)
|
|
874
|
+
return {
|
|
875
|
+
"success": False,
|
|
876
|
+
"status_code": status,
|
|
877
|
+
"error_message": detail or f"PostHog responded with {status}",
|
|
878
|
+
}
|
|
879
|
+
except Exception as exc:
|
|
880
|
+
logger.error(f"PostHog connectivity test failed: {exc}")
|
|
881
|
+
return {"success": False, "status_code": 0, "error_message": str(exc)}
|
|
882
|
+
|
|
813
883
|
###############################################################################
|
|
814
884
|
# MCP SERVER CONNECTIVITY TEST
|
|
815
885
|
###############################################################################
|
|
@@ -1358,6 +1428,7 @@ async def test_connectivity(tool_config: List[Dict[str, Any]]) -> Dict[str, Dict
|
|
|
1358
1428
|
"youtube": test_youtube,
|
|
1359
1429
|
"salesforce": test_salesforce,
|
|
1360
1430
|
"clay": test_clay,
|
|
1431
|
+
"posthog": test_posthog,
|
|
1361
1432
|
"mcpServer": test_mcp_server,
|
|
1362
1433
|
"slack": test_slack,
|
|
1363
1434
|
"mailgun": test_mailgun,
|
|
@@ -1479,6 +1550,28 @@ async def test_connectivity(tool_config: List[Dict[str, Any]]) -> Dict[str, Dict
|
|
|
1479
1550
|
results[tool_name] = await test_mailgun(api_key, domain)
|
|
1480
1551
|
continue
|
|
1481
1552
|
|
|
1553
|
+
# ------------------------------------------------------------------ #
|
|
1554
|
+
# Special-case: PostHog (needs host + project id + personal API key)
|
|
1555
|
+
# ------------------------------------------------------------------ #
|
|
1556
|
+
if tool_name == "posthog":
|
|
1557
|
+
api_host = next((c["value"] for c in config_entries if c["name"] == "api_host"), None)
|
|
1558
|
+
project_id = next((c["value"] for c in config_entries if c["name"] == "project_id"), None)
|
|
1559
|
+
personal_api_key = next(
|
|
1560
|
+
(c["value"] for c in config_entries if c["name"] in ("personal_api_key", "personalApiKey")),
|
|
1561
|
+
None,
|
|
1562
|
+
)
|
|
1563
|
+
|
|
1564
|
+
if not api_host or not project_id or not personal_api_key:
|
|
1565
|
+
results[tool_name] = {
|
|
1566
|
+
"success": False,
|
|
1567
|
+
"status_code": 0,
|
|
1568
|
+
"error_message": "Missing api_host, project_id, or personal_api_key for PostHog.",
|
|
1569
|
+
}
|
|
1570
|
+
else:
|
|
1571
|
+
logger.info("Testing connectivity for PostHog…")
|
|
1572
|
+
results[tool_name] = await test_posthog(api_host, project_id, personal_api_key)
|
|
1573
|
+
continue
|
|
1574
|
+
|
|
1482
1575
|
# ------------------------------------------------------------------ #
|
|
1483
1576
|
# Special-case: Salesforce (requires credentials)
|
|
1484
1577
|
# ------------------------------------------------------------------ #
|
|
@@ -46,7 +46,7 @@ dhisana/utils/generate_linkedin_response_message.py,sha256=udAt4V_vNuieyyfhrtTFW
|
|
|
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
48
|
dhisana/utils/google_oauth_tools.py,sha256=cYynzqRgv79aPMK8DehrXUyAPReO3hyDJiJ8-1008XM,25232
|
|
49
|
-
dhisana/utils/google_workspace_tools.py,sha256=
|
|
49
|
+
dhisana/utils/google_workspace_tools.py,sha256=UVxEicpPsm_DfadXygp6ELzXuVfFv9cVSaQrQwFoxqM,45898
|
|
50
50
|
dhisana/utils/hubspot_clearbit.py,sha256=keNX1F_RnDl9AOPxYEOTMdukV_A9g8v9j1fZyT4tuP4,3440
|
|
51
51
|
dhisana/utils/hubspot_crm_tools.py,sha256=lbXFCeq690_TDLjDG8Gm5E-2f1P5EuDqNf5j8PYpMm8,99298
|
|
52
52
|
dhisana/utils/instantly_tools.py,sha256=hhqjDPyLE6o0dzzuvryszbK3ipnoGU2eBm6NlsUGJjY,4771
|
|
@@ -77,8 +77,8 @@ dhisana/utils/serpapi_search_tools.py,sha256=MglMPN9wHkGAHb7YAQXvEDsWK3RGS-zu3wU
|
|
|
77
77
|
dhisana/utils/serperdev_google_jobs.py,sha256=m5_2f_5y79FOFZz1A_go6m0hIUfbbAoZ0YTjUMO2BSI,4508
|
|
78
78
|
dhisana/utils/serperdev_local_business.py,sha256=JoZfTg58Hojv61cyuwA2lcnPdLT1lawnWaBNrUYWnuQ,6447
|
|
79
79
|
dhisana/utils/serperdev_search.py,sha256=_iBKIfHMq4gFv5StYz58eArriygoi1zW6VnLlux8vto,9363
|
|
80
|
-
dhisana/utils/smtp_email_tools.py,sha256=
|
|
81
|
-
dhisana/utils/test_connect.py,sha256=
|
|
80
|
+
dhisana/utils/smtp_email_tools.py,sha256=yWg5BmQgRfnHiID7waHkq2sCNuCFBHe0-uVFgWtlr7c,17035
|
|
81
|
+
dhisana/utils/test_connect.py,sha256=wkswcwEyMSVhZACHtqsZDhP6QpDMW4wq5MTcfRokvOM,69021
|
|
82
82
|
dhisana/utils/trasform_json.py,sha256=s48DoyzVVCI4dTvSUwF5X-exX6VH6nPWrjbUENkYuNE,6979
|
|
83
83
|
dhisana/utils/web_download_parse_tools.py,sha256=ouXwH7CmjcRjoBfP5BWat86MvcGO-8rLCmWQe_eZKjc,7810
|
|
84
84
|
dhisana/utils/workflow_code_model.py,sha256=YPWse5vBb3O6Km2PvKh1Q3AB8qBkzLt1CrR5xOL9Mro,99
|
|
@@ -92,8 +92,8 @@ dhisana/workflow/agent.py,sha256=esv7_i_XuMkV2j1nz_UlsHov_m6X5WZZiZm_tG4OBHU,565
|
|
|
92
92
|
dhisana/workflow/flow.py,sha256=xWE3qQbM7j2B3FH8XnY3zOL_QXX4LbTW4ArndnEYJE0,1638
|
|
93
93
|
dhisana/workflow/task.py,sha256=HlWz9mtrwLYByoSnePOemBUBrMEcj7KbgNjEE1oF5wo,1830
|
|
94
94
|
dhisana/workflow/test.py,sha256=kwW8jWqSBNcRmoyaxlTuZCMOpGJpTbJQgHI7gSjwdzM,3399
|
|
95
|
-
dhisana-0.0.1.
|
|
96
|
-
dhisana-0.0.1.
|
|
97
|
-
dhisana-0.0.1.
|
|
98
|
-
dhisana-0.0.1.
|
|
99
|
-
dhisana-0.0.1.
|
|
95
|
+
dhisana-0.0.1.dev227.dist-info/METADATA,sha256=xJCNdyGsigROGAFmkncnMU5ogDb2sScbvmOmCfTh1SQ,1190
|
|
96
|
+
dhisana-0.0.1.dev227.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
97
|
+
dhisana-0.0.1.dev227.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
|
|
98
|
+
dhisana-0.0.1.dev227.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
|
|
99
|
+
dhisana-0.0.1.dev227.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|