-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Freshsales] Refactoring/Improvements (#32)
* Freshsales improvements and refactoring * add search_limit to readme * PR review
- Loading branch information
1 parent
8f864f2
commit e307b03
Showing
5 changed files
with
141 additions
and
124 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import requests | ||
from flask import current_app as app | ||
|
||
from . import UpstreamProviderError | ||
from .constants import ( | ||
ENTITY_ENV_VAR_ENABLED_MAPPING, | ||
CONTACT_PARAMETERS, | ||
SALES_ACCOUNT_PARAMETERS, | ||
DEAL_PARAMETERS, | ||
) | ||
|
||
client = None | ||
|
||
|
||
class FreshsalesClient: | ||
def __init__(self, base_path, api_key, search_limit): | ||
self.base_url = f"https://{base_path}/api" | ||
self.headers = { | ||
"Authorization": f"Token token={api_key}", | ||
} | ||
self.search_limit = search_limit | ||
self.entity_types = self.build_entity_types() | ||
|
||
def build_entity_types(self): | ||
""" | ||
Builds the entity list string for the `includes` query parameter, based | ||
off environment variables. | ||
For example, the default returned format will look like 'user,contact,sales_account,deal' | ||
See: https://developers.freshworks.com/crm/api/#search | ||
""" | ||
|
||
def is_env_var_true(env_var_name): | ||
value = app.config.get(env_var_name, "False") | ||
return value.lower() == "true" | ||
|
||
entities = [ | ||
enum.value | ||
for enum, env_var in ENTITY_ENV_VAR_ENABLED_MAPPING.items() | ||
if is_env_var_true(env_var) | ||
] | ||
return ",".join(entities) | ||
|
||
def _get(self, url, params={}, raise_on_error=False): | ||
response = requests.request( | ||
"GET", | ||
url, | ||
headers=self.headers, | ||
params=params, | ||
) | ||
|
||
if response.status_code != 200: | ||
if raise_on_error: | ||
message = response.text or f"Error: HTTP {response.status_code}" | ||
raise UpstreamProviderError(message) | ||
|
||
return None | ||
|
||
return response.json() | ||
|
||
def search(self, query): | ||
search_url = f"{self.base_url}/search" | ||
params = { | ||
"q": query, | ||
"include": self.entity_types, | ||
"per_page": self.search_limit, | ||
} | ||
|
||
return self._get( | ||
search_url, | ||
params, | ||
True, | ||
) | ||
|
||
def get_contact_details(self, id): | ||
url = f"{self.base_url}/contacts/{id}" | ||
params = {"include": ",".join(CONTACT_PARAMETERS)} | ||
|
||
return self._get( | ||
url, | ||
params, | ||
) | ||
|
||
def get_sales_account_details(self, id): | ||
url = f"{self.base_url}/sales_accounts/{id}" | ||
params = {"include": ",".join(SALES_ACCOUNT_PARAMETERS)} | ||
|
||
return self._get( | ||
url, | ||
params, | ||
) | ||
|
||
def get_deal_details(self, id): | ||
url = f"{self.base_url}/deals/{id}" | ||
params = {"include": ",".join(DEAL_PARAMETERS)} | ||
|
||
return self._get( | ||
url, | ||
params, | ||
) | ||
|
||
|
||
def get_client(): | ||
global client | ||
if not client: | ||
assert ( | ||
base_path := app.config.get("BUNDLE_ALIAS") | ||
), "FRESHSALES_BUNDLE_ALIAS must be set" | ||
assert (api_key := app.config.get("API_KEY")), "FRESHSALES_API_KEY must be set" | ||
search_limit = app.config.get("SEARCH_LIMIT", 15) | ||
|
||
client = FreshsalesClient(base_path, api_key, search_limit) | ||
|
||
return client |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,141 +1,45 @@ | ||
import logging | ||
import os | ||
from typing import Any | ||
|
||
import requests | ||
from flask import current_app as app | ||
|
||
from . import UpstreamProviderError | ||
from .enums import EntityChoices | ||
from .constants import ( | ||
BASE_PATH, | ||
API_TOKEN, | ||
RESULTS_LIMIT, | ||
ENTITY_ENV_VAR_ENABLED_MAPPING, | ||
CONTACT_PARAMETERS, | ||
SALES_ACCOUNT_PARAMETERS, | ||
DEAL_PARAMETERS, | ||
) | ||
from .client import get_client | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
""" | ||
Builds the entity list string for the `includes` query parameter, based | ||
off environment variables. | ||
For example, the default returned format will look like 'user,contact,sales_account,deal' | ||
See: https://developers.freshworks.com/crm/api/#search | ||
""" | ||
|
||
|
||
def get_entity_types() -> str: | ||
def is_env_var_true(env_var_name): | ||
value = app.config.get(env_var_name, "False") | ||
return value.lower() == "true" | ||
|
||
entities = [ | ||
enum.value | ||
for enum, env_var in ENTITY_ENV_VAR_ENABLED_MAPPING.items() | ||
if is_env_var_true(env_var) | ||
] | ||
return ",".join(entities) | ||
|
||
|
||
def get_contact_details(id): | ||
url = f"{BASE_PATH}/contacts/{id}" | ||
params = {"include": ",".join(CONTACT_PARAMETERS)} | ||
headers = { | ||
"Authorization": f"Token token={API_TOKEN}", | ||
} | ||
response = requests.get( | ||
url, | ||
headers=headers, | ||
params=params, | ||
) | ||
|
||
if response.status_code != 200: | ||
return None | ||
|
||
return response.json() | ||
|
||
|
||
def get_sales_account_details(id): | ||
url = f"{BASE_PATH}/sales_accounts/{id}" | ||
params = {"include": ",".join(SALES_ACCOUNT_PARAMETERS)} | ||
headers = { | ||
"Authorization": f"Token token={API_TOKEN}", | ||
} | ||
response = requests.get( | ||
url, | ||
headers=headers, | ||
params=params, | ||
) | ||
|
||
if response.status_code != 200: | ||
return None | ||
|
||
return response.json() | ||
|
||
|
||
def get_deal_details(id): | ||
url = f"{BASE_PATH}/deals/{id}" | ||
params = {"include": ",".join(DEAL_PARAMETERS)} | ||
headers = { | ||
"Authorization": f"Token token={API_TOKEN}", | ||
} | ||
response = requests.get( | ||
url, | ||
headers=headers, | ||
params=params, | ||
) | ||
|
||
if response.status_code != 200: | ||
return None | ||
|
||
return response.json() | ||
|
||
|
||
def search(query) -> list[dict[str, Any]]: | ||
assert API_TOKEN, "FRESHSALES_API_KEY must be set" | ||
|
||
url = f"{BASE_PATH}/search" | ||
entities = get_entity_types() | ||
|
||
params = { | ||
"q": query, | ||
"include": entities, | ||
"per_page": RESULTS_LIMIT, | ||
} | ||
headers = { | ||
"Authorization": f"Token token={API_TOKEN}", | ||
} | ||
response = requests.get( | ||
url, | ||
headers=headers, | ||
params=params, | ||
) | ||
|
||
if response.status_code != 200: | ||
message = response.text or f"Error: HTTP {response.status_code}" | ||
raise UpstreamProviderError(message) | ||
freshsales_client = get_client() | ||
search_results = freshsales_client.search(query) | ||
|
||
results = [] | ||
# Contact, Sales Account and Deals can be fetched with extra details | ||
for entry in response.json(): | ||
for entry in search_results: | ||
type = entry["type"] | ||
decorated_entry = None | ||
if type == EntityChoices.CONTACT.value: | ||
decorated_entry = get_contact_details(entry["id"]) | ||
decorated_entry = freshsales_client.get_contact_details(entry["id"]) | ||
elif type == EntityChoices.SALES_ACCOUNT.value: | ||
decorated_entry = get_sales_account_details(entry["id"]) | ||
decorated_entry = freshsales_client.get_sales_account_details(entry["id"]) | ||
elif type == EntityChoices.DEAL.value: | ||
decorated_entry = get_deal_details(entry["id"]) | ||
decorated_entry = freshsales_client.get_deal_details(entry["id"]) | ||
|
||
if decorated_entry is not None: | ||
results.append(decorated_entry) | ||
else: | ||
results.append(entry) | ||
entry = decorated_entry | ||
|
||
results.append(serialize_result(entry)) | ||
|
||
return results | ||
|
||
|
||
def serialize_result(entry): | ||
serialized_result = {} | ||
|
||
for key, value in entry.items(): | ||
serialized_result[key] = ( | ||
str(value) | ||
if not isinstance(value, list) | ||
else ", ".join(str(vl) for vl in value) | ||
) | ||
|
||
return serialized_result |