dhisana 0.0.1.dev219__tar.gz → 0.0.1.dev221__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.dev219 → dhisana-0.0.1.dev221}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/setup.py +1 -1
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/apollo_tools.py +104 -41
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/test_connect.py +63 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana.egg-info/PKG-INFO +1 -1
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana.egg-info/SOURCES.txt +1 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/tests/test_apollo_company_search.py +2 -2
- dhisana-0.0.1.dev221/tests/test_apollo_lead_search.py +119 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/README.md +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/setup.cfg +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/schemas/common.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/schemas/sales.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/add_mapping.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/cache_output_tools_local.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/clean_properties.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/compose_search_query.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/dataframe_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/email_parse_helpers.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/email_provider.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/enrich_lead_information.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/fetch_openai_config.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/field_validators.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/generate_content.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/generate_email.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/generate_email_response.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/google_oauth_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/google_workspace_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/mailgun_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/microsoft365_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/profile.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/proxy_curl_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/search_router.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/search_router_jobs.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/sendgrid_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/serarch_router_local_business.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/serpapi_google_search.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/serpapi_search_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/serperdev_local_business.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/serperdev_search.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/smtp_email_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/tests/test_agent_tools.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/tests/test_connectivity.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/tests/test_google_document.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/tests/test_hubspot_call_logs.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/tests/test_linkedin_serper.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/tests/test_mcp_connectivity.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/tests/test_proxycurl_get_company_search_id.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/tests/test_proxycurl_job_count.py +0 -0
- {dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/tests/test_structured_output_with_mcp.py +0 -0
|
@@ -11,7 +11,7 @@ from dhisana.schemas.sales import LeadsQueryFilters, CompanyQueryFilters
|
|
|
11
11
|
from dhisana.utils.cache_output_tools import cache_output, retrieve_output
|
|
12
12
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
13
13
|
from urllib.parse import urlparse, parse_qs
|
|
14
|
-
from typing import Any, Dict, List, Optional, Union
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
15
15
|
|
|
16
16
|
from dhisana.utils.clean_properties import cleanup_properties
|
|
17
17
|
|
|
@@ -19,50 +19,81 @@ logging.basicConfig(level=logging.INFO)
|
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def get_apollo_access_token(tool_config: Optional[List[Dict]] = None) -> str:
|
|
22
|
+
def get_apollo_access_token(tool_config: Optional[List[Dict]] = None) -> Tuple[str, bool]:
|
|
23
23
|
"""
|
|
24
|
-
Retrieves
|
|
24
|
+
Retrieves an Apollo access token from tool configuration or environment variables.
|
|
25
25
|
|
|
26
26
|
Args:
|
|
27
|
-
tool_config (list):
|
|
28
|
-
Each dictionary should have a "name" key and a "configuration" key,
|
|
29
|
-
where "configuration" is a list of dictionaries containing "name" and "value" keys.
|
|
27
|
+
tool_config (list): Optional tool configuration payload provided to the tool.
|
|
30
28
|
|
|
31
29
|
Returns:
|
|
32
|
-
str:
|
|
30
|
+
Tuple[str, bool]: A tuple containing the token string and a boolean flag indicating
|
|
31
|
+
whether the token represents an OAuth bearer token (``True``) or an API key (``False``).
|
|
33
32
|
|
|
34
33
|
Raises:
|
|
35
34
|
ValueError: If the Apollo integration has not been configured.
|
|
36
35
|
"""
|
|
37
|
-
|
|
36
|
+
token: Optional[str] = None
|
|
37
|
+
is_oauth = False
|
|
38
38
|
|
|
39
39
|
if tool_config:
|
|
40
|
-
logger.debug(f"Tool config provided: {tool_config}")
|
|
41
40
|
apollo_config = next(
|
|
42
41
|
(item for item in tool_config if item.get("name") == "apollo"), None
|
|
43
42
|
)
|
|
44
43
|
if apollo_config:
|
|
45
44
|
config_map = {
|
|
46
|
-
item["name"]: item
|
|
45
|
+
item["name"]: item.get("value")
|
|
47
46
|
for item in apollo_config.get("configuration", [])
|
|
48
47
|
if item
|
|
49
48
|
}
|
|
50
|
-
|
|
49
|
+
|
|
50
|
+
raw_oauth = config_map.get("oauth_tokens")
|
|
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
|
+
token = (
|
|
58
|
+
raw_oauth.get("access_token")
|
|
59
|
+
or raw_oauth.get("token")
|
|
60
|
+
)
|
|
61
|
+
if token:
|
|
62
|
+
is_oauth = True
|
|
63
|
+
|
|
64
|
+
if not token:
|
|
65
|
+
direct_access_token = config_map.get("access_token")
|
|
66
|
+
if direct_access_token:
|
|
67
|
+
token = direct_access_token
|
|
68
|
+
is_oauth = True
|
|
69
|
+
|
|
70
|
+
if not token:
|
|
71
|
+
api_key = config_map.get("apiKey") or config_map.get("api_key")
|
|
72
|
+
if api_key:
|
|
73
|
+
token = api_key
|
|
74
|
+
is_oauth = False
|
|
51
75
|
else:
|
|
52
76
|
logger.warning("No 'apollo' config item found in tool_config.")
|
|
53
|
-
else:
|
|
54
|
-
logger.debug("No tool_config provided or it's None.")
|
|
55
77
|
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
if not token:
|
|
79
|
+
env_oauth_token = os.getenv("APOLLO_ACCESS_TOKEN")
|
|
80
|
+
if env_oauth_token:
|
|
81
|
+
token = env_oauth_token
|
|
82
|
+
is_oauth = True
|
|
58
83
|
|
|
59
|
-
if not
|
|
84
|
+
if not token:
|
|
85
|
+
env_api_key = os.getenv("APOLLO_API_KEY")
|
|
86
|
+
if env_api_key:
|
|
87
|
+
token = env_api_key
|
|
88
|
+
is_oauth = False
|
|
89
|
+
|
|
90
|
+
if not token:
|
|
60
91
|
logger.error("Apollo integration is not configured.")
|
|
61
92
|
raise ValueError(
|
|
62
93
|
"Apollo integration is not configured. Please configure the connection to Apollo in Integrations."
|
|
63
94
|
)
|
|
64
95
|
|
|
65
|
-
return
|
|
96
|
+
return token, is_oauth
|
|
66
97
|
|
|
67
98
|
|
|
68
99
|
@assistant_tool
|
|
@@ -94,16 +125,17 @@ async def enrich_person_info_from_apollo(
|
|
|
94
125
|
"""
|
|
95
126
|
logger.info("Entering enrich_person_info_from_apollo")
|
|
96
127
|
|
|
97
|
-
|
|
128
|
+
token, is_oauth = get_apollo_access_token(tool_config)
|
|
98
129
|
|
|
99
130
|
if not linkedin_url and not email and not phone:
|
|
100
131
|
logger.warning("No linkedin_url, email, or phone provided. At least one is required.")
|
|
101
132
|
return {'error': "At least one of linkedin_url, email, or phone must be provided"}
|
|
102
133
|
|
|
103
|
-
headers = {
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
|
|
134
|
+
headers = {"Content-Type": "application/json"}
|
|
135
|
+
if is_oauth:
|
|
136
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
137
|
+
else:
|
|
138
|
+
headers["X-Api-Key"] = token
|
|
107
139
|
|
|
108
140
|
data = {}
|
|
109
141
|
if linkedin_url:
|
|
@@ -186,11 +218,12 @@ async def lookup_person_in_apollo_by_name(
|
|
|
186
218
|
logger.warning("No full_name provided.")
|
|
187
219
|
return {'error': "Full name is required"}
|
|
188
220
|
|
|
189
|
-
|
|
190
|
-
headers = {
|
|
191
|
-
|
|
192
|
-
"
|
|
193
|
-
|
|
221
|
+
token, is_oauth = get_apollo_access_token(tool_config)
|
|
222
|
+
headers = {"Content-Type": "application/json"}
|
|
223
|
+
if is_oauth:
|
|
224
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
225
|
+
else:
|
|
226
|
+
headers["X-Api-Key"] = token
|
|
194
227
|
|
|
195
228
|
# Construct the query payload
|
|
196
229
|
data = {
|
|
@@ -263,18 +296,21 @@ async def enrich_organization_info_from_apollo(
|
|
|
263
296
|
"""
|
|
264
297
|
logger.info("Entering enrich_organization_info_from_apollo")
|
|
265
298
|
|
|
266
|
-
|
|
299
|
+
token, is_oauth = get_apollo_access_token(tool_config)
|
|
267
300
|
|
|
268
301
|
if not organization_domain:
|
|
269
302
|
logger.warning("No organization domain provided.")
|
|
270
303
|
return {'error': "organization domain must be provided"}
|
|
271
304
|
|
|
272
305
|
headers = {
|
|
273
|
-
"X-Api-Key": f"{APOLLO_API_KEY}",
|
|
274
306
|
"Content-Type": "application/json",
|
|
275
307
|
"Cache-Control": "no-cache",
|
|
276
308
|
"accept": "application/json"
|
|
277
309
|
}
|
|
310
|
+
if is_oauth:
|
|
311
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
312
|
+
else:
|
|
313
|
+
headers["X-Api-Key"] = token
|
|
278
314
|
|
|
279
315
|
cached_response = retrieve_output("enrich_organization_info_from_apollo", organization_domain)
|
|
280
316
|
if cached_response is not None:
|
|
@@ -364,12 +400,15 @@ async def search_people_with_apollo(
|
|
|
364
400
|
logger.warning("No payload given; returning empty result.")
|
|
365
401
|
return []
|
|
366
402
|
|
|
367
|
-
|
|
403
|
+
token, is_oauth = get_apollo_access_token(tool_config)
|
|
368
404
|
headers = {
|
|
369
405
|
"Cache-Control": "no-cache",
|
|
370
406
|
"Content-Type": "application/json",
|
|
371
|
-
"X-Api-Key": api_key,
|
|
372
407
|
}
|
|
408
|
+
if is_oauth:
|
|
409
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
410
|
+
else:
|
|
411
|
+
headers["X-Api-Key"] = token
|
|
373
412
|
|
|
374
413
|
url = "https://api.apollo.io/api/v1/mixed_people/search"
|
|
375
414
|
logger.info(f"Sending payload to Apollo (single page): {json.dumps(dynamic_payload, indent=2)}")
|
|
@@ -534,7 +573,7 @@ async def search_leads_with_apollo(
|
|
|
534
573
|
# Important: handle personNotTitles as well
|
|
535
574
|
"personNotTitles": "person_not_titles",
|
|
536
575
|
|
|
537
|
-
"qOrganizationJobTitles": "
|
|
576
|
+
"qOrganizationJobTitles": "q_organization_job_titles",
|
|
538
577
|
"sortAscending": "sort_ascending",
|
|
539
578
|
"sortByField": "sort_by_field",
|
|
540
579
|
"contactEmailStatusV2": "contact_email_status",
|
|
@@ -607,6 +646,8 @@ async def search_leads_with_apollo(
|
|
|
607
646
|
"organization_ids",
|
|
608
647
|
"organization_num_employees_ranges",
|
|
609
648
|
"person_not_titles", # <--- added so single item is forced into list
|
|
649
|
+
"q_organization_job_titles",
|
|
650
|
+
"organization_latest_funding_stage_cd",
|
|
610
651
|
):
|
|
611
652
|
if isinstance(final_value, str):
|
|
612
653
|
final_value = [final_value]
|
|
@@ -636,6 +677,10 @@ async def search_leads_with_apollo(
|
|
|
636
677
|
"page": 1,
|
|
637
678
|
"per_page": min(max_items, 100),
|
|
638
679
|
}
|
|
680
|
+
if query.job_openings_with_titles:
|
|
681
|
+
dynamic_payload["q_organization_job_titles"] = query.job_openings_with_titles
|
|
682
|
+
if query.latest_funding_stages:
|
|
683
|
+
dynamic_payload["organization_latest_funding_stage_cd"] = query.latest_funding_stages
|
|
639
684
|
if query.sort_by_field is not None:
|
|
640
685
|
dynamic_payload["sort_by_field"] = query.sort_by_field
|
|
641
686
|
if query.sort_ascending is not None:
|
|
@@ -746,7 +791,7 @@ async def search_leads_with_apollo_page(
|
|
|
746
791
|
"organizationNumEmployeesRanges": "organization_num_employees_ranges",
|
|
747
792
|
"personTitles": "person_titles",
|
|
748
793
|
"personNotTitles": "person_not_titles",
|
|
749
|
-
"qOrganizationJobTitles": "
|
|
794
|
+
"qOrganizationJobTitles": "q_organization_job_titles",
|
|
750
795
|
"sortAscending": "sort_ascending",
|
|
751
796
|
"sortByField": "sort_by_field",
|
|
752
797
|
"contactEmailStatusV2": "contact_email_status",
|
|
@@ -804,6 +849,8 @@ async def search_leads_with_apollo_page(
|
|
|
804
849
|
"organization_ids",
|
|
805
850
|
"organization_num_employees_ranges",
|
|
806
851
|
"person_not_titles",
|
|
852
|
+
"q_organization_job_titles",
|
|
853
|
+
"organization_latest_funding_stage_cd",
|
|
807
854
|
):
|
|
808
855
|
if isinstance(final_value, str):
|
|
809
856
|
final_value = [final_value]
|
|
@@ -827,6 +874,10 @@ async def search_leads_with_apollo_page(
|
|
|
827
874
|
or [f"{query.min_employees_in_organization or 1},{query.max_employees_in_organization or 1000}"]
|
|
828
875
|
),
|
|
829
876
|
}
|
|
877
|
+
if query.job_openings_with_titles:
|
|
878
|
+
dynamic_payload["q_organization_job_titles"] = query.job_openings_with_titles
|
|
879
|
+
if query.latest_funding_stages:
|
|
880
|
+
dynamic_payload["organization_latest_funding_stage_cd"] = query.latest_funding_stages
|
|
830
881
|
if query.sort_by_field is not None:
|
|
831
882
|
dynamic_payload["sort_by_field"] = query.sort_by_field
|
|
832
883
|
if query.sort_ascending is not None:
|
|
@@ -840,12 +891,15 @@ async def search_leads_with_apollo_page(
|
|
|
840
891
|
f" Payload: {json.dumps(page_payload, indent=2)}")
|
|
841
892
|
|
|
842
893
|
# Get the full Apollo API response with pagination metadata
|
|
843
|
-
|
|
894
|
+
token, is_oauth = get_apollo_access_token(tool_config)
|
|
844
895
|
headers = {
|
|
845
896
|
"Cache-Control": "no-cache",
|
|
846
897
|
"Content-Type": "application/json",
|
|
847
|
-
"X-Api-Key": api_key,
|
|
848
898
|
}
|
|
899
|
+
if is_oauth:
|
|
900
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
901
|
+
else:
|
|
902
|
+
headers["X-Api-Key"] = token
|
|
849
903
|
|
|
850
904
|
url = "https://api.apollo.io/api/v1/mixed_people/search"
|
|
851
905
|
|
|
@@ -950,17 +1004,20 @@ async def get_organization_details_from_apollo(
|
|
|
950
1004
|
"""
|
|
951
1005
|
logger.info("Entering get_organization_details_from_apollo")
|
|
952
1006
|
|
|
953
|
-
|
|
1007
|
+
token, is_oauth = get_apollo_access_token(tool_config)
|
|
954
1008
|
if not organization_id:
|
|
955
1009
|
logger.warning("No organization_id provided.")
|
|
956
1010
|
return {'error': "Organization ID must be provided"}
|
|
957
1011
|
|
|
958
1012
|
headers = {
|
|
959
|
-
"X-Api-Key": APOLLO_API_KEY,
|
|
960
1013
|
"Content-Type": "application/json",
|
|
961
1014
|
"Cache-Control": "no-cache",
|
|
962
1015
|
"Accept": "application/json"
|
|
963
1016
|
}
|
|
1017
|
+
if is_oauth:
|
|
1018
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
1019
|
+
else:
|
|
1020
|
+
headers["X-Api-Key"] = token
|
|
964
1021
|
|
|
965
1022
|
cached_response = retrieve_output("get_organization_details_from_apollo", organization_id)
|
|
966
1023
|
if cached_response is not None:
|
|
@@ -1202,12 +1259,15 @@ async def search_companies_with_apollo(
|
|
|
1202
1259
|
logger.warning("No payload given; returning empty result.")
|
|
1203
1260
|
return []
|
|
1204
1261
|
|
|
1205
|
-
|
|
1262
|
+
token, is_oauth = get_apollo_access_token(tool_config)
|
|
1206
1263
|
headers = {
|
|
1207
1264
|
"Cache-Control": "no-cache",
|
|
1208
1265
|
"Content-Type": "application/json",
|
|
1209
|
-
"X-Api-Key": api_key,
|
|
1210
1266
|
}
|
|
1267
|
+
if is_oauth:
|
|
1268
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
1269
|
+
else:
|
|
1270
|
+
headers["X-Api-Key"] = token
|
|
1211
1271
|
|
|
1212
1272
|
url = "https://api.apollo.io/api/v1/organizations/search"
|
|
1213
1273
|
logger.info(f"Sending payload to Apollo organizations endpoint (single page): {json.dumps(dynamic_payload, indent=2)}")
|
|
@@ -1508,12 +1568,15 @@ async def search_companies_with_apollo_page(
|
|
|
1508
1568
|
f" Payload: {json.dumps(cleaned_payload, indent=2)}")
|
|
1509
1569
|
|
|
1510
1570
|
# Get the full Apollo API response with pagination metadata
|
|
1511
|
-
|
|
1571
|
+
token, is_oauth = get_apollo_access_token(tool_config)
|
|
1512
1572
|
headers = {
|
|
1513
1573
|
"Cache-Control": "no-cache",
|
|
1514
1574
|
"Content-Type": "application/json",
|
|
1515
|
-
"X-Api-Key": api_key,
|
|
1516
1575
|
}
|
|
1576
|
+
if is_oauth:
|
|
1577
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
1578
|
+
else:
|
|
1579
|
+
headers["X-Api-Key"] = token
|
|
1517
1580
|
|
|
1518
1581
|
url = "https://api.apollo.io/api/v1/organizations/search"
|
|
1519
1582
|
|
|
@@ -296,6 +296,68 @@ async def test_proxycurl(api_key: str) -> Dict[str, Any]:
|
|
|
296
296
|
return {"success": False, "status_code": 0, "error_message": str(e)}
|
|
297
297
|
|
|
298
298
|
|
|
299
|
+
async def test_exa(api_key: str) -> Dict[str, Any]:
|
|
300
|
+
"""Verify Exa connectivity by issuing a minimal search request."""
|
|
301
|
+
|
|
302
|
+
url = "https://api.exa.ai/search"
|
|
303
|
+
headers = {
|
|
304
|
+
"x-api-key": api_key,
|
|
305
|
+
"Content-Type": "application/json",
|
|
306
|
+
"Accept": "application/json",
|
|
307
|
+
}
|
|
308
|
+
payload = {
|
|
309
|
+
"query": "Dhisana connectivity check",
|
|
310
|
+
"numResults": 1,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
timeout = aiohttp.ClientTimeout(total=10)
|
|
315
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
316
|
+
async with session.post(url, headers=headers, json=payload) as response:
|
|
317
|
+
status = response.status
|
|
318
|
+
data = await safe_json(response)
|
|
319
|
+
|
|
320
|
+
if status != 200:
|
|
321
|
+
err_message = None
|
|
322
|
+
if isinstance(data, dict):
|
|
323
|
+
err_message = (
|
|
324
|
+
data.get("message")
|
|
325
|
+
or data.get("error")
|
|
326
|
+
or data.get("detail")
|
|
327
|
+
)
|
|
328
|
+
if isinstance(err_message, dict):
|
|
329
|
+
err_message = err_message.get("message") or str(err_message)
|
|
330
|
+
return {
|
|
331
|
+
"success": False,
|
|
332
|
+
"status_code": status,
|
|
333
|
+
"error_message": err_message or f"Non-200 from Exa: {status}",
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if isinstance(data, dict):
|
|
337
|
+
if "error" in data:
|
|
338
|
+
error_value = data["error"]
|
|
339
|
+
if isinstance(error_value, dict):
|
|
340
|
+
error_value = error_value.get("message") or str(error_value)
|
|
341
|
+
return {
|
|
342
|
+
"success": False,
|
|
343
|
+
"status_code": status,
|
|
344
|
+
"error_message": str(error_value),
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
results = data.get("results")
|
|
348
|
+
if isinstance(results, list):
|
|
349
|
+
return {"success": True, "status_code": status, "error_message": None}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
"success": False,
|
|
353
|
+
"status_code": status,
|
|
354
|
+
"error_message": "Unexpected response from Exa API.",
|
|
355
|
+
}
|
|
356
|
+
except Exception as exc:
|
|
357
|
+
logger.error(f"Exa test failed: {exc}")
|
|
358
|
+
return {"success": False, "status_code": 0, "error_message": str(exc)}
|
|
359
|
+
|
|
360
|
+
|
|
299
361
|
async def test_apollo(api_key: str) -> Dict[str, Any]:
|
|
300
362
|
organization_domain = 'microsoft.com'
|
|
301
363
|
url = f'https://api.apollo.io/api/v1/organizations/enrich?domain={organization_domain}'
|
|
@@ -1179,6 +1241,7 @@ async def test_connectivity(tool_config: List[Dict[str, Any]]) -> Dict[str, Dict
|
|
|
1179
1241
|
"serpapi": test_serpapi,
|
|
1180
1242
|
"serperdev": test_serperdev,
|
|
1181
1243
|
"proxycurl": test_proxycurl,
|
|
1244
|
+
"exa": test_exa,
|
|
1182
1245
|
"apollo": test_apollo,
|
|
1183
1246
|
"hubspot": test_hubspot,
|
|
1184
1247
|
"github": test_github,
|
|
@@ -103,6 +103,7 @@ src/dhisana/workflow/task.py
|
|
|
103
103
|
src/dhisana/workflow/test.py
|
|
104
104
|
tests/test_agent_tools.py
|
|
105
105
|
tests/test_apollo_company_search.py
|
|
106
|
+
tests/test_apollo_lead_search.py
|
|
106
107
|
tests/test_connectivity.py
|
|
107
108
|
tests/test_google_document.py
|
|
108
109
|
tests/test_hubspot_call_logs.py
|
|
@@ -116,7 +116,7 @@ class TestApolloCompanySearch(unittest.TestCase):
|
|
|
116
116
|
@patch('src.dhisana.utils.apollo_tools.get_apollo_access_token')
|
|
117
117
|
async def test_search_companies_with_apollo_page(self, mock_get_token, mock_fetch_data):
|
|
118
118
|
"""Test the paginated company search function."""
|
|
119
|
-
mock_get_token.return_value = "test_api_key"
|
|
119
|
+
mock_get_token.return_value = ("test_api_key", False)
|
|
120
120
|
mock_fetch_data.return_value = self.mock_apollo_response
|
|
121
121
|
|
|
122
122
|
query = CompanyQueryFilters(
|
|
@@ -189,7 +189,7 @@ class TestApolloCompanySearch(unittest.TestCase):
|
|
|
189
189
|
@patch('src.dhisana.utils.apollo_tools.get_apollo_access_token')
|
|
190
190
|
async def test_search_companies_with_apollo_basic(self, mock_get_token, mock_fetch_data):
|
|
191
191
|
"""Test the basic company search function."""
|
|
192
|
-
mock_get_token.return_value = "test_api_key"
|
|
192
|
+
mock_get_token.return_value = ("test_api_key", False)
|
|
193
193
|
mock_fetch_data.return_value = self.mock_apollo_response
|
|
194
194
|
|
|
195
195
|
payload = {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import AsyncMock, patch
|
|
3
|
+
|
|
4
|
+
from src.dhisana.schemas.sales import LeadsQueryFilters
|
|
5
|
+
from src.dhisana.utils.apollo_tools import search_leads_with_apollo_page
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def apollo_people_response():
|
|
10
|
+
return {
|
|
11
|
+
"pagination": {
|
|
12
|
+
"page": 1,
|
|
13
|
+
"per_page": 25,
|
|
14
|
+
"total_entries": 1,
|
|
15
|
+
"total_pages": 1,
|
|
16
|
+
},
|
|
17
|
+
"people": [
|
|
18
|
+
{
|
|
19
|
+
"id": "person_1",
|
|
20
|
+
"name": "Alex Example",
|
|
21
|
+
"first_name": "Alex",
|
|
22
|
+
"last_name": "Example",
|
|
23
|
+
"title": "VP Sales",
|
|
24
|
+
"headline": "Revenue leader",
|
|
25
|
+
"email": "alex@example.com",
|
|
26
|
+
"linkedin_url": "https://linkedin.com/in/alex-example",
|
|
27
|
+
"city": "San Francisco",
|
|
28
|
+
"state": "California",
|
|
29
|
+
"organization": {
|
|
30
|
+
"name": "Example Inc",
|
|
31
|
+
"primary_domain": "example.com",
|
|
32
|
+
"linkedin_url": "https://linkedin.com/company/example",
|
|
33
|
+
"website_url": "https://example.com",
|
|
34
|
+
"keywords": ["SaaS", "Sales"],
|
|
35
|
+
},
|
|
36
|
+
"contact": {"sanitized_phone": "+1-555-0100"},
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"contacts": [],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def tool_config():
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
"name": "apollo",
|
|
48
|
+
"configuration": [
|
|
49
|
+
{"name": "apiKey", "value": "test_api_key"},
|
|
50
|
+
],
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.asyncio
|
|
56
|
+
async def test_search_leads_with_apollo_page_parses_url_filters(apollo_people_response, tool_config):
|
|
57
|
+
example_url = (
|
|
58
|
+
"https://app.apollo.io/#/people?page=1&personTitles[]=VP%20Sales&"
|
|
59
|
+
"organizationLatestFundingStageCd[]=3&organizationLatestFundingStageCd[]=4&"
|
|
60
|
+
"organizationLatestFundingStageCd[]=5&qOrganizationJobTitles[]=SDR&"
|
|
61
|
+
"qOrganizationJobTitles[]=account%20executive"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
with patch(
|
|
65
|
+
"src.dhisana.utils.apollo_tools.fetch_apollo_data",
|
|
66
|
+
new=AsyncMock(return_value=apollo_people_response),
|
|
67
|
+
) as mock_fetch_data, patch(
|
|
68
|
+
"src.dhisana.utils.apollo_tools.get_apollo_access_token",
|
|
69
|
+
return_value="test_api_key",
|
|
70
|
+
):
|
|
71
|
+
result = await search_leads_with_apollo_page(
|
|
72
|
+
query=LeadsQueryFilters(),
|
|
73
|
+
page=1,
|
|
74
|
+
per_page=25,
|
|
75
|
+
example_url=example_url,
|
|
76
|
+
tool_config=tool_config,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
assert result["current_page"] == 1
|
|
80
|
+
assert result["total_entries"] == 1
|
|
81
|
+
assert len(result["results"]) == 1
|
|
82
|
+
|
|
83
|
+
payload = mock_fetch_data.call_args[0][3]
|
|
84
|
+
assert payload["q_organization_job_titles"] == ["SDR", "account executive"]
|
|
85
|
+
assert payload["organization_latest_funding_stage_cd"] == ["3", "4", "5"]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_search_leads_with_apollo_page_uses_query_filters(apollo_people_response, tool_config):
|
|
90
|
+
query = LeadsQueryFilters(
|
|
91
|
+
person_current_titles=["Head of Sales"],
|
|
92
|
+
job_openings_with_titles=["SDR", "Account Executive"],
|
|
93
|
+
latest_funding_stages=["3", "4"],
|
|
94
|
+
organization_num_employees_ranges=["201,500"],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
with patch(
|
|
98
|
+
"src.dhisana.utils.apollo_tools.fetch_apollo_data",
|
|
99
|
+
new=AsyncMock(return_value=apollo_people_response),
|
|
100
|
+
) as mock_fetch_data, patch(
|
|
101
|
+
"src.dhisana.utils.apollo_tools.get_apollo_access_token",
|
|
102
|
+
return_value="test_api_key",
|
|
103
|
+
):
|
|
104
|
+
result = await search_leads_with_apollo_page(
|
|
105
|
+
query=query,
|
|
106
|
+
page=2,
|
|
107
|
+
per_page=50,
|
|
108
|
+
tool_config=tool_config,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
assert result["current_page"] == 1
|
|
112
|
+
assert len(result["results"]) == 1
|
|
113
|
+
|
|
114
|
+
payload = mock_fetch_data.call_args[0][3]
|
|
115
|
+
assert payload["page"] == 2
|
|
116
|
+
assert payload["per_page"] == 50
|
|
117
|
+
assert payload["person_titles"] == ["Head of Sales"]
|
|
118
|
+
assert payload["q_organization_job_titles"] == ["SDR", "Account Executive"]
|
|
119
|
+
assert payload["organization_latest_funding_stage_cd"] == ["3", "4"]
|
|
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.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/check_email_validity_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/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.dev219 → dhisana-0.0.1.dev221}/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.dev219 → dhisana-0.0.1.dev221}/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.dev219 → dhisana-0.0.1.dev221}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/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.dev219 → dhisana-0.0.1.dev221}/src/dhisana/utils/openapi_tool/openapi_tool.py
RENAMED
|
File without changes
|
{dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/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
|
|
File without changes
|
{dhisana-0.0.1.dev219 → dhisana-0.0.1.dev221}/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.dev219 → dhisana-0.0.1.dev221}/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
|