From ef4da7b26cd0d8e508bd8e2773050a0dcfdd9289 Mon Sep 17 00:00:00 2001 From: Eric Bergerson Date: Wed, 20 Jan 2021 15:02:58 -0500 Subject: [PATCH 1/4] Structure basic additions to the package to support OAuth --- demo/__init__.py | 5 + demo/auth_demo/__init__.py | 6 + demo/auth_demo/oauth_demo.py | 180 ++++++++++++++++++++++++++++ demo/auth_demo/pardot_demo.ini | 31 +++++ pypardot/auth_handler.py | 89 ++++++++++++++ pypardot/auth_pardot_api.py | 95 +++++++++++++++ pypardot/client.py | 48 ++++---- pypardot/objects/emailtemplates.py | 2 +- pypardot/objects/listmemberships.py | 2 +- pypardot/objects/prospects.py | 6 +- 10 files changed, 435 insertions(+), 29 deletions(-) create mode 100644 demo/__init__.py create mode 100644 demo/auth_demo/__init__.py create mode 100644 demo/auth_demo/oauth_demo.py create mode 100644 demo/auth_demo/pardot_demo.ini create mode 100644 pypardot/auth_handler.py create mode 100644 pypardot/auth_pardot_api.py diff --git a/demo/__init__.py b/demo/__init__.py new file mode 100644 index 0000000..65afeef --- /dev/null +++ b/demo/__init__.py @@ -0,0 +1,5 @@ +''' + +''' +__author__ = 'eb' + diff --git a/demo/auth_demo/__init__.py b/demo/auth_demo/__init__.py new file mode 100644 index 0000000..120a4d1 --- /dev/null +++ b/demo/auth_demo/__init__.py @@ -0,0 +1,6 @@ +''' + +''' +__author__ = 'eb' + + diff --git a/demo/auth_demo/oauth_demo.py b/demo/auth_demo/oauth_demo.py new file mode 100644 index 0000000..ee997ff --- /dev/null +++ b/demo/auth_demo/oauth_demo.py @@ -0,0 +1,180 @@ +''' +A demonstration of connecting to Pardot using both +a pardot-only user and the existing api and +using a sf user utilizing the expanding api. + +This demonstration requires a configuration file +called `pardot_demo.ini` to supply all of the +instance specific authentication data needed to +run this code in a specific environment. +The format of the file should match the +example `pardot_demo.ini` file provided +in this package. The file should +be placed in the users home directory, +purposefully outside this repository. +''' +__author__ = 'eb' + +import logging +from logging import Logger +from pathlib import Path +from typing import Tuple, Dict, List, cast +from configparser import ConfigParser + +import requests + +from auth_pardot_api import AuthPardotAPI +from client import PardotAPI +from auth_handler import T, TraditionalAuthHandler, OAuthHandler + + +class PardotAuthenticationDemo(object): + + def __init__(self, config_file: Path, logger: Logger = None) -> None: + super().__init__() + self.logger = logger + self.parser = ConfigParser() + self.parser.read(config_file) + + def run(self): + # Demonstrate raw oauth process + self.access_pardot_via_oauth_using_raw_requests() + + # Demonstrate accessing pardot the traditional way using + # a Pardot-Only user via the existing PyPardot4 api + self.access_pardot_via_traditional_api() + + # Demonstrate accessing pardot using + # a salesforce user and OAuth2 + self.access_pardot_via_oauth_api() + + + def access_pardot_via_oauth_using_raw_requests(self): + """ + Demonstrate the low level request formation needed to retrieve an access token + from Salesforce and how to use it to access Pardot. + """ + self.logger and self.logger.info("\tAccess Pardot via SF SSO Using Raw Requests") + access_token, bus_unit_id = self.retrieve_access_token() + prospects = self.send_pardot_request(access_token, bus_unit_id) + self.logger and self.logger.info("\t\t...Success") + + def access_pardot_via_traditional_api(self): + """ + Use the existing PardotAPI to fetch prospect data via a pardot-only user. + """ + self.logger and self.logger.info("\tAccess Pardot via Pardot-Only User") + auth_handler = self.get_auth_handler("pardot") + pd = PardotAPI(auth_handler.username, auth_handler.password, auth_handler.userkey) + self.query_pardot_api(pd, "pardot") + + self.logger and self.logger.info("\tAccess Pardot Sandbox via Pardot-Only User") + self.query_pardot_api(pd, "pardot_sandbox") + self.logger and self.logger.info("\t\t...Success") + + def access_pardot_via_oauth_api(self): + """ + Use the AuthPardotAPI to fetch prospect data via OAuth2 using a SSO user from Salesforce + """ + + # Accessing a production pardot server using credentials from a production salesforce instance + self.logger and self.logger.info("\tAccess Pardot via SF SSO") + auth_handler = self.get_auth_handler("salesforce") + pd = AuthPardotAPI(auth_handler, logger=self.logger) + self.query_pardot_api(pd, "salesforce") + + # Accessing a pardot sandbox server using credentials from a salesforce sandbox instance + # Commented out because I can't test this, we don't have a pardot sandbox + auth_handler = self.get_auth_handler("salesforce") + pd = AuthPardotAPI(auth_handler, logger=self.logger) + self.query_pardot_api(pd, "salesforce_sandbox") + self.logger and self.logger.info("\t\t...Success") + + def query_pardot_api(self, pd: PardotAPI, section: str) -> Tuple[Dict, List]: + """ + Use a pardot api to fetch prospect data + The read_by_email() utilizes an http post while the query() utilizes an http get(). + """ + prospect_email = self.parser.get(section, "prospect_email") + response = pd.prospects.read_by_email(email="eb@object.com") + prospect = response["prospect"] + self.logger and self.logger.debug( + f"\t\tFound Prospect in {section}: {prospect['first_name']} {prospect['last_name']} for email {prospect['email']}") + + prospect_date_filter = self.parser.get(section, "prospect_date_filter", fallback="2021-01-01") + response = pd.prospects.query(created_after=prospect_date_filter) + prospects = response["prospect"] + self.logger and self.logger.debug(f"\t\tFound Prospects in {section} created after {prospect_date_filter}:") + for p in prospects: + self.logger and self.logger.debug(f"\t\t\t{p['first_name']} {p['last_name']} <{p['email']}>") + + return prospect, prospects + + def retrieve_access_token(self): + self.logger and self.logger.debug("\t\tAuthenticate Pardot with with OAuth2 parameters from 'salesforce'") + auth_handler = self.get_auth_handler("salesforce") + params = { + "grant_type": "password", + "client_id": auth_handler.consumer_key, + "client_secret": auth_handler.consumer_secret, + "username": auth_handler.username, + "password": auth_handler.password + auth_handler.token + } + url = "https://login.salesforce.com/services/oauth2/token" + r = requests.post(url, params=params) + access_token = r.json().get("access_token") + instance_url = r.json().get("instance_url") + self.logger and self.logger.debug( + f"\t\tRetrieved oauth access_token for {instance_url}") + + return access_token, auth_handler.business_unit_id + + def send_pardot_request(self, access_token, bus_unit_id) -> List: + params = {"format": "json", + "id_less_than": 5427630} + headers = {"Authorization": f"Bearer {access_token}", + "Pardot-Business-Unit-Id": bus_unit_id, + "Content-Type": "application/x-www-form-urlencoded" + } + url = "https://pi.pardot.com/api/prospect/version/4/do/query" + response = requests.post(url, + params=params, + headers=headers).json() + if '@attributes' not in response or 'stat' not in response['@attributes']: + raise ValueError(f"Pardot Request Failure: Corrupted Response") + if response['@attributes']['stat'] != "ok": + raise ValueError(f"Pardot Request Failure: {response['@attributes']['stat']}") + + prospects = response["result"]["prospect"] + self.logger and self.logger.debug(f"\t\tFound prospects in Production with ids less than 5427630:") + for prospect in prospects: + self.logger and self.logger.debug( + f"\t\t\t{prospect['first_name']} {prospect['last_name']} <{prospect['email']}>") + return prospects + + def get_auth_handler(self, section_name: str) -> T: + if section_name.startswith("pardot"): + return TraditionalAuthHandler(self.parser.get(section_name, "username"), + self.parser.get(section_name, "password"), + self.parser.get(section_name, "userkey"), + logger=self.logger) + elif section_name.startswith("salesforce"): + return OAuthHandler(self.parser.get(section_name, "user"), + self.parser.get(section_name, "password"), + self.parser.get(section_name, "consumer_key"), + self.parser.get(section_name, "consumer_secret"), + self.parser.get(section_name, "business_unit_id"), + token=self.parser.get(section_name, "token"), + logger=self.logger) + +if __name__ == '__main__': + logger = logging.getLogger("OAUTH_DEMO") + logger.setLevel(logging.DEBUG) + ch = logging.StreamHandler() + formatter = logging.Formatter('[{levelname:>8s}] {asctime} {name:s}: {message:s}', style='{') + ch.setFormatter(formatter) + ch.setLevel(logging.NOTSET) + logger.addHandler(ch) + config_file = Path("~/oauth_demo.ini").expanduser() + demo = PardotAuthenticationDemo(config_file, logger=logger) + demo.run() diff --git a/demo/auth_demo/pardot_demo.ini b/demo/auth_demo/pardot_demo.ini new file mode 100644 index 0000000..53305ca --- /dev/null +++ b/demo/auth_demo/pardot_demo.ini @@ -0,0 +1,31 @@ +############################### +[pardot] +username= +password= +userkeyn= +prospect_email= +prospect_date_filter= + +[pardot_sandbox] +username= +password= +userkeyn= +prospect_email= +prospect_date_filter= + +[salesforce] +user= +password= +token= +consumer_key= +consumer_secret= +business_unit_id= + +[salesforce_sandbox] +user= +password= +token= +consumer_key= +consumer_secret= +business_unit_id= +############################### diff --git a/pypardot/auth_handler.py b/pypardot/auth_handler.py new file mode 100644 index 0000000..8c2cc95 --- /dev/null +++ b/pypardot/auth_handler.py @@ -0,0 +1,89 @@ +''' + +''' +__author__ = 'eb' + +from logging import Logger +from typing import Optional, Dict, TypeVar, Generic + +import requests + +T = TypeVar("T", bound="AuthHandler") + + +class AuthHandler(Generic[T]): + """Abstract base class for handling authentication""" + + def __init__(self, logger: Optional[Logger] = None) -> None: + super().__init__() + self.logger = logger + + def handle_authentication(self) -> bool: + raise NotImplementedError(f"handle_authentication() called on abstract base class {self.__class__}") + + def auth_header(self) -> Dict: + raise NotImplementedError(f"auth_header() called on abstract base class {self.__class__}") + + +class UserAuthHandler(AuthHandler[T]): + """Abstract base class for handling authentication that requires a username and password""" + + def __init__(self, username: str, password: str, **kwargs) -> None: + super().__init__(**kwargs) + self.username = username + self.password = password + + def handle_authentication(self) -> bool: + raise NotImplementedError( + f"handle_authentication() called on {self.__class__}. Should be handled by PardotAPI") + + def auth_header(self) -> Dict: + raise NotImplementedError( + f"auth_header() called on {self.__class__}. Should be handled by PardotAPI") + + +class TraditionalAuthHandler(UserAuthHandler[T]): + """Handles authentication of Pardot-only users""" + + def __init__(self, username: str, password: str, userkey: str, **kwargs) -> None: + super().__init__(username=username, password=password, **kwargs) + self.userkey = userkey + + +class OAuthHandler(UserAuthHandler[T]): + """ + Handles authentication of SSO Pardot users via OAuth2 when + dependent on username, password, consumer_key, consumer_secret and busines_unit_id. + """ + + def __init__(self, username: str, password: str, + consumer_key: str, consumer_secret: str, business_unit_id: str, + token: Optional[str] = None, is_sandbox: bool = False, **kwargs) -> None: + super().__init__(username=username, password=password, **kwargs) + self.consumer_key = consumer_key + self.consumer_secret = consumer_secret + self.business_unit_id: Optional[str] = business_unit_id + self.token = token + self.is_sandbox = is_sandbox + self.access_token: Optional[str] = None + + def handle_authentication(self) -> bool: + params = { + "grant_type": "password", + "client_id": self.consumer_key, + "client_secret": self.consumer_secret, + "username": self.username, + "password": (self.password + self.token) if self.token is not None else self.password + } + prefix = "test" if self.is_sandbox else "login" + url = f"https://{prefix}.salesforce.com/services/oauth2/token" + r = requests.post(url, params=params) + content = r.json() + self.access_token = content.get("access_token") + self.logger and self.logger.debug(f"Retrieved oauth access_token for {content.get('instance_url')}") + return True + + def auth_header(self) -> Dict: + return {"Authorization": f"Bearer {self.access_token}", + "Pardot-Business-Unit-Id": self.business_unit_id, + "Content-Type": "application/x-www-form-urlencoded"} diff --git a/pypardot/auth_pardot_api.py b/pypardot/auth_pardot_api.py new file mode 100644 index 0000000..f9b2eea --- /dev/null +++ b/pypardot/auth_pardot_api.py @@ -0,0 +1,95 @@ +''' +A demonstration of how to support OAuth2 +using Salesforce for authentication after 2/15/2021. + +This implementation uses a subclass of the standard PardotAPI class +to demonstrate a solution without any modifications to the +original PyPardot4 code. In this way a solution is suggested +without enforcing an architecture on the maintainers of the code. + +It would be expected that this code would be retrofitted into +the codebase for later releases that support OAuth2. +''' +__author__ = 'eb' + +from logging import Logger +from typing import Optional, cast + +import requests + +from auth_handler import AuthHandler, UserAuthHandler, OAuthHandler +from client import PardotAPI +from errors import PardotAPIError + + +class AuthPardotAPI(PardotAPI): + + def __init__(self, auth_handler: UserAuthHandler, version=4, logger: Optional[Logger] = None): + super().__init__(auth_handler.username, auth_handler.password, auth_handler.userkey, version) + self.auth_handler: Optional[AuthHandler] = auth_handler + self.logger = logger + + def use_username_authorization(self) -> bool: + return self.auth_handler is None + + def authenticate(self): + if self.use_username_authorization(): + self.logger and self.logger.debug(f"Authenticate Pardot with user {self.email}") + return super().authenticate() + else: + oauth_handler: OAuthHandler = cast(self.auth_handler, OAuthHandler) + self.logger and self.logger.debug(f"Authenticate Pardot with OAuth2 key {oauth_handler.consumer_key}") + success = self.auth_handler.handle_authentication() + return success + + def post(self, object_name, path=None, params=None, retries=0): + """ + Uses the default methodology if oauth is not required. + + Makes a POST request to the API. Checks for invalid requests that raise PardotAPIErrors. If the API key is + invalid, one re-authentication request is made, in case the key has simply expired. If no errors are raised, + returns either the JSON response, or if no JSON was returned, returns the HTTP response status code. + """ + if self.use_username_authorization(): + return super().post(object_name, path=path, params=params, retries=retries) + + params = {} if params is None else params + params.update({'format': 'json'}) + headers = self._build_auth_header() + try: + self._check_auth(object_name=object_name) + request = requests.post(self._full_path(object_name, self.version, path), data=params, headers=headers) + response = self._check_response(request) + return response + except PardotAPIError as err: + if err.message == 'access_token is invalid, unknown, or malformed': + # The handle_expired should work fine + response = self._handle_expired_api_key(err, retries, 'post', object_name, path, params) + return response + else: + raise err + + # Did not need to overload get because it uses _build_auth_header() + # already, so proper authorization is included in the request + # def get(self, object_name, path=None, params=None, retries=0): + # pass + + def _check_auth(self, object_name): + if self.use_username_authorization(): + return super()._check_auth(object_name) + + if object_name == 'login': + return + + oauth_handler: OAuthHandler = cast(self.auth_handler, OAuthHandler) + if oauth_handler.access_token is None: + self.authenticate() + + def _build_auth_header(self): + if self.use_username_authorization(): + return super()._build_auth_header() + + oauth_handler: OAuthHandler = cast(self.auth_handler, OAuthHandler) + if not oauth_handler.access_token: + raise Exception('Cannot build Authorization header. access_token or bus_unit_id is empty') + return oauth_handler.auth_header() diff --git a/pypardot/client.py b/pypardot/client.py index 0c4cc47..e7773b4 100644 --- a/pypardot/client.py +++ b/pypardot/client.py @@ -1,28 +1,28 @@ import requests -from .objects.accounts import Accounts -from .objects.customfields import CustomFields -from .objects.customredirects import CustomRedirects -from .objects.dynamiccontent import DynamicContent -from .objects.emailclicks import EmailClicks -from .objects.emailtemplates import EmailTemplates -from .objects.forms import Forms -from .objects.lifecyclehistories import LifecycleHistories -from .objects.lifecyclestages import LifecycleStages -from .objects.lists import Lists -from .objects.listmemberships import ListMemberships -from .objects.emails import Emails -from .objects.prospects import Prospects -from .objects.opportunities import Opportunities -from .objects.prospectaccounts import ProspectAccounts -from .objects.tags import Tags -from .objects.tagobjects import TagObjects -from .objects.users import Users -from .objects.visits import Visits -from .objects.visitors import Visitors -from .objects.visitoractivities import VisitorActivities -from .objects.campaigns import Campaigns +from objects.accounts import Accounts +from objects.customfields import CustomFields +from objects.customredirects import CustomRedirects +from objects.dynamiccontent import DynamicContent +from objects.emailclicks import EmailClicks +from objects.emailtemplates import EmailTemplates +from objects.forms import Forms +from objects.lifecyclehistories import LifecycleHistories +from objects.lifecyclestages import LifecycleStages +from objects.lists import Lists +from objects.listmemberships import ListMemberships +from objects.emails import Emails +from objects.prospects import Prospects +from objects.opportunities import Opportunities +from objects.prospectaccounts import ProspectAccounts +from objects.tags import Tags +from objects.tagobjects import TagObjects +from objects.users import Users +from objects.visits import Visits +from objects.visitors import Visitors +from objects.visitoractivities import VisitorActivities +from objects.campaigns import Campaigns -from .errors import PardotAPIError +from errors import PardotAPIError # Issue #1 (http://code.google.com/p/pybing/issues/detail?id=1) # Python 2.6 has json built in, 2.5 needs simplejson @@ -175,4 +175,4 @@ def _build_auth_header(self): if not self.user_key or not self.api_key: raise Exception('Cannot build Authorization header. user or api key is empty') auth_string = 'Pardot api_key=%s, user_key=%s' % (self.api_key, self.user_key) - return {'Authorization': auth_string} \ No newline at end of file + return {'Authorization': auth_string} diff --git a/pypardot/objects/emailtemplates.py b/pypardot/objects/emailtemplates.py index 49fc624..adcb2a3 100644 --- a/pypardot/objects/emailtemplates.py +++ b/pypardot/objects/emailtemplates.py @@ -1,4 +1,4 @@ -from ..errors import PardotAPIArgumentError +from errors import PardotAPIArgumentError class EmailTemplates(object): diff --git a/pypardot/objects/listmemberships.py b/pypardot/objects/listmemberships.py index 0d47411..08c8638 100644 --- a/pypardot/objects/listmemberships.py +++ b/pypardot/objects/listmemberships.py @@ -1,4 +1,4 @@ -from ..errors import PardotAPIArgumentError +from errors import PardotAPIArgumentError class ListMemberships(object): diff --git a/pypardot/objects/prospects.py b/pypardot/objects/prospects.py index 6251e3c..77e6115 100644 --- a/pypardot/objects/prospects.py +++ b/pypardot/objects/prospects.py @@ -1,4 +1,4 @@ -from ..errors import PardotAPIArgumentError +from errors import PardotAPIArgumentError class Prospects(object): @@ -94,7 +94,7 @@ def read_by_id(self, id=None, **kwargs): raise PardotAPIArgumentError('id is required to read a prospect.') response = self._post(path='/do/read/id/{id}'.format(id=id), params=kwargs) return response - + def read_by_fid(self, fid=None, **kwargs): """ Returns data for the prospect specified by . must be a valid CRM FID. @@ -227,4 +227,4 @@ def _post(self, object_name='prospect', path=None, params=None): if params is None: params = {} response = self.client.post(object_name=object_name, path=path, params=params) - return response \ No newline at end of file + return response From 9e37a040faa5b8b9ddc0e7cbb9205539c32a4aa5 Mon Sep 17 00:00:00 2001 From: Eric Bergerson Date: Thu, 21 Jan 2021 11:21:32 -0500 Subject: [PATCH 2/4] All functionality seems to be working. --- demo/auth_demo/oauth_demo.py | 104 +++++++++++++++++++++------------ demo/auth_demo/pardot_demo.ini | 6 +- pypardot/auth_handler.py | 30 +++++++++- pypardot/auth_pardot_api.py | 21 +++---- 4 files changed, 110 insertions(+), 51 deletions(-) diff --git a/demo/auth_demo/oauth_demo.py b/demo/auth_demo/oauth_demo.py index ee997ff..c268ae5 100644 --- a/demo/auth_demo/oauth_demo.py +++ b/demo/auth_demo/oauth_demo.py @@ -18,34 +18,39 @@ import logging from logging import Logger from pathlib import Path -from typing import Tuple, Dict, List, cast +from typing import Tuple, Dict, List from configparser import ConfigParser import requests from auth_pardot_api import AuthPardotAPI from client import PardotAPI -from auth_handler import T, TraditionalAuthHandler, OAuthHandler +from auth_handler import T as AuthHndlr, TraditionalAuthHandler, OAuthHandler class PardotAuthenticationDemo(object): def __init__(self, config_file: Path, logger: Logger = None) -> None: super().__init__() + self.config_file = config_file self.logger = logger self.parser = ConfigParser() self.parser.read(config_file) - def run(self): - # Demonstrate raw oauth process - self.access_pardot_via_oauth_using_raw_requests() + def run(self): # Demonstrate accessing pardot the traditional way using # a Pardot-Only user via the existing PyPardot4 api self.access_pardot_via_traditional_api() + # Demonstrate the acturl requrets and responses + # needed to access pardot using + # a salesforce user via SSO using OAuth2 + self.access_pardot_via_oauth_using_raw_requests() + # Demonstrate accessing pardot using - # a salesforce user and OAuth2 + # a salesforce user via SSO using OAuth2 + # using the AuthPardotAPI sub-class of the existing PardotAPI class self.access_pardot_via_oauth_api() @@ -55,22 +60,36 @@ def access_pardot_via_oauth_using_raw_requests(self): from Salesforce and how to use it to access Pardot. """ self.logger and self.logger.info("\tAccess Pardot via SF SSO Using Raw Requests") - access_token, bus_unit_id = self.retrieve_access_token() - prospects = self.send_pardot_request(access_token, bus_unit_id) - self.logger and self.logger.info("\t\t...Success") + if self.has_sections(["test_data", "salesforce"]): + access_token, bus_unit_id = self.retrieve_access_token() + prospects = self.send_pardot_request(access_token, bus_unit_id) + self.logger and self.logger.info("\t\t...Success") def access_pardot_via_traditional_api(self): """ Use the existing PardotAPI to fetch prospect data via a pardot-only user. """ - self.logger and self.logger.info("\tAccess Pardot via Pardot-Only User") - auth_handler = self.get_auth_handler("pardot") - pd = PardotAPI(auth_handler.username, auth_handler.password, auth_handler.userkey) - self.query_pardot_api(pd, "pardot") - - self.logger and self.logger.info("\tAccess Pardot Sandbox via Pardot-Only User") - self.query_pardot_api(pd, "pardot_sandbox") - self.logger and self.logger.info("\t\t...Success") + self.logger and self.logger.info("\tAccess Pardot via PardotAPI using Pardot-Only User") + if self.has_sections(["test_data", "pardot"]): + auth_handler = self.get_auth_handler("pardot") + pd = PardotAPI(auth_handler.username, auth_handler.password, auth_handler.userkey) + pd.authenticate() + self.query_pardot_api(pd, "pardot") + + self.logger and self.logger.info("\tAccess Pardot via AuthPardotAPI using Pardot-Only User") + if self.has_sections(["test_data", "pardot"]): + auth_handler = self.get_auth_handler("pardot") + pd = AuthPardotAPI(auth_handler, logger=self.logger) + pd.authenticate() + self.query_pardot_api(pd, "pardot") + + self.logger and self.logger.info("\tAccess Pardot Sandbox via AuthPardotAPI using Pardot-Only User") + if self.has_sections(["test_data", "pardot_sandbox"]): + auth_handler = self.get_auth_handler("pardot_sandbox") + pd = AuthPardotAPI(auth_handler, logger=self.logger) + pd.authenticate() + self.query_pardot_api(pd, "pardot_sandbox") + self.logger and self.logger.info("\t\t...Success") def access_pardot_via_oauth_api(self): """ @@ -79,34 +98,38 @@ def access_pardot_via_oauth_api(self): # Accessing a production pardot server using credentials from a production salesforce instance self.logger and self.logger.info("\tAccess Pardot via SF SSO") - auth_handler = self.get_auth_handler("salesforce") - pd = AuthPardotAPI(auth_handler, logger=self.logger) - self.query_pardot_api(pd, "salesforce") + if self.has_sections(["test_data", "salesforce"]): + auth_handler = self.get_auth_handler("salesforce") + pd = AuthPardotAPI(auth_handler, logger=self.logger) + self.query_pardot_api(pd, "salesforce") + self.logger and self.logger.info("\t\t...Success") # Accessing a pardot sandbox server using credentials from a salesforce sandbox instance # Commented out because I can't test this, we don't have a pardot sandbox - auth_handler = self.get_auth_handler("salesforce") - pd = AuthPardotAPI(auth_handler, logger=self.logger) - self.query_pardot_api(pd, "salesforce_sandbox") - self.logger and self.logger.info("\t\t...Success") + # self.logger and self.logger.info("\tAccess Pardot Sandbox via SF Sandbox SSO") + # if self.has_sections(["test_data", "salesforce_sandbox"]): + # auth_handler = self.get_auth_handler("salesforce_sandbox") + # pd = AuthPardotAPI(auth_handler, logger=self.logger) + # self.query_pardot_api(pd, "salesforce_sandbox") + # self.logger and self.logger.info("\t\t...Success") def query_pardot_api(self, pd: PardotAPI, section: str) -> Tuple[Dict, List]: """ Use a pardot api to fetch prospect data The read_by_email() utilizes an http post while the query() utilizes an http get(). """ - prospect_email = self.parser.get(section, "prospect_email") - response = pd.prospects.read_by_email(email="eb@object.com") + prospect_email = self.parser.get("test_data", "prospect_email") + response = pd.prospects.read_by_email(email=prospect_email) prospect = response["prospect"] - self.logger and self.logger.debug( + self.logger and self.logger.info( f"\t\tFound Prospect in {section}: {prospect['first_name']} {prospect['last_name']} for email {prospect['email']}") - prospect_date_filter = self.parser.get(section, "prospect_date_filter", fallback="2021-01-01") + prospect_date_filter = self.parser.get("test_data", "prospect_date_filter", fallback="2021-01-01") response = pd.prospects.query(created_after=prospect_date_filter) prospects = response["prospect"] - self.logger and self.logger.debug(f"\t\tFound Prospects in {section} created after {prospect_date_filter}:") + self.logger and self.logger.info(f"\t\tFound {len(prospects)} Prospects in {section} created after {prospect_date_filter}:") for p in prospects: - self.logger and self.logger.debug(f"\t\t\t{p['first_name']} {p['last_name']} <{p['email']}>") + self.logger and self.logger.debug(f"\t\t\t{p['created_at']}: {p['first_name']} {p['last_name']} <{p['email']}>") return prospect, prospects @@ -130,8 +153,9 @@ def retrieve_access_token(self): return access_token, auth_handler.business_unit_id def send_pardot_request(self, access_token, bus_unit_id) -> List: + prospect_date_filter = self.parser.get("test_data", "prospect_date_filter", fallback="2021-01-01") params = {"format": "json", - "id_less_than": 5427630} + "created_after": prospect_date_filter} headers = {"Authorization": f"Bearer {access_token}", "Pardot-Business-Unit-Id": bus_unit_id, "Content-Type": "application/x-www-form-urlencoded" @@ -146,13 +170,13 @@ def send_pardot_request(self, access_token, bus_unit_id) -> List: raise ValueError(f"Pardot Request Failure: {response['@attributes']['stat']}") prospects = response["result"]["prospect"] - self.logger and self.logger.debug(f"\t\tFound prospects in Production with ids less than 5427630:") - for prospect in prospects: + self.logger and self.logger.info(f"\t\tFound {len(prospects)} Prospects in Production created after {prospect_date_filter}:") + for p in prospects: self.logger and self.logger.debug( - f"\t\t\t{prospect['first_name']} {prospect['last_name']} <{prospect['email']}>") + f"\t\t\t{p['created_at']}: {p['first_name']} {p['last_name']} <{p['email']}>") return prospects - def get_auth_handler(self, section_name: str) -> T: + def get_auth_handler(self, section_name: str) -> AuthHndlr: if section_name.startswith("pardot"): return TraditionalAuthHandler(self.parser.get(section_name, "username"), self.parser.get(section_name, "password"), @@ -167,9 +191,17 @@ def get_auth_handler(self, section_name: str) -> T: token=self.parser.get(section_name, "token"), logger=self.logger) + def has_sections(self, sections: List[str]) -> bool: + missing = [s for s in sections if s not in self.parser.sections()] + valid = len(missing) == 0 + if not valid: + self.logger and self.logger.info(f"\t\t...Skip, missing sections {missing} " + f"from config file {self.config_file}") + return valid + if __name__ == '__main__': logger = logging.getLogger("OAUTH_DEMO") - logger.setLevel(logging.DEBUG) + logger.setLevel(logging.INFO) ch = logging.StreamHandler() formatter = logging.Formatter('[{levelname:>8s}] {asctime} {name:s}: {message:s}', style='{') ch.setFormatter(formatter) diff --git a/demo/auth_demo/pardot_demo.ini b/demo/auth_demo/pardot_demo.ini index 53305ca..36f32d5 100644 --- a/demo/auth_demo/pardot_demo.ini +++ b/demo/auth_demo/pardot_demo.ini @@ -10,8 +10,6 @@ prospect_date_filter= password= userkeyn= -prospect_email= -prospect_date_filter= [salesforce] user= @@ -28,4 +26,8 @@ token= consumer_key= consumer_secret= business_unit_id= + +[test_data] +prospect_email= +prospect_date_filter= ############################### diff --git a/pypardot/auth_handler.py b/pypardot/auth_handler.py index 8c2cc95..7e0039b 100644 --- a/pypardot/auth_handler.py +++ b/pypardot/auth_handler.py @@ -24,6 +24,9 @@ def handle_authentication(self) -> bool: def auth_header(self) -> Dict: raise NotImplementedError(f"auth_header() called on abstract base class {self.__class__}") + def __repr__(self): + return f"{self.__class__.__name__}" + class UserAuthHandler(AuthHandler[T]): """Abstract base class for handling authentication that requires a username and password""" @@ -33,6 +36,14 @@ def __init__(self, username: str, password: str, **kwargs) -> None: self.username = username self.password = password + def __repr__(self): + return f"{super().__repr__()}: UN:{self.username[:10]}..." + + def get_userkey(self) -> Optional[str]: + raise NotImplementedError( + f"get_userkey() called on abstract base class {self.__class__}") + + def handle_authentication(self) -> bool: raise NotImplementedError( f"handle_authentication() called on {self.__class__}. Should be handled by PardotAPI") @@ -42,6 +53,7 @@ def auth_header(self) -> Dict: f"auth_header() called on {self.__class__}. Should be handled by PardotAPI") + class TraditionalAuthHandler(UserAuthHandler[T]): """Handles authentication of Pardot-only users""" @@ -49,6 +61,13 @@ def __init__(self, username: str, password: str, userkey: str, **kwargs) -> None super().__init__(username=username, password=password, **kwargs) self.userkey = userkey + def __repr__(self): + return f"{super().__repr__()}, UK:{self.userkey[:10]}..." + + def get_userkey(self) -> Optional[str]: + return self.userkey + + class OAuthHandler(UserAuthHandler[T]): """ @@ -67,6 +86,15 @@ def __init__(self, username: str, password: str, self.is_sandbox = is_sandbox self.access_token: Optional[str] = None + def __repr__(self): + at_label = f"{self.access_token[:10]}..." if self.access_token else "None" + return f"{super().__repr__()}, CK:{self.consumer_key[:10]}..., CS:{self.consumer_secret[:10]}..., " \ + f"BU:{self.business_unit_id} AT:{at_label}" + + + def get_userkey(self) -> Optional[str]: + return None + def handle_authentication(self) -> bool: params = { "grant_type": "password", @@ -80,7 +108,7 @@ def handle_authentication(self) -> bool: r = requests.post(url, params=params) content = r.json() self.access_token = content.get("access_token") - self.logger and self.logger.debug(f"Retrieved oauth access_token for {content.get('instance_url')}") + self.logger and self.logger.debug(f"OAuthHandler: Retrieved oauth access_token for {content.get('instance_url')}") return True def auth_header(self) -> Dict: diff --git a/pypardot/auth_pardot_api.py b/pypardot/auth_pardot_api.py index f9b2eea..92ab612 100644 --- a/pypardot/auth_pardot_api.py +++ b/pypardot/auth_pardot_api.py @@ -17,7 +17,7 @@ import requests -from auth_handler import AuthHandler, UserAuthHandler, OAuthHandler +from auth_handler import AuthHandler, UserAuthHandler, OAuthHandler, TraditionalAuthHandler from client import PardotAPI from errors import PardotAPIError @@ -25,20 +25,20 @@ class AuthPardotAPI(PardotAPI): def __init__(self, auth_handler: UserAuthHandler, version=4, logger: Optional[Logger] = None): - super().__init__(auth_handler.username, auth_handler.password, auth_handler.userkey, version) + super().__init__(auth_handler.username, auth_handler.password, auth_handler.get_userkey(), version) self.auth_handler: Optional[AuthHandler] = auth_handler self.logger = logger def use_username_authorization(self) -> bool: - return self.auth_handler is None + return self.auth_handler is None or isinstance(self.auth_handler, TraditionalAuthHandler) def authenticate(self): if self.use_username_authorization(): - self.logger and self.logger.debug(f"Authenticate Pardot with user {self.email}") + self.logger and self.logger.debug(f"AuthPardotAPI: Authenticate Pardot with Pardot-Only user {self.email[:10]}...") return super().authenticate() else: - oauth_handler: OAuthHandler = cast(self.auth_handler, OAuthHandler) - self.logger and self.logger.debug(f"Authenticate Pardot with OAuth2 key {oauth_handler.consumer_key}") + oauth_handler: OAuthHandler = cast(OAuthHandler, self.auth_handler) + self.logger and self.logger.debug(f"AuthPardotAPI: Authenticate Pardot with OAuth2 key {oauth_handler.consumer_key[:10]}...") success = self.auth_handler.handle_authentication() return success @@ -55,9 +55,9 @@ def post(self, object_name, path=None, params=None, retries=0): params = {} if params is None else params params.update({'format': 'json'}) - headers = self._build_auth_header() try: self._check_auth(object_name=object_name) + headers = self._build_auth_header() request = requests.post(self._full_path(object_name, self.version, path), data=params, headers=headers) response = self._check_response(request) return response @@ -78,10 +78,7 @@ def _check_auth(self, object_name): if self.use_username_authorization(): return super()._check_auth(object_name) - if object_name == 'login': - return - - oauth_handler: OAuthHandler = cast(self.auth_handler, OAuthHandler) + oauth_handler: OAuthHandler = cast(OAuthHandler, self.auth_handler) if oauth_handler.access_token is None: self.authenticate() @@ -89,7 +86,7 @@ def _build_auth_header(self): if self.use_username_authorization(): return super()._build_auth_header() - oauth_handler: OAuthHandler = cast(self.auth_handler, OAuthHandler) + oauth_handler: OAuthHandler = cast(OAuthHandler, self.auth_handler) if not oauth_handler.access_token: raise Exception('Cannot build Authorization header. access_token or bus_unit_id is empty') return oauth_handler.auth_header() From b37de2a03e0b203164bda056a10a52f1d865fafe Mon Sep 17 00:00:00 2001 From: Eric Bergerson Date: Thu, 21 Jan 2021 12:06:06 -0500 Subject: [PATCH 3/4] Document the branch --- demo/auth_demo/README.md | 77 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 demo/auth_demo/README.md diff --git a/demo/auth_demo/README.md b/demo/auth_demo/README.md new file mode 100644 index 0000000..a0b2a04 --- /dev/null +++ b/demo/auth_demo/README.md @@ -0,0 +1,77 @@ +# PyPardot4 Single Sign On Enhancements + +The upcoming Spring '21 release of Salesforce requires that all access to Pardot +be authenticated via Single Sign On with Salesforce via OAuth2. This branch provides +an approach to enhancing PyPardot4 to extend it's functionality to be ready for +the new release. This branch is **NOT** meant to actually be merged, as is, into +the PyPardot4 project. Rather, it was committed to provide an example of an approach +and to discuss with the community if it would be a valuable approach, and what changes +would move it towards code that should be merged with the project. + +## Approach + +1. To show additional functionality through a subclass of PardotAPI, so as to highlight the + methodology for supporting SSO without changing existing code. +1. To separate authentication from api through the use of a hierarchy of authenticators. + +## Demonstration + +There is demonstration code provided in `oauth_demo.py` to show retroactive support for Pardot-Only +authentication, raw `requests` level construction of working with Pardot using +OAuth2 authentication, and use of the AuthPardotAPI extension to PardotAPI to +work with the pardot api exactly as before with the extended ability to authenticate +using SSO credentials. + +The demonstration program requires a configuration file, `oauth_demo.ini` which by default +is sought in the users home directory. This can be changed at the top level of the +demo program. The content needed in that file is described in `pardot_demo.ini` next to +the demonstration program. Since the config file will have private credentials in it, +it is purposefully located outside of the project by default. If your environment +does not support the sandboxes or you do not want to hit your production instances you +can leave out sections, the demo program will skip the demonstrations it can not perform. + +## Issues for Discussion + +### Retroactive Support + +Given that Pardot-Only authentication is fully going away, retroactive support for +this authentication is not really necessary. However, for the short time that both exist, it +is useful for testing. Given this, it is not necessary to truly subclass the Pardot API class. +That was done here to show a clean seperation between what was and what can be. + +### Separating Authentication from PardotAPI + +That said, it is suggested strongly that authentication functionality be taken out of the +ultimate PardotAPI class and be shifted to a proper hierarchy of authentication classes. +A quick review of the +[`simple_salesforce`](https://github.com/simple-salesforce/simple-salesforce) package shows +three or more different ways in which one can authenticate with the Salesforce API. +The code in this branch demonstrates achieving SSO using only one of them. By separating +the authentication from the API class, it allows the PyPardot4 package to be easily extendable +to other methods of Salesforce authentication. + +### Context Managers + +It would seem very appropriate to enhance the PardotAPI class to be a context manager, such +that it is authenticated as part of entering the context. This branch does not yet include +those extensions in the AuthPardotAPI sub-class to not confuse the primary objective of +accomplishing SSO. However, if this code is to be cleaned up for true integration into +the project, it is strongly suggested that this enhancement be included. + +## Caveats - IMPORTANT + +### 3.7 + +This code was developed and tested under Python 3.7.4. +It was not tested under any other version. + +### Relative Imports + +This demonstration code made every effort to not change any of the existing code and demonstrate +the SSO functionality simply by adding code (for now). However, there were changes made to +remove all relative imports to enable the code to execute in our 3.7.4 environment. +This is another good reason not to merge this code as is. If the community decides +that we should move ahead with this branch, the continued use of relative imports should +be discussed and if it is decided that they should continue to be utilized, we will need +assistance in figuring out how to get them to work in our environment. + From e171182834ce1ce58209fa7c0c22f1caf7fd64db Mon Sep 17 00:00:00 2001 From: Eric Bergerson Date: Fri, 22 Jan 2021 13:25:11 -0500 Subject: [PATCH 4/4] Refine documentation --- demo/auth_demo/README.md | 40 ++++++++-------- demo/auth_demo/oauth_demo.py | 90 ++++++++++++++++++++---------------- 2 files changed, 70 insertions(+), 60 deletions(-) diff --git a/demo/auth_demo/README.md b/demo/auth_demo/README.md index a0b2a04..a111934 100644 --- a/demo/auth_demo/README.md +++ b/demo/auth_demo/README.md @@ -5,57 +5,58 @@ be authenticated via Single Sign On with Salesforce via OAuth2. This branch pro an approach to enhancing PyPardot4 to extend it's functionality to be ready for the new release. This branch is **NOT** meant to actually be merged, as is, into the PyPardot4 project. Rather, it was committed to provide an example of an approach -and to discuss with the community if it would be a valuable approach, and what changes -would move it towards code that should be merged with the project. +and to discuss with the community if it would be a valuable addition, and if so, what changes +would be needed to prepare the code to be merged with the project. ## Approach 1. To show additional functionality through a subclass of PardotAPI, so as to highlight the methodology for supporting SSO without changing existing code. -1. To separate authentication from api through the use of a hierarchy of authenticators. +1. To separate authentication methodology from the api through the + use of a hierarchy of authenticator classes. ## Demonstration -There is demonstration code provided in `oauth_demo.py` to show retroactive support for Pardot-Only -authentication, raw `requests` level construction of working with Pardot using -OAuth2 authentication, and use of the AuthPardotAPI extension to PardotAPI to -work with the pardot api exactly as before with the extended ability to authenticate -using SSO credentials. +There is demonstration code provided in `oauth_demo.py` that shows: +1. Retroactive support for Pardot-Only authentication. +1. Example of how to perform SSO via OAuth2 authentication using raw `requests` level construction. +1. Support for SSO via OAuth2 authentication via the AuthPardotAPI subclass of PardotAPI The demonstration program requires a configuration file, `oauth_demo.ini` which by default is sought in the users home directory. This can be changed at the top level of the demo program. The content needed in that file is described in `pardot_demo.ini` next to the demonstration program. Since the config file will have private credentials in it, -it is purposefully located outside of the project by default. If your environment -does not support the sandboxes or you do not want to hit your production instances you -can leave out sections, the demo program will skip the demonstrations it can not perform. +it is purposefully located outside of the project by default. The configuration file has sections +for different pardot and salesforce instances, both production and sand box. If you do not desire to +hit against any of these while running the demo, or you do not have access to any of these, you +can eleminate those sections. The demo program will skip code that requires the missing sections. ## Issues for Discussion ### Retroactive Support Given that Pardot-Only authentication is fully going away, retroactive support for -this authentication is not really necessary. However, for the short time that both exist, it -is useful for testing. Given this, it is not necessary to truly subclass the Pardot API class. -That was done here to show a clean seperation between what was and what can be. +the old style of authentication is not really necessary. However, for the short time that both exist, it +is useful for testing. Given this, it is not necessary to truly subclass the Pardot API class as shown here. +That architecture was used in this version to show a clean separation between what was and what can be. ### Separating Authentication from PardotAPI That said, it is suggested strongly that authentication functionality be taken out of the -ultimate PardotAPI class and be shifted to a proper hierarchy of authentication classes. +updated PardotAPI class and be shifted to a proper hierarchy of authentication classes. A quick review of the [`simple_salesforce`](https://github.com/simple-salesforce/simple-salesforce) package shows three or more different ways in which one can authenticate with the Salesforce API. The code in this branch demonstrates achieving SSO using only one of them. By separating the authentication from the API class, it allows the PyPardot4 package to be easily extendable -to other methods of Salesforce authentication. +to other methods of Salesforce authentication to be used for SSO if necessary. ### Context Managers It would seem very appropriate to enhance the PardotAPI class to be a context manager, such that it is authenticated as part of entering the context. This branch does not yet include those extensions in the AuthPardotAPI sub-class to not confuse the primary objective of -accomplishing SSO. However, if this code is to be cleaned up for true integration into +accomplishing SSO. However, if this code is to be re-worked in order to be merged into the project, it is strongly suggested that this enhancement be included. ## Caveats - IMPORTANT @@ -68,10 +69,9 @@ It was not tested under any other version. ### Relative Imports This demonstration code made every effort to not change any of the existing code and demonstrate -the SSO functionality simply by adding code (for now). However, there were changes made to -remove all relative imports to enable the code to execute in our 3.7.4 environment. +the SSO functionality simply by adding code (for now). However, there were changes made +removing all relative imports from existing files to enable the code to execute in our 3.7.4 environment. This is another good reason not to merge this code as is. If the community decides that we should move ahead with this branch, the continued use of relative imports should be discussed and if it is decided that they should continue to be utilized, we will need assistance in figuring out how to get them to work in our environment. - diff --git a/demo/auth_demo/oauth_demo.py b/demo/auth_demo/oauth_demo.py index c268ae5..184b0b2 100644 --- a/demo/auth_demo/oauth_demo.py +++ b/demo/auth_demo/oauth_demo.py @@ -1,17 +1,18 @@ ''' A demonstration of connecting to Pardot using both a pardot-only user and the existing api and -using a sf user utilizing the expanding api. - -This demonstration requires a configuration file -called `pardot_demo.ini` to supply all of the -instance specific authentication data needed to -run this code in a specific environment. -The format of the file should match the -example `pardot_demo.ini` file provided -in this package. The file should -be placed in the users home directory, -purposefully outside this repository. +using a sf user utilizing the expanded api. + +This demonstration requires a configuration file. +By default it looks for a file called +`oauth_demo.ini` in the users home directory +(see __main__ at the bottom of this file). + +This file is used to supply the authentication data +needed to run this code. In this package is +a file `pardot_demo.ini` that contains the structure +of the config file and a description of what values +need to be provided. ''' __author__ = 'eb' @@ -37,38 +38,27 @@ def __init__(self, config_file: Path, logger: Logger = None) -> None: self.parser = ConfigParser() self.parser.read(config_file) - def run(self): # Demonstrate accessing pardot the traditional way using - # a Pardot-Only user via the existing PyPardot4 api - self.access_pardot_via_traditional_api() + # a Pardot-Only user + self.access_pardot_using_pardot_only_user() - # Demonstrate the acturl requrets and responses - # needed to access pardot using + # Demonstrate the formation of and responses to the low + # level requests needed to access pardot using # a salesforce user via SSO using OAuth2 - self.access_pardot_via_oauth_using_raw_requests() + self.access_pardot_via_sso_using_raw_requests() # Demonstrate accessing pardot using # a salesforce user via SSO using OAuth2 # using the AuthPardotAPI sub-class of the existing PardotAPI class - self.access_pardot_via_oauth_api() - + self.access_pardot_via_sso_in_enhanced_api() - def access_pardot_via_oauth_using_raw_requests(self): + def access_pardot_using_pardot_only_user(self): """ - Demonstrate the low level request formation needed to retrieve an access token - from Salesforce and how to use it to access Pardot. + Use the existing PardotAPI to fetch prospect data via a pardot-only user. """ - self.logger and self.logger.info("\tAccess Pardot via SF SSO Using Raw Requests") - if self.has_sections(["test_data", "salesforce"]): - access_token, bus_unit_id = self.retrieve_access_token() - prospects = self.send_pardot_request(access_token, bus_unit_id) - self.logger and self.logger.info("\t\t...Success") - def access_pardot_via_traditional_api(self): - """ - Use the existing PardotAPI to fetch prospect data via a pardot-only user. - """ + # Using the traditional PyPardot4 PardotAPI class self.logger and self.logger.info("\tAccess Pardot via PardotAPI using Pardot-Only User") if self.has_sections(["test_data", "pardot"]): auth_handler = self.get_auth_handler("pardot") @@ -76,6 +66,7 @@ def access_pardot_via_traditional_api(self): pd.authenticate() self.query_pardot_api(pd, "pardot") + # using a TraditionalAuthHandler via the enhanced AuthPardotAPI self.logger and self.logger.info("\tAccess Pardot via AuthPardotAPI using Pardot-Only User") if self.has_sections(["test_data", "pardot"]): auth_handler = self.get_auth_handler("pardot") @@ -83,6 +74,7 @@ def access_pardot_via_traditional_api(self): pd.authenticate() self.query_pardot_api(pd, "pardot") + # To a sandbox using a TraditionalAuthHandler via the enhanced AuthPardotAPI self.logger and self.logger.info("\tAccess Pardot Sandbox via AuthPardotAPI using Pardot-Only User") if self.has_sections(["test_data", "pardot_sandbox"]): auth_handler = self.get_auth_handler("pardot_sandbox") @@ -91,9 +83,21 @@ def access_pardot_via_traditional_api(self): self.query_pardot_api(pd, "pardot_sandbox") self.logger and self.logger.info("\t\t...Success") - def access_pardot_via_oauth_api(self): + def access_pardot_via_sso_using_raw_requests(self): """ - Use the AuthPardotAPI to fetch prospect data via OAuth2 using a SSO user from Salesforce + Demonstrate the formation of and responses to the low + level requests needed to access pardot using + a salesforce user via SSO using OAuth2 + """ + self.logger and self.logger.info("\tAccess Pardot via SF SSO Using Raw Requests") + if self.has_sections(["test_data", "salesforce"]): + access_token, bus_unit_id = self.retrieve_access_token() + prospects = self.send_pardot_request(access_token, bus_unit_id) + self.logger and self.logger.info("\t\t...Success") + + def access_pardot_via_sso_in_enhanced_api(self): + """ + Use the AuthPardotAPI to fetch prospect data via sso using OAuth2 authentication """ # Accessing a production pardot server using credentials from a production salesforce instance @@ -105,7 +109,7 @@ def access_pardot_via_oauth_api(self): self.logger and self.logger.info("\t\t...Success") # Accessing a pardot sandbox server using credentials from a salesforce sandbox instance - # Commented out because I can't test this, we don't have a pardot sandbox + # Commented out because I can't test this, our pardot sandbox can not be authenticated using SSO. # self.logger and self.logger.info("\tAccess Pardot Sandbox via SF Sandbox SSO") # if self.has_sections(["test_data", "salesforce_sandbox"]): # auth_handler = self.get_auth_handler("salesforce_sandbox") @@ -115,8 +119,10 @@ def access_pardot_via_oauth_api(self): def query_pardot_api(self, pd: PardotAPI, section: str) -> Tuple[Dict, List]: """ - Use a pardot api to fetch prospect data - The read_by_email() utilizes an http post while the query() utilizes an http get(). + Demonstrate and return prospect data fetched from pardot using the pardot api. + This code purposefully includes the use of the api read_by_email() and query() method, + because the former utilizes an http post() while the later utilizes an http get(). + This ensures the demonstration includes both forms of interaction. """ prospect_email = self.parser.get("test_data", "prospect_email") response = pd.prospects.read_by_email(email=prospect_email) @@ -127,9 +133,11 @@ def query_pardot_api(self, pd: PardotAPI, section: str) -> Tuple[Dict, List]: prospect_date_filter = self.parser.get("test_data", "prospect_date_filter", fallback="2021-01-01") response = pd.prospects.query(created_after=prospect_date_filter) prospects = response["prospect"] - self.logger and self.logger.info(f"\t\tFound {len(prospects)} Prospects in {section} created after {prospect_date_filter}:") + self.logger and self.logger.info( + f"\t\tFound {len(prospects)} Prospects in {section} created after {prospect_date_filter}:") for p in prospects: - self.logger and self.logger.debug(f"\t\t\t{p['created_at']}: {p['first_name']} {p['last_name']} <{p['email']}>") + self.logger and self.logger.debug( + f"\t\t\t{p['created_at']}: {p['first_name']} {p['last_name']} <{p['email']}>") return prospect, prospects @@ -170,7 +178,8 @@ def send_pardot_request(self, access_token, bus_unit_id) -> List: raise ValueError(f"Pardot Request Failure: {response['@attributes']['stat']}") prospects = response["result"]["prospect"] - self.logger and self.logger.info(f"\t\tFound {len(prospects)} Prospects in Production created after {prospect_date_filter}:") + self.logger and self.logger.info( + f"\t\tFound {len(prospects)} Prospects in Production created after {prospect_date_filter}:") for p in prospects: self.logger and self.logger.debug( f"\t\t\t{p['created_at']}: {p['first_name']} {p['last_name']} <{p['email']}>") @@ -199,9 +208,10 @@ def has_sections(self, sections: List[str]) -> bool: f"from config file {self.config_file}") return valid + if __name__ == '__main__': logger = logging.getLogger("OAUTH_DEMO") - logger.setLevel(logging.INFO) + logger.setLevel(logging.INFO) # Change to logging.DEBUG for more detailed output ch = logging.StreamHandler() formatter = logging.Formatter('[{levelname:>8s}] {asctime} {name:s}: {message:s}', style='{') ch.setFormatter(formatter)