From bdb61ccd2f49b3503b8e8c5f69e7f5a3dfc4c5be Mon Sep 17 00:00:00 2001 From: Tianjing Li Date: Wed, 10 Jan 2024 11:18:13 -0500 Subject: [PATCH 1/3] Freshsales improvements and refactoring --- freshsales/.env-template | 1 + freshsales/provider/client.py | 122 ++++++++++++++++++++++++++ freshsales/provider/constants.py | 4 - freshsales/provider/provider.py | 142 +++++-------------------------- 4 files changed, 146 insertions(+), 123 deletions(-) create mode 100644 freshsales/provider/client.py diff --git a/freshsales/.env-template b/freshsales/.env-template index 5a21ef956..bdda026e6 100644 --- a/freshsales/.env-template +++ b/freshsales/.env-template @@ -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= \ No newline at end of file diff --git a/freshsales/provider/client.py b/freshsales/provider/client.py new file mode 100644 index 000000000..dc85d0e46 --- /dev/null +++ b/freshsales/provider/client.py @@ -0,0 +1,122 @@ +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 _request(self, method, url, headers={}, params={}, raise_on_error=False): + response = requests.request( + method, + url, + headers=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._request( + "GET", + search_url, + self.headers, + params, + True, + ) + + def get_contact_details(self, id): + url = f"{self.base_url}/contacts/{id}" + params = {"include": ",".join(CONTACT_PARAMETERS)} + + return self._request( + "GET", + url, + self.headers, + params, + ) + + def get_sales_account_details(self, id): + url = f"{self.base_url}/sales_accounts/{id}" + params = {"include": ",".join(SALES_ACCOUNT_PARAMETERS)} + + return self._request( + "GET", + url, + self.headers, + params, + ) + + def get_deal_details(self, id): + url = f"{self.base_url}/deals/{id}" + params = {"include": ",".join(DEAL_PARAMETERS)} + + return self._request( + "GET", + url, + self.headers, + 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 diff --git a/freshsales/provider/constants.py b/freshsales/provider/constants.py index feb4390bf..3780325c5 100644 --- a/freshsales/provider/constants.py +++ b/freshsales/provider/constants.py @@ -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", diff --git a/freshsales/provider/provider.py b/freshsales/provider/provider.py index 071468558..2323d0c92 100644 --- a/freshsales/provider/provider.py +++ b/freshsales/provider/provider.py @@ -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 From ad517ac7f611ca935a7bcc44655fa25adf77185d Mon Sep 17 00:00:00 2001 From: Tianjing Li Date: Wed, 10 Jan 2024 11:20:32 -0500 Subject: [PATCH 2/3] add search_limit to readme --- freshsales/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freshsales/README.md b/freshsales/README.md index 35df60f14..1a5bacbdb 100644 --- a/freshsales/README.md +++ b/freshsales/README.md @@ -1,4 +1,4 @@ -# Freshsales Connector +# Freshsales Quick StartConnector Connects Cohere to Freshsales, the sales CRM tool. @@ -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 From 542792fca400328ebee869398b097e32f033045f Mon Sep 17 00:00:00 2001 From: Tianjing Li Date: Thu, 11 Jan 2024 11:58:40 -0500 Subject: [PATCH 3/3] PR review --- freshsales/README.md | 2 +- freshsales/provider/client.py | 22 +++++++--------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/freshsales/README.md b/freshsales/README.md index 1a5bacbdb..f1fbb5c2d 100644 --- a/freshsales/README.md +++ b/freshsales/README.md @@ -1,4 +1,4 @@ -# Freshsales Quick StartConnector +# Freshsales Quick Start Connector Connects Cohere to Freshsales, the sales CRM tool. diff --git a/freshsales/provider/client.py b/freshsales/provider/client.py index dc85d0e46..a1211c9f3 100644 --- a/freshsales/provider/client.py +++ b/freshsales/provider/client.py @@ -41,11 +41,11 @@ def is_env_var_true(env_var_name): ] return ",".join(entities) - def _request(self, method, url, headers={}, params={}, raise_on_error=False): + def _get(self, url, params={}, raise_on_error=False): response = requests.request( - method, + "GET", url, - headers=headers, + headers=self.headers, params=params, ) @@ -66,10 +66,8 @@ def search(self, query): "per_page": self.search_limit, } - return self._request( - "GET", + return self._get( search_url, - self.headers, params, True, ) @@ -78,10 +76,8 @@ def get_contact_details(self, id): url = f"{self.base_url}/contacts/{id}" params = {"include": ",".join(CONTACT_PARAMETERS)} - return self._request( - "GET", + return self._get( url, - self.headers, params, ) @@ -89,10 +85,8 @@ def get_sales_account_details(self, id): url = f"{self.base_url}/sales_accounts/{id}" params = {"include": ",".join(SALES_ACCOUNT_PARAMETERS)} - return self._request( - "GET", + return self._get( url, - self.headers, params, ) @@ -100,10 +94,8 @@ def get_deal_details(self, id): url = f"{self.base_url}/deals/{id}" params = {"include": ",".join(DEAL_PARAMETERS)} - return self._request( - "GET", + return self._get( url, - self.headers, params, )