dhisana 0.0.1.dev226__tar.gz → 0.0.1.dev228__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/setup.py +1 -1
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/schemas/common.py +9 -1
- dhisana-0.0.1.dev228/src/dhisana/utils/email_body_utils.py +72 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/google_oauth_tools.py +25 -2
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/google_workspace_tools.py +61 -6
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/mailgun_tools.py +14 -2
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/microsoft365_tools.py +10 -2
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/sendgrid_tools.py +14 -3
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/smtp_email_tools.py +38 -1
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana.egg-info/PKG-INFO +1 -1
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana.egg-info/SOURCES.txt +2 -0
- dhisana-0.0.1.dev228/tests/test_email_body_utils.py +23 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/README.md +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/setup.cfg +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/schemas/sales.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/add_mapping.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/apollo_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/cache_output_tools_local.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/clean_properties.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/compose_search_query.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/dataframe_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/email_parse_helpers.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/email_provider.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/enrich_lead_information.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/fetch_openai_config.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/field_validators.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_content.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_email.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_email_response.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/profile.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/proxy_curl_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/search_router.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/search_router_jobs.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/serarch_router_local_business.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/serpapi_google_search.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/serpapi_search_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/serperdev_local_business.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/serperdev_search.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/test_connect.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/tests/test_agent_tools.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/tests/test_apollo_company_search.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/tests/test_apollo_lead_search.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/tests/test_connectivity.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/tests/test_google_document.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/tests/test_hubspot_call_logs.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/tests/test_linkedin_serper.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/tests/test_mcp_connectivity.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/tests/test_proxycurl_get_company_search_id.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/tests/test_proxycurl_job_count.py +0 -0
- {dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/tests/test_structured_output_with_mcp.py +0 -0
|
@@ -364,6 +364,12 @@ class Integration(IntegrationBase):
|
|
|
364
364
|
Integration.model_rebuild()
|
|
365
365
|
IntegrationUpdate.model_rebuild()
|
|
366
366
|
|
|
367
|
+
class BodyFormat(str, Enum):
|
|
368
|
+
AUTO = "auto"
|
|
369
|
+
HTML = "html"
|
|
370
|
+
TEXT = "text"
|
|
371
|
+
|
|
372
|
+
|
|
367
373
|
class SendEmailContext(BaseModel):
|
|
368
374
|
recipient: str
|
|
369
375
|
subject: str
|
|
@@ -371,6 +377,7 @@ class SendEmailContext(BaseModel):
|
|
|
371
377
|
sender_name: str
|
|
372
378
|
sender_email: str
|
|
373
379
|
labels: Optional[List[str]]
|
|
380
|
+
body_format: BodyFormat = BodyFormat.AUTO
|
|
374
381
|
|
|
375
382
|
class QueryEmailContext(BaseModel):
|
|
376
383
|
start_time: str
|
|
@@ -386,4 +393,5 @@ class ReplyEmailContext(BaseModel):
|
|
|
386
393
|
sender_email: str
|
|
387
394
|
sender_name: str
|
|
388
395
|
mark_as_read: str = "True"
|
|
389
|
-
add_labels: Optional[List[str]] = None
|
|
396
|
+
add_labels: Optional[List[str]] = None
|
|
397
|
+
reply_body_format: BodyFormat = BodyFormat.AUTO
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Small helpers for handling e-mail bodies across providers."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
import html as html_lib
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def looks_like_html(text: str) -> bool:
|
|
9
|
+
"""Heuristically determine whether the body contains HTML markup."""
|
|
10
|
+
return bool(text and re.search(r"<[a-zA-Z][^>]*>", text))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _normalize_format_hint(format_hint: Optional[str]) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Normalize a user-supplied format hint into html/text/auto.
|
|
16
|
+
|
|
17
|
+
Accepts variations like "plain" or "plaintext" as text.
|
|
18
|
+
"""
|
|
19
|
+
if not format_hint:
|
|
20
|
+
return "auto"
|
|
21
|
+
fmt_raw = getattr(format_hint, "value", format_hint)
|
|
22
|
+
fmt = str(fmt_raw).strip().lower()
|
|
23
|
+
if fmt in ("html",):
|
|
24
|
+
return "html"
|
|
25
|
+
if fmt in ("text", "plain", "plain_text", "plaintext"):
|
|
26
|
+
return "text"
|
|
27
|
+
return "auto"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def html_to_plain_text(html: str) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Produce a very lightweight plain-text version of an HTML fragment.
|
|
33
|
+
This keeps newlines on block boundaries and strips tags.
|
|
34
|
+
"""
|
|
35
|
+
if not html:
|
|
36
|
+
return ""
|
|
37
|
+
text = re.sub(r"(?is)<(script|style).*?>.*?</\1>", " ", html)
|
|
38
|
+
text = re.sub(r"(?i)<br\s*/?>", "\n", text)
|
|
39
|
+
text = re.sub(r"(?i)</(p|div|li|h[1-6])\s*>", "\n", text)
|
|
40
|
+
text = re.sub(r"(?is)<.*?>", "", text)
|
|
41
|
+
text = html_lib.unescape(text)
|
|
42
|
+
text = re.sub(r"\s+\n", "\n", text)
|
|
43
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
44
|
+
return text.strip()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def plain_text_to_html(text: str) -> str:
|
|
48
|
+
"""Wrap plain text in a minimal HTML container that preserves newlines."""
|
|
49
|
+
if text is None:
|
|
50
|
+
return ""
|
|
51
|
+
escaped = html_lib.escape(text)
|
|
52
|
+
return f'<div style="white-space: pre-wrap">{escaped}</div>'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def body_variants(body: Optional[str], format_hint: Optional[str]) -> Tuple[str, str, str]:
|
|
56
|
+
"""
|
|
57
|
+
Return (plain, html, resolved_format) honoring an optional format hint.
|
|
58
|
+
|
|
59
|
+
resolved_format is "html" or "text" after applying auto-detection.
|
|
60
|
+
"""
|
|
61
|
+
content = body or ""
|
|
62
|
+
fmt = _normalize_format_hint(format_hint)
|
|
63
|
+
|
|
64
|
+
if fmt == "html":
|
|
65
|
+
return html_to_plain_text(content), content, "html"
|
|
66
|
+
if fmt == "text":
|
|
67
|
+
return content, plain_text_to_html(content), "text"
|
|
68
|
+
|
|
69
|
+
if looks_like_html(content):
|
|
70
|
+
return html_to_plain_text(content), content, "html"
|
|
71
|
+
|
|
72
|
+
return content, plain_text_to_html(content), "text"
|
|
@@ -2,6 +2,7 @@ import base64
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
import re
|
|
5
|
+
from email.mime.multipart import MIMEMultipart
|
|
5
6
|
from email.mime.text import MIMEText
|
|
6
7
|
from typing import Any, Dict, List, Optional
|
|
7
8
|
|
|
@@ -22,6 +23,7 @@ from dhisana.utils.email_parse_helpers import (
|
|
|
22
23
|
)
|
|
23
24
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
24
25
|
from dhisana.utils.cache_output_tools import retrieve_output, cache_output
|
|
26
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
25
27
|
from typing import Optional as _Optional # avoid name clash in wrappers
|
|
26
28
|
|
|
27
29
|
def _status_phrase(code: int) -> str:
|
|
@@ -127,7 +129,18 @@ async def send_email_using_google_oauth_async(
|
|
|
127
129
|
"""
|
|
128
130
|
token = get_google_access_token(tool_config)
|
|
129
131
|
|
|
130
|
-
|
|
132
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
133
|
+
send_email_context.body,
|
|
134
|
+
getattr(send_email_context, "body_format", None),
|
|
135
|
+
)
|
|
136
|
+
# Use multipart/alternative when we have both; fall back to single part for pure text.
|
|
137
|
+
if resolved_fmt == "text":
|
|
138
|
+
message = MIMEText(plain_body, "plain", _charset="utf-8")
|
|
139
|
+
else:
|
|
140
|
+
message = MIMEMultipart("alternative")
|
|
141
|
+
message.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
142
|
+
message.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
143
|
+
|
|
131
144
|
message["to"] = send_email_context.recipient
|
|
132
145
|
message["from"] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
133
146
|
message["subject"] = send_email_context.subject
|
|
@@ -265,7 +278,17 @@ async def reply_to_email_google_oauth_async(
|
|
|
265
278
|
message_id_header = headers_map.get("Message-ID", "") or ""
|
|
266
279
|
|
|
267
280
|
# 2) Build reply MIME
|
|
268
|
-
|
|
281
|
+
plain_reply, html_reply, resolved_reply_fmt = body_variants(
|
|
282
|
+
reply_email_context.reply_body,
|
|
283
|
+
getattr(reply_email_context, "reply_body_format", None),
|
|
284
|
+
)
|
|
285
|
+
if resolved_reply_fmt == "text":
|
|
286
|
+
msg = MIMEText(plain_reply, "plain", _charset="utf-8")
|
|
287
|
+
else:
|
|
288
|
+
msg = MIMEMultipart("alternative")
|
|
289
|
+
msg.attach(MIMEText(plain_reply, "plain", _charset="utf-8"))
|
|
290
|
+
msg.attach(MIMEText(html_reply, "html", _charset="utf-8"))
|
|
291
|
+
|
|
269
292
|
msg["To"] = to_addresses
|
|
270
293
|
if cc_addresses:
|
|
271
294
|
msg["Cc"] = cc_addresses
|
|
@@ -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
|
|
|
@@ -22,8 +24,9 @@ from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
|
|
|
22
24
|
from dhisana.schemas.sales import MessageItem
|
|
23
25
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
24
26
|
from dhisana.utils.email_parse_helpers import *
|
|
27
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
25
28
|
import asyncio
|
|
26
|
-
from dhisana.schemas.common import (SendEmailContext, QueryEmailContext, ReplyEmailContext)
|
|
29
|
+
from dhisana.schemas.common import (SendEmailContext, QueryEmailContext, ReplyEmailContext, BodyFormat)
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
################################################################################
|
|
@@ -109,6 +112,28 @@ def get_google_credentials(
|
|
|
109
112
|
return credentials
|
|
110
113
|
|
|
111
114
|
|
|
115
|
+
def _looks_like_html(text: str) -> bool:
|
|
116
|
+
"""Heuristically determine whether the body contains HTML markup."""
|
|
117
|
+
return bool(text and re.search(r"<[a-zA-Z][^>]*>", text))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _html_to_plain_text(html: str) -> str:
|
|
121
|
+
"""
|
|
122
|
+
Produce a very lightweight plain-text version of an HTML fragment.
|
|
123
|
+
This keeps newlines on block boundaries and strips tags.
|
|
124
|
+
"""
|
|
125
|
+
if not html:
|
|
126
|
+
return ""
|
|
127
|
+
text = re.sub(r"(?is)<(script|style).*?>.*?</\1>", " ", html)
|
|
128
|
+
text = re.sub(r"(?i)<br\s*/?>", "\n", text)
|
|
129
|
+
text = re.sub(r"(?i)</(p|div|li|h[1-6])\s*>", "\n", text)
|
|
130
|
+
text = re.sub(r"(?is)<.*?>", "", text)
|
|
131
|
+
text = html_lib.unescape(text)
|
|
132
|
+
text = re.sub(r"\s+\n", "\n", text)
|
|
133
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
134
|
+
return text.strip()
|
|
135
|
+
|
|
136
|
+
|
|
112
137
|
|
|
113
138
|
@assistant_tool
|
|
114
139
|
async def send_email_using_service_account_async(
|
|
@@ -137,8 +162,19 @@ async def send_email_using_service_account_async(
|
|
|
137
162
|
|
|
138
163
|
gmail_api_url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'
|
|
139
164
|
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
166
|
+
send_email_context.body,
|
|
167
|
+
getattr(send_email_context, "body_format", None),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if resolved_fmt == "text":
|
|
171
|
+
message = MIMEText(plain_body, _subtype="plain", _charset="utf-8")
|
|
172
|
+
else:
|
|
173
|
+
# Gmail prefers multipart/alternative when HTML is present.
|
|
174
|
+
message = MIMEMultipart("alternative")
|
|
175
|
+
message.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
176
|
+
message.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
177
|
+
|
|
142
178
|
message['to'] = send_email_context.recipient
|
|
143
179
|
message['from'] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
144
180
|
message['subject'] = send_email_context.subject
|
|
@@ -477,6 +513,7 @@ class SendEmailContext(BaseModel):
|
|
|
477
513
|
sender_name: str
|
|
478
514
|
sender_email: str
|
|
479
515
|
labels: Optional[List[str]]
|
|
516
|
+
body_format: BodyFormat = BodyFormat.AUTO
|
|
480
517
|
|
|
481
518
|
@assistant_tool
|
|
482
519
|
async def send_email_using_service_account_async(
|
|
@@ -505,8 +542,18 @@ async def send_email_using_service_account_async(
|
|
|
505
542
|
|
|
506
543
|
gmail_api_url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'
|
|
507
544
|
|
|
545
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
546
|
+
send_email_context.body,
|
|
547
|
+
getattr(send_email_context, "body_format", None),
|
|
548
|
+
)
|
|
549
|
+
|
|
508
550
|
# Construct the MIME text message
|
|
509
|
-
|
|
551
|
+
if resolved_fmt == "text":
|
|
552
|
+
message = MIMEText(plain_body, _subtype="plain", _charset="utf-8")
|
|
553
|
+
else:
|
|
554
|
+
message = MIMEMultipart("alternative")
|
|
555
|
+
message.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
556
|
+
message.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
510
557
|
message['to'] = send_email_context.recipient
|
|
511
558
|
message['from'] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
512
559
|
message['subject'] = send_email_context.subject
|
|
@@ -861,7 +908,16 @@ async def reply_to_email_async(
|
|
|
861
908
|
message_id_header = headers_dict.get('Message-ID', '')
|
|
862
909
|
|
|
863
910
|
# 3. Create the reply email message
|
|
864
|
-
|
|
911
|
+
plain_reply, html_reply, resolved_reply_fmt = body_variants(
|
|
912
|
+
reply_email_context.reply_body,
|
|
913
|
+
getattr(reply_email_context, "reply_body_format", None),
|
|
914
|
+
)
|
|
915
|
+
if resolved_reply_fmt == "text":
|
|
916
|
+
msg = MIMEText(plain_reply, _subtype="plain", _charset="utf-8")
|
|
917
|
+
else:
|
|
918
|
+
msg = MIMEMultipart("alternative")
|
|
919
|
+
msg.attach(MIMEText(plain_reply, "plain", _charset="utf-8"))
|
|
920
|
+
msg.attach(MIMEText(html_reply, "html", _charset="utf-8"))
|
|
865
921
|
msg['To'] = to_addresses
|
|
866
922
|
if cc_addresses:
|
|
867
923
|
msg['Cc'] = cc_addresses
|
|
@@ -1186,4 +1242,3 @@ def save_values_to_csv(values: List[List[str]], output_filename: str) -> str:
|
|
|
1186
1242
|
writer.writerows(values)
|
|
1187
1243
|
|
|
1188
1244
|
return local_file_path
|
|
1189
|
-
|
|
@@ -7,6 +7,7 @@ import aiohttp
|
|
|
7
7
|
|
|
8
8
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
9
9
|
from dhisana.schemas.common import SendEmailContext
|
|
10
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def get_mailgun_notify_key(tool_config: Optional[List[Dict]] = None) -> str:
|
|
@@ -59,6 +60,7 @@ async def send_email_with_mailgun(
|
|
|
59
60
|
subject: str,
|
|
60
61
|
message: str,
|
|
61
62
|
tool_config: Optional[List[Dict]] = None,
|
|
63
|
+
body_format: Optional[str] = None,
|
|
62
64
|
):
|
|
63
65
|
"""
|
|
64
66
|
Send an email using the Mailgun API.
|
|
@@ -74,13 +76,17 @@ async def send_email_with_mailgun(
|
|
|
74
76
|
api_key = get_mailgun_notify_key(tool_config)
|
|
75
77
|
domain = get_mailgun_notify_domain(tool_config)
|
|
76
78
|
|
|
79
|
+
body = message or ""
|
|
77
80
|
data = {
|
|
78
81
|
"from": sender,
|
|
79
82
|
"to": recipients,
|
|
80
83
|
"subject": subject,
|
|
81
|
-
"html": message,
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
plain_body, html_body, _ = body_variants(body, body_format)
|
|
87
|
+
data["text"] = plain_body
|
|
88
|
+
data["html"] = html_body
|
|
89
|
+
|
|
84
90
|
async with aiohttp.ClientSession() as session:
|
|
85
91
|
async with session.post(
|
|
86
92
|
f"https://api.mailgun.net/v3/{domain}/messages",
|
|
@@ -107,11 +113,17 @@ async def send_email_using_mailgun_async(
|
|
|
107
113
|
api_key = get_mailgun_notify_key(tool_config)
|
|
108
114
|
domain = get_mailgun_notify_domain(tool_config)
|
|
109
115
|
|
|
116
|
+
plain_body, html_body, _ = body_variants(
|
|
117
|
+
send_email_context.body,
|
|
118
|
+
getattr(send_email_context, "body_format", None),
|
|
119
|
+
)
|
|
120
|
+
|
|
110
121
|
data = {
|
|
111
122
|
"from": f"{send_email_context.sender_name} <{send_email_context.sender_email}>",
|
|
112
123
|
"to": [send_email_context.recipient],
|
|
113
124
|
"subject": send_email_context.subject,
|
|
114
|
-
"
|
|
125
|
+
"text": plain_body,
|
|
126
|
+
"html": html_body,
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
async with aiohttp.ClientSession() as session:
|
|
@@ -12,6 +12,7 @@ from dhisana.schemas.common import (
|
|
|
12
12
|
ReplyEmailContext,
|
|
13
13
|
)
|
|
14
14
|
from dhisana.schemas.sales import MessageItem
|
|
15
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def get_microsoft365_access_token(tool_config: Optional[List[Dict]] = None) -> str:
|
|
@@ -149,11 +150,18 @@ async def send_email_using_microsoft_graph_async(
|
|
|
149
150
|
base_url = "https://graph.microsoft.com/v1.0"
|
|
150
151
|
base_res = _base_resource(sender_email, tool_config, auth_mode)
|
|
151
152
|
|
|
153
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
154
|
+
send_email_context.body,
|
|
155
|
+
getattr(send_email_context, "body_format", None),
|
|
156
|
+
)
|
|
157
|
+
content_type = "Text" if resolved_fmt == "text" else "HTML"
|
|
158
|
+
content_body = plain_body if resolved_fmt == "text" else html_body
|
|
159
|
+
|
|
152
160
|
message_payload: Dict[str, Any] = {
|
|
153
161
|
"subject": send_email_context.subject,
|
|
154
162
|
"body": {
|
|
155
|
-
"contentType":
|
|
156
|
-
"content":
|
|
163
|
+
"contentType": content_type,
|
|
164
|
+
"content": content_body,
|
|
157
165
|
},
|
|
158
166
|
"toRecipients": [
|
|
159
167
|
{"emailAddress": {"address": send_email_context.recipient}}
|
|
@@ -14,6 +14,7 @@ import aiohttp
|
|
|
14
14
|
|
|
15
15
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
16
16
|
from dhisana.schemas.common import SendEmailContext
|
|
17
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
17
18
|
|
|
18
19
|
# --------------------------------------------------------------------------- #
|
|
19
20
|
# Mailgun (re-exported from dedicated module for backward compatibility)
|
|
@@ -57,6 +58,7 @@ async def send_email_with_sendgrid(
|
|
|
57
58
|
subject: str,
|
|
58
59
|
message: str,
|
|
59
60
|
tool_config: Optional[List[Dict]] = None,
|
|
61
|
+
body_format: Optional[str] = None,
|
|
60
62
|
):
|
|
61
63
|
"""
|
|
62
64
|
Send an email using SendGrid's v3 Mail Send API.
|
|
@@ -79,6 +81,12 @@ async def send_email_with_sendgrid(
|
|
|
79
81
|
if not to_list:
|
|
80
82
|
return {"error": "No recipients provided"}
|
|
81
83
|
|
|
84
|
+
plain_body, html_body, _ = body_variants(message, body_format)
|
|
85
|
+
content = [
|
|
86
|
+
{"type": "text/plain", "value": plain_body},
|
|
87
|
+
{"type": "text/html", "value": html_body},
|
|
88
|
+
]
|
|
89
|
+
|
|
82
90
|
payload = {
|
|
83
91
|
"personalizations": [
|
|
84
92
|
{
|
|
@@ -87,9 +95,7 @@ async def send_email_with_sendgrid(
|
|
|
87
95
|
}
|
|
88
96
|
],
|
|
89
97
|
"from": from_obj,
|
|
90
|
-
"content":
|
|
91
|
-
{"type": "text/html", "value": message or ""}
|
|
92
|
-
],
|
|
98
|
+
"content": content,
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
headers = {
|
|
@@ -126,11 +132,16 @@ async def send_email_using_sendgrid_async(
|
|
|
126
132
|
Provider-style wrapper for SendGrid using SendEmailContext.
|
|
127
133
|
Returns an opaque token since SendGrid does not return a message id.
|
|
128
134
|
"""
|
|
135
|
+
plain_body, html_body, _ = body_variants(
|
|
136
|
+
ctx.body,
|
|
137
|
+
getattr(ctx, "body_format", None),
|
|
138
|
+
)
|
|
129
139
|
result = await send_email_with_sendgrid(
|
|
130
140
|
sender=f"{ctx.sender_name} <{ctx.sender_email}>",
|
|
131
141
|
recipients=[ctx.recipient],
|
|
132
142
|
subject=ctx.subject,
|
|
133
143
|
message=ctx.body or "",
|
|
144
|
+
body_format=getattr(ctx, "body_format", None),
|
|
134
145
|
tool_config=tool_config,
|
|
135
146
|
)
|
|
136
147
|
# Normalise output to a string id-like value
|
|
@@ -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
|
|
@@ -31,6 +33,7 @@ from dhisana.utils.google_workspace_tools import (
|
|
|
31
33
|
QueryEmailContext,
|
|
32
34
|
SendEmailContext,
|
|
33
35
|
)
|
|
36
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
# --------------------------------------------------------------------------- #
|
|
@@ -102,6 +105,28 @@ def _to_datetime(val: Union[datetime, str]) -> datetime:
|
|
|
102
105
|
) from exc
|
|
103
106
|
|
|
104
107
|
|
|
108
|
+
def _looks_like_html(text: str) -> bool:
|
|
109
|
+
"""Heuristically determine whether the body contains HTML markup."""
|
|
110
|
+
return bool(text and re.search(r"<[a-zA-Z][^>]*>", text))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _html_to_plain_text(html: str) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Produce a very lightweight plain-text version of an HTML fragment.
|
|
116
|
+
This keeps newlines on block boundaries and strips tags.
|
|
117
|
+
"""
|
|
118
|
+
if not html:
|
|
119
|
+
return ""
|
|
120
|
+
text = re.sub(r"(?is)<(script|style).*?>.*?</\1>", " ", html)
|
|
121
|
+
text = re.sub(r"(?i)<br\s*/?>", "\n", text)
|
|
122
|
+
text = re.sub(r"(?i)</(p|div|li|h[1-6])\s*>", "\n", text)
|
|
123
|
+
text = re.sub(r"(?is)<.*?>", "", text)
|
|
124
|
+
text = html_lib.unescape(text)
|
|
125
|
+
text = re.sub(r"\s+\n", "\n", text)
|
|
126
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
127
|
+
return text.strip()
|
|
128
|
+
|
|
129
|
+
|
|
105
130
|
# --------------------------------------------------------------------------- #
|
|
106
131
|
# Outbound -- SMTP
|
|
107
132
|
# --------------------------------------------------------------------------- #
|
|
@@ -127,7 +152,19 @@ async def send_email_via_smtp_async(
|
|
|
127
152
|
str
|
|
128
153
|
The Message-ID of the sent message (e.g., "<uuid@yourdomain.com>").
|
|
129
154
|
"""
|
|
130
|
-
|
|
155
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
156
|
+
ctx.body,
|
|
157
|
+
getattr(ctx, "body_format", None),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if resolved_fmt == "text":
|
|
161
|
+
msg = MIMEText(plain_body, _subtype="plain", _charset="utf-8")
|
|
162
|
+
else:
|
|
163
|
+
# Build multipart/alternative so HTML-capable clients see rich content.
|
|
164
|
+
msg = MIMEMultipart("alternative")
|
|
165
|
+
msg.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
166
|
+
msg.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
167
|
+
|
|
131
168
|
msg["From"] = f"{ctx.sender_name} <{ctx.sender_email}>"
|
|
132
169
|
msg["To"] = ctx.recipient
|
|
133
170
|
msg["Subject"] = ctx.subject
|
|
@@ -38,6 +38,7 @@ src/dhisana/utils/compose_three_step_workflow.py
|
|
|
38
38
|
src/dhisana/utils/composite_tools.py
|
|
39
39
|
src/dhisana/utils/dataframe_tools.py
|
|
40
40
|
src/dhisana/utils/domain_parser.py
|
|
41
|
+
src/dhisana/utils/email_body_utils.py
|
|
41
42
|
src/dhisana/utils/email_parse_helpers.py
|
|
42
43
|
src/dhisana/utils/email_provider.py
|
|
43
44
|
src/dhisana/utils/enrich_lead_information.py
|
|
@@ -105,6 +106,7 @@ tests/test_agent_tools.py
|
|
|
105
106
|
tests/test_apollo_company_search.py
|
|
106
107
|
tests/test_apollo_lead_search.py
|
|
107
108
|
tests/test_connectivity.py
|
|
109
|
+
tests/test_email_body_utils.py
|
|
108
110
|
tests/test_google_document.py
|
|
109
111
|
tests/test_hubspot_call_logs.py
|
|
110
112
|
tests/test_linkedin_serper.py
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from dhisana.schemas.common import BodyFormat
|
|
4
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize(
|
|
8
|
+
"body,format_hint,expected_resolved",
|
|
9
|
+
[
|
|
10
|
+
("<p>Hello</p>", BodyFormat.HTML, "html"),
|
|
11
|
+
("Hello", BodyFormat.TEXT, "text"),
|
|
12
|
+
],
|
|
13
|
+
)
|
|
14
|
+
def test_body_variants_honors_body_format_enum(body, format_hint, expected_resolved):
|
|
15
|
+
plain, html, resolved = body_variants(body, format_hint)
|
|
16
|
+
|
|
17
|
+
if expected_resolved == "html":
|
|
18
|
+
assert html == body
|
|
19
|
+
assert plain == "Hello"
|
|
20
|
+
else:
|
|
21
|
+
assert plain == body
|
|
22
|
+
assert html.startswith("<div")
|
|
23
|
+
assert resolved == expected_resolved
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/check_email_validity_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/check_linkedin_url_validity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/compose_three_step_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/extract_email_content_for_llm.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_linkedin_connect_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/openai_assistant_and_file_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/openapi_tool/openapi_tool.py
RENAMED
|
File without changes
|
{dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/parse_linkedin_messages_txt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/serarch_router_local_business.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev226 → dhisana-0.0.1.dev228}/src/dhisana/utils/serpapi_local_business_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|