Skip to content

Commit

Permalink
[Freshsales] Refactoring/Improvements (#32)
Browse files Browse the repository at this point in the history
* Freshsales improvements and refactoring

* add search_limit to readme

* PR review
  • Loading branch information
tianjing-li authored Jan 11, 2024
1 parent 8f864f2 commit e307b03
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 124 deletions.
1 change: 1 addition & 0 deletions freshsales/.env-template
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ FRESHSALES_USER_ENTITY_ENABLED=True
FRESHSALES_CONTACT_ENTITY_ENABLED=True
FRESHSALES_SALES_ACCOUNT_ENTITY_ENABLED=True
FRESHSALES_DEAL_ENTITY_ENABLED=True
FRESHSALES_SEARCH_LIMIT=
FRESHSALES_CONNECTOR_API_KEY=
4 changes: 3 additions & 1 deletion freshsales/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Freshsales Connector
# Freshsales Quick Start Connector

Connects Cohere to Freshsales, the sales CRM tool.

Expand All @@ -16,6 +16,8 @@ There are additional environment variables that define which entities are enable
Freshsales allows searching Users, Contacts, Deals, and Sales Accounts, by default the `.env-template` file has
these values enabled.

Optionally, the `FRESHSALES_SEARCH_LIMIT` variable can be set to set the maximum number of search results returned. By default, this is set to 15.

Finally, to protect this connector from abuse, the `FRESHSALES_CONNECTOR_API_KEY` environment variable must be set to a secure value that will be used for this connector's own bearer token authentication.

## Development
Expand Down
114 changes: 114 additions & 0 deletions freshsales/provider/client.py
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
4 changes: 0 additions & 4 deletions freshsales/provider/constants.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import os
from .enums import EntityChoices

BASE_PATH = f"https://{os.environ.get('FRESHSALES_BUNDLE_ALIAS', '')}/api"
API_TOKEN = os.environ.get("FRESHSALES_API_KEY")
RESULTS_LIMIT = 15
ENTITY_ENV_VAR_ENABLED_MAPPING = {
EntityChoices.USER: "USER_ENTITY_ENABLED",
EntityChoices.CONTACT: "CONTACT_ENTITY_ENABLED",
Expand Down
142 changes: 23 additions & 119 deletions freshsales/provider/provider.py
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

0 comments on commit e307b03

Please sign in to comment.