diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..9e3126c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,47 @@ +name: Tests + +on: + push: + branches: + - master + - 'feature/**' + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - '3.11' + - '3.12' + - '3.13' + - '3.14' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e ".[dev]" + + - name: Verify suds-jurko is not installed + run: | + if python -m pip show suds-jurko >/dev/null 2>&1; then + echo "suds-jurko must not be installed" + exit 1 + fi + + - name: Run offline tests + run: pytest tests/ -q + + - name: Compile package and samples + run: python -m compileall FuelSDK objsamples ET_Client.py diff --git a/FuelSDK/Public_WebAppTests/test_ET_Client.py b/FuelSDK/Public_WebAppTests/test_ET_Client.py index 44d149b..a53adaf 100644 --- a/FuelSDK/Public_WebAppTests/test_ET_Client.py +++ b/FuelSDK/Public_WebAppTests/test_ET_Client.py @@ -1,8 +1,14 @@ -from unittest import TestCase +import os +import unittest + from FuelSDK import ET_Client -class TestET_Client(TestCase): +@unittest.skipUnless( + os.environ.get('FUELSDK_LIVE_TESTS') == '1', + 'Set FUELSDK_LIVE_TESTS=1 and configure credentials to run live tests.', +) +class TestET_Client(unittest.TestCase): @classmethod def setUpClass(cls): diff --git a/FuelSDK/__init__.py b/FuelSDK/__init__.py index 7dad4b6..aa8ec56 100644 --- a/FuelSDK/__init__.py +++ b/FuelSDK/__init__.py @@ -1,4 +1,4 @@ -__version__ = '1.3.0' +from FuelSDK.constants import __version__, USER_AGENT # Runtime patch the suds library from FuelSDK.suds_patch import _PropertyAppender diff --git a/FuelSDK/client.py b/FuelSDK/client.py index f1d34ee..ecbdf15 100644 --- a/FuelSDK/client.py +++ b/FuelSDK/client.py @@ -11,6 +11,7 @@ from suds.sax.element import Element +from FuelSDK.constants import USER_AGENT from FuelSDK.objects import ET_DataExtension,ET_Subscriber @@ -195,12 +196,17 @@ def configure_client(self, get_server_wsdl, params, tokenResponse): ## get the JWT from the params if passed in...or go to the server to get it if (params is not None and 'jwt' in params): - decodedJWT = jwt.decode(params['jwt'], self.appsignature) - self.authToken = decodedJWT['request']['user']['oauthToken'] - self.authTokenExpiration = time.time() + decodedJWT['request']['user']['expiresIn'] - self.internalAuthToken = decodedJWT['request']['user']['internalOauthToken'] - if 'refreshToken' in decodedJWT: - self.refreshKey = tokenResponse['request']['user']['refreshToken'] + decodedJWT = jwt.decode( + params['jwt'], + self.appsignature, + algorithms=['HS256'], + ) + user = decodedJWT['request']['user'] + self.authToken = user['oauthToken'] + self.authTokenExpiration = time.time() + user['expiresIn'] + self.internalAuthToken = user['internalOauthToken'] + if 'refreshToken' in user: + self.refreshKey = user['refreshToken'] self.build_soap_client() pass else: @@ -238,7 +244,7 @@ def retrieve_server_wsdl(self, wsdl_url, file_location): get the WSDL from the server and save it locally """ r = requests.get(wsdl_url) - f = open(file_location, 'w') + f = open(file_location, 'w', encoding='utf-8') f.write(r.text) @@ -248,7 +254,7 @@ def build_soap_client(self): self.soap_client = suds.client.Client(self.wsdl_file_url, faults=False, cachingpolicy=1) self.soap_client.set_options(location=self.soap_endpoint) - self.soap_client.set_options(headers={'user-agent' : 'FuelSDK-Python-v1.3.0'}) + self.soap_client.set_options(headers={'user-agent' : USER_AGENT}) if self.use_oAuth2_authentication == 'True': element_oAuth = Element('fueloauth', ns=('etns', 'http://exacttarget.com')) @@ -277,7 +283,7 @@ def refresh_token(self, force_refresh = False): #If we don't already have a token or the token expires within 5 min(300 seconds), get one if (force_refresh or self.authToken is None or (self.authTokenExpiration is not None and time.time() + 300 > self.authTokenExpiration)): - headers = {'content-type' : 'application/json', 'user-agent' : 'FuelSDK-Python-v1.3.0'} + headers = {'content-type' : 'application/json', 'user-agent' : USER_AGENT} if (self.authToken is None): payload = {'clientId' : self.client_id, 'clientSecret' : self.client_secret, 'accessType': 'offline'} else: @@ -313,7 +319,7 @@ def refresh_token_with_oAuth2(self, force_refresh=False): or self.authTokenExpiration is not None and time.time() + 300 > self.authTokenExpiration: headers = {'content-type': 'application/json', - 'user-agent': 'FuelSDK-Python-v1.3.0'} + 'user-agent': USER_AGENT} payload = self.create_payload() @@ -395,7 +401,7 @@ def get_soap_endpoint(self): """ try: r = requests.get(self.base_api_url + '/platform/v1/endpoints/soap', headers={ - 'user-agent': 'FuelSDK-Python-v1.3.0', + 'user-agent': USER_AGENT, 'authorization': 'Bearer ' + self.authToken }) diff --git a/FuelSDK/constants.py b/FuelSDK/constants.py new file mode 100644 index 0000000..c7f493c --- /dev/null +++ b/FuelSDK/constants.py @@ -0,0 +1,2 @@ +__version__ = '2.0.0' +USER_AGENT = 'FuelSDK-Python-v' + __version__ diff --git a/FuelSDK/rest.py b/FuelSDK/rest.py index 698447b..b3e947a 100644 --- a/FuelSDK/rest.py +++ b/FuelSDK/rest.py @@ -2,6 +2,8 @@ import json import copy +from FuelSDK.constants import USER_AGENT + ######## ## @@ -331,7 +333,7 @@ def __init__(self, auth_stub, endpoint, qs = None): fullendpoint += urlSeparator + qStringValue + '=' + str(qs[qStringValue]) urlSeparator = '&' - headers = {'authorization' : 'Bearer ' + auth_stub.authToken, 'user-agent' : 'FuelSDK-Python-v1.3.0'} + headers = {'authorization' : 'Bearer ' + auth_stub.authToken, 'user-agent' : USER_AGENT} r = requests.get(fullendpoint, headers=headers) @@ -349,7 +351,7 @@ class ET_PostRest(ET_Constructor): def __init__(self, auth_stub, endpoint, payload): auth_stub.refresh_token() - headers = {'content-type' : 'application/json', 'user-agent' : 'FuelSDK-Python-v1.3.0', 'authorization' : 'Bearer ' + auth_stub.authToken} + headers = {'content-type' : 'application/json', 'user-agent' : USER_AGENT, 'authorization' : 'Bearer ' + auth_stub.authToken} r = requests.post(endpoint, data=json.dumps(payload), headers=headers) obj = super(ET_PostRest, self).__init__(r, True) @@ -364,7 +366,7 @@ class ET_PatchRest(ET_Constructor): def __init__(self, auth_stub, endpoint, payload): auth_stub.refresh_token() - headers = {'content-type' : 'application/json', 'user-agent' : 'FuelSDK-Python-v1.3.0', 'authorization' : 'Bearer ' + auth_stub.authToken} + headers = {'content-type' : 'application/json', 'user-agent' : USER_AGENT, 'authorization' : 'Bearer ' + auth_stub.authToken} r = requests.patch(endpoint , data=json.dumps(payload), headers=headers) obj = super(ET_PatchRest, self).__init__(r, True) @@ -379,7 +381,7 @@ class ET_DeleteRest(ET_Constructor): def __init__(self, auth_stub, endpoint): auth_stub.refresh_token() - headers = {'authorization' : 'Bearer ' + auth_stub.authToken, 'user-agent' : 'FuelSDK-Python-v1.3.0'} + headers = {'authorization' : 'Bearer ' + auth_stub.authToken, 'user-agent' : USER_AGENT} r = requests.delete(endpoint, headers=headers) obj = super(ET_DeleteRest, self).__init__(r, True) diff --git a/README.md b/README.md index 38c3295..1bb7906 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FuelSDK-Python v1.3.0 +# FuelSDK-Python v2.0.0 Feverup's fork of Salesforce Marketing Cloud Fuel SDK for Python @@ -6,6 +6,13 @@ Feverup's fork of Salesforce Marketing Cloud Fuel SDK for Python The Fuel SDK for Python provides easy access to Salesforce Marketing Cloud's Fuel API Family services, including a collection of REST APIs and a SOAP API. These APIs provide access to Salesforce Marketing Cloud functionality via common collection types such as array/hash. +New Features in Version 2.0.0 +------------ +* Python 3.11–3.14 support +* Replaced unmaintained `suds-jurko` with the community-maintained [`suds`](https://pypi.org/project/suds/) package (suds-community) +* PyJWT 2.x compatibility +* Offline unit tests and GitHub Actions CI + New Features in Version 1.3.0 ------------ * Added Refresh Token support for OAuth2 authentication @@ -171,11 +178,11 @@ list.auth_stub = myClient response = list.get() # Print out the results for viewing -print 'Post Status: ' + str(response.status) -print 'Code: ' + str(response.code) -print 'Message: ' + str(response.message) -print 'Result Count: ' + str(len(response.results)) -print 'Results: ' + str(response.results) +print('Post Status: ' + str(response.status)) +print('Code: ' + str(response.code)) +print('Message: ' + str(response.message)) +print('Result Count: ' + str(len(response.results))) +print('Results: ' + str(response.results)) ``` @@ -257,10 +264,23 @@ Using this wsdl file also resolves [issue:81](https://github.com/salesforce-mark If you would like to help contribute to the FuelSDK-Python project, checkout the code from the [GitHub project page](https://github.com/salesforce-marketingcloud/FuelSDK-Python). The use of [virtualenvwrapper](http://virtualenvwrapper.readthedocs.org/) is highly recommended. After installing virtualenvwrapper you can run the following commands to setup a sandbox for development. ``` -git clone git@github.com:salesforce-marketingcloud/FuelSDK-Python.git +git clone git@github.com:Feverup/FuelSDK-Python.git mkvirtualenv FuelSDK-Python cd FuelSDK-Python -pip install -r requirements.txt +pip install -r requirements-dev.txt +pip install -e . +``` + +Run the offline test suite: + +``` +pytest tests/ +``` + +To run live Marketing Cloud integration tests, configure `config.python` (or environment variables) and set `FUELSDK_LIVE_TESTS=1`: + +``` +FUELSDK_LIVE_TESTS=1 pytest tests/test_live_et_client.py FuelSDK/Public_WebAppTests/test_ET_Client.py ``` You will then have a sandbox which includes all dependencies for doing development on FuelSDK-Python. @@ -274,60 +294,19 @@ On Windows: ## Requirements -Python 3.3.x +Python 3.11+ (tested on 3.11, 3.12, 3.13, and 3.14) Libraries: -* pyjwt +* PyJWT (2.x) * requests -* suds - -### Custom Suds Changes (Deprecated) +* suds (community fork, `suds>=1.2.0` on PyPI) -**Note**: Suds is now patched at runtime when importing the FuelSDK. You no longer need to edit the library. Please be aware of the change. +### Suds runtime patches -The default Suds 0.4 Package that is available for download needs to have a couple small fixes applied in order for it to fully support the Fuel SDK. Please update your suds installation using the following instructions: -Suds-jurko 0.6 supports Python 3.x.x +FuelSDK applies small runtime patches to suds when the package is imported (see `FuelSDK/suds_patch.py` and `FuelSDK/__init__.py`). You do **not** need to install or modify `suds-jurko` manually. -- Download the suds package source from https://pypi.python.org/pypi/suds-jurko/0.6 -- Open the file located wihin the uncompressed files at: `suds\mx\appender.py` -- At line 223, the following lines will be present: -```python -child.setText(p.get()) -parent.append(child) -for item in p.items(): - cont = Content(tag=item[0], value=item[1]) - Appender.append(self, child, cont) -``` - -- Replace those lines with: -```python -child_value = p.get() -if(child_value is None): - pass -else: - child.setText(child_value) - parent.append(child) - for item in p.items(): - cont = Content(tag=item[0], value=item[1]) - Appender.append(self, child, cont) -``` - -- Open the file located wihin the uncompressed files at `suds\bindings\document.py` -- After line 62 which reads: -```python -n += 1 -``` - -- Add the following lines: -```python -if value is None: - continue -``` -- Install Suds by running the command -``` -python setup.py install -`` +The historical instructions for downloading `suds-jurko` 0.6 and editing files under `site-packages` are obsolete as of v2.0.0. ## Copyright and license Copyright (c) 2017 Salesforce diff --git a/objsamples/sample_bounceevent.py b/objsamples/sample_bounceevent.py index 5dc6dc6..e027d57 100644 --- a/objsamples/sample_bounceevent.py +++ b/objsamples/sample_bounceevent.py @@ -34,32 +34,32 @@ # The following request could potentially bring back large amounts of data if run against a production account ''' - print '>>> Retrieve All BounceEvents with GetMoreResults' + print('>>> Retrieve All BounceEvents with GetMoreResults') getBounceEvent = ET_BounceEvent.new() getBounceEvent.auth_stub = stubObj getBounceEvent.props = ["SendID","SubscriberKey","EventDate","Client.ID","EventType","BatchID","TriggeredSendDefinitionObjectID","PartnerKey"] getResponse = getBounceEvent.get - print 'Retrieve Status: ' + str(getResponse.status) - print 'Code: ' + str(getResponse.code) - print 'Message: ' + str(getResponse.message) - print 'MoreResults: ' + str(getResponse.more_results) - print 'RequestID: ' + str(getResponse.request_id) - print 'Results Length: ' + str(len(getResponse.results)) + print('Retrieve Status: ' + str(getResponse.status) + print('Code: ' + str(getResponse.code) + print('Message: ' + str(getResponse.message) + print('MoreResults: ' + str(getResponse.more_results) + print('RequestID: ' + str(getResponse.request_id) + print('Results Length: ' + str(len(getResponse.results)) # Since this could potentially return a large number of results, we do not want to print the results - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) while getResponse.moreResults do - print '>>> Continue Retrieve All BounceEvents with GetMoreResults' + print('>>> Continue Retrieve All BounceEvents with GetMoreResults') getResponse = getBounceEvent.getMoreResults - print 'Retrieve Status: ' + str(getResponse.status) - print 'Code: ' + str(getResponse.code) - print 'Message: ' + str(getResponse.message) - print 'MoreResults: ' + str(getResponse.more_results) - print 'RequestID: ' + str(getResponse.request_id) - print 'Results Length: ' + str(len(getResponse.results)) + print('Retrieve Status: ' + str(getResponse.status) + print('Code: ' + str(getResponse.code) + print('Message: ' + str(getResponse.message) + print('MoreResults: ' + str(getResponse.more_results) + print('RequestID: ' + str(getResponse.request_id) + print('Results Length: ' + str(len(getResponse.results)) end ''' except Exception as e: - print ('Caught exception: ' + e.message) + print ('Caught exception: ' + str(e)) print (e) \ No newline at end of file diff --git a/objsamples/sample_campaign.py b/objsamples/sample_campaign.py index 12899e1..74822fb 100644 --- a/objsamples/sample_campaign.py +++ b/objsamples/sample_campaign.py @@ -163,5 +163,5 @@ print( '-----------------------------') except Exception as e: - print( 'Caught exception: ' + e.message) + print( 'Caught exception: ' + str(e)) print( e ) \ No newline at end of file diff --git a/objsamples/sample_clickevent.py b/objsamples/sample_clickevent.py index d2eea19..ca13fff 100644 --- a/objsamples/sample_clickevent.py +++ b/objsamples/sample_clickevent.py @@ -21,7 +21,7 @@ print ('RequestID: ' + str(getResponse.request_id)) print ('Results Length: ' + str(len(getResponse.results))) # Since this could potentially return a large number of results, we do not want to print the results - #print 'Results: ' + getResponse.results.to_s + #print('Results: ') + getResponse.results.to_s while getResponse.more_results: print ('>>> Continue Retrieve Filtered ClickEvents with GetMoreResults') @@ -35,32 +35,32 @@ # The following request could potentially bring back large amounts of data if run against a production account ''' - print '>>> Retrieve All ClickEvents with GetMoreResults' + print('>>> Retrieve All ClickEvents with GetMoreResults') getClickEvent = ET_ClickEvent.new() getClickEvent.auth_stub = stubObj getClickEvent.props = ["SendID","SubscriberKey","EventDate","Client.ID","EventType","BatchID","TriggeredSendDefinitionObjectID","PartnerKey"] getResponse = getClickEvent.get() - print 'Retrieve Status: ' + str(getResponse.status) - print 'Code: ' + str(getResponse.code) - print 'Message: ' + str(getResponse.message) - print 'MoreResults: ' + str(getResponse.more_results) - print 'RequestID: ' + str(getResponse.request_id) - print 'Results Length: ' + str(len(getResponse.results)) + print('Retrieve Status: ' + str(getResponse.status) + print('Code: ' + str(getResponse.code) + print('Message: ' + str(getResponse.message) + print('MoreResults: ' + str(getResponse.more_results) + print('RequestID: ' + str(getResponse.request_id) + print('Results Length: ' + str(len(getResponse.results)) # Since this could potentially return a large number of results, we do not want to print the results - #print 'Results: ' + getResponse.results.to_s + #print('Results: ') + getResponse.results.to_s while getResponse.more_results: - print '>>> Continue Retrieve All ClickEvents with GetMoreResults' + print('>>> Continue Retrieve All ClickEvents with GetMoreResults') getResponse = getClickEvent.getMoreResults() - print 'Retrieve Status: ' + str(getResponse.status) - print 'Code: ' + str(getResponse.code) - print 'Message: ' + str(getResponse.message) - print 'MoreResults: ' + str(getResponse.more_results) - print 'RequestID: ' + str(getResponse.request_id) - print 'Results Length: ' + str(len(getResponse.results)) + print('Retrieve Status: ' + str(getResponse.status) + print('Code: ' + str(getResponse.code) + print('Message: ' + str(getResponse.message) + print('MoreResults: ' + str(getResponse.more_results) + print('RequestID: ' + str(getResponse.request_id) + print('Results Length: ' + str(len(getResponse.results)) ''' except Exception as e: - print ('Caught exception: ' + e.message) + print ('Caught exception: ' + str(e)) print (e) diff --git a/objsamples/sample_contentarea.py b/objsamples/sample_contentarea.py index f65fbd9..0a5657c 100644 --- a/objsamples/sample_contentarea.py +++ b/objsamples/sample_contentarea.py @@ -15,7 +15,7 @@ print('Message: ' + str(getResponse.message)) print('MoreResults: ' + str(getResponse.more_results)) print('Results Length: ' + str(len(getResponse.results))) - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) while getResponse.more_results: print('>>> Continue Retrieve All ContentArea with GetMoreResults') @@ -108,5 +108,5 @@ print('Results: ' + str(getResponse.results)) except Exception as e: - print('Caught exception: ' + str(e.message)) + print('Caught exception: ' + str(e)) print(e) \ No newline at end of file diff --git a/objsamples/sample_dataextension.py b/objsamples/sample_dataextension.py index ae844ca..e115e79 100644 --- a/objsamples/sample_dataextension.py +++ b/objsamples/sample_dataextension.py @@ -17,7 +17,7 @@ print('MoreResults: ' + str(getResponse.more_results)) print('RequestID: ' + str(getResponse.request_id)) print('Results Length: ' + str(len(getResponse.results))) - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) # Get all of the DataExtensions in an Account belonging to a specific sub account print('>>> Get all of the DataExtensions in an Account belonging to a specific sub account') @@ -32,7 +32,7 @@ print('MoreResults: ' + str(getResponse.more_results)) print('RequestID: ' + str(getResponse.request_id)) print('Results Length: ' + str(len(getResponse.results))) - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) # Specify a name for the data extension that will be used for testing # Note: Name and CustomerKey will be the same value @@ -186,7 +186,7 @@ print('MoreResults: ' + str(getResponse.more_results)) print('RequestID: ' + str(getResponse.request_id)) print('Results Length: ' + str(len(getResponse.results))) - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) while getResponse.more_results: print('>>> Continue Retrieve lots of rows with moreResults') @@ -199,5 +199,5 @@ print('Results Length: ' + str(len(getResponse.results))) except Exception as e: - print('Caught exception: ' + str(e.message)) + print('Caught exception: ' + str(e)) print(e) diff --git a/objsamples/sample_email.py b/objsamples/sample_email.py index 8432b82..99352c5 100644 --- a/objsamples/sample_email.py +++ b/objsamples/sample_email.py @@ -15,7 +15,7 @@ print('Message: ' + str(getResponse.message)) print('MoreResults: ' + str(getResponse.more_results)) print('Results Length: ' + str(len(getResponse.results))) - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) while getResponse.more_results: print('>>> Continue Retrieve All Email with GetMoreResults') @@ -108,5 +108,5 @@ print('Results: ' + str(getResponse.results)) except Exception as e: - print('Caught exception: ' + e.message) + print('Caught exception: ' + str(e)) print(e) \ No newline at end of file diff --git a/objsamples/sample_folder.py b/objsamples/sample_folder.py index 4d06a00..b3cb03d 100644 --- a/objsamples/sample_folder.py +++ b/objsamples/sample_folder.py @@ -15,7 +15,7 @@ print('Message: ' + str(getResponse.message)) print('MoreResults: ' + str(getResponse.more_results)) print('Results Length: ' + str(len(getResponse.results))) - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) while getResponse.more_results: print('>>> Continue Retrieve All Folder with GetMoreResults') @@ -126,5 +126,5 @@ print('Results: ' + str(getResponse.results)) except Exception as e: - print('Caught exception: ' + str(e.message)) + print('Caught exception: ' + str(e)) print(e) \ No newline at end of file diff --git a/objsamples/sample_helper_AddSubscriberToList.py b/objsamples/sample_helper_AddSubscriberToList.py index b37876b..046a6d7 100644 --- a/objsamples/sample_helper_AddSubscriberToList.py +++ b/objsamples/sample_helper_AddSubscriberToList.py @@ -46,5 +46,5 @@ print('Results: ' + str(deleteResponse.results)) except Exception as e: - print('Caught exception: ' + e.message) + print('Caught exception: ' + str(e)) print(e) \ No newline at end of file diff --git a/objsamples/sample_helper_CreateDataExtensions.py b/objsamples/sample_helper_CreateDataExtensions.py index 55d69eb..0cf7159 100644 --- a/objsamples/sample_helper_CreateDataExtensions.py +++ b/objsamples/sample_helper_CreateDataExtensions.py @@ -46,5 +46,5 @@ print('Results: ' + str(delResponse.results)) except Exception as e: - print('Caught exception: ' + e.message) + print('Caught exception: ' + str(e)) print(e) \ No newline at end of file diff --git a/objsamples/sample_list.py b/objsamples/sample_list.py index d0f3b0e..2626d01 100644 --- a/objsamples/sample_list.py +++ b/objsamples/sample_list.py @@ -91,6 +91,6 @@ print('Results: ' + str(getResponse.results)) except Exception as e: - print('Caught exception: ' + str(e.message)) + print('Caught exception: ' + str(e)) diff --git a/objsamples/sample_list_subscriber.py b/objsamples/sample_list_subscriber.py index a0081d8..60f110d 100644 --- a/objsamples/sample_list_subscriber.py +++ b/objsamples/sample_list_subscriber.py @@ -83,5 +83,5 @@ print('Results: ' + str(deleteResponse.results)) except Exception as e: - print('Caught exception: ' + e.message) + print('Caught exception: ' + str(e)) print(e) \ No newline at end of file diff --git a/objsamples/sample_openevent.py b/objsamples/sample_openevent.py index 1e93180..f6c6d41 100644 --- a/objsamples/sample_openevent.py +++ b/objsamples/sample_openevent.py @@ -21,7 +21,7 @@ print('RequestID: ' + str(getResponse.request_id)) print('Results Length: ' + str(len(getResponse.results))) # Since this could potentially return a large number of results, we do not want to print the results - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) while getResponse.more_results: print('>>> Continue Retrieve Filtered OpenEvents with GetMoreResults') @@ -35,31 +35,31 @@ # The following request could potentially bring back large amounts of data if run against a production account ''' - print '>>> Retrieve All OpenEvents with GetMoreResults' + print('>>> Retrieve All OpenEvents with GetMoreResults') getOpenEvent = ET_Client.ET_OpenEvent() getOpenEvent.auth_stub = stubObj getOpenEvent.props = ["SendID","SubscriberKey","EventDate","Client.ID","EventType","BatchID","TriggeredSendDefinitionObjectID","PartnerKey"] getResponse = getOpenEvent.get() - print 'Retrieve Status: ' + str(getResponse.status) - print 'Code: ' + str(getResponse.code) - print 'Message: ' + str(getResponse.message) - print 'MoreResults: ' + str(getResponse.more_results) - print 'RequestID: ' + str(getResponse.request_id) - print 'Results Length: ' + str(len(getResponse.results)) + print('Retrieve Status: ' + str(getResponse.status) + print('Code: ' + str(getResponse.code) + print('Message: ' + str(getResponse.message) + print('MoreResults: ' + str(getResponse.more_results) + print('RequestID: ' + str(getResponse.request_id) + print('Results Length: ' + str(len(getResponse.results)) # Since this could potentially return a large number of results, we do not want to print the results - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) while getResponse.more_results: - print '>>> Continue Retrieve All OpenEvents with GetMoreResults' + print('>>> Continue Retrieve All OpenEvents with GetMoreResults') getResponse = getOpenEvent.getMoreResults() - print 'Retrieve Status: ' + str(getResponse.status) - print 'Code: ' + str(getResponse.code) - print 'Message: ' + str(getResponse.message) - print 'MoreResults: ' + str(getResponse.more_results) - print 'RequestID: ' + str(getResponse.request_id) - print 'Results Length: ' + str(len(getResponse.results)) + print('Retrieve Status: ' + str(getResponse.status) + print('Code: ' + str(getResponse.code) + print('Message: ' + str(getResponse.message) + print('MoreResults: ' + str(getResponse.more_results) + print('RequestID: ' + str(getResponse.request_id) + print('Results Length: ' + str(len(getResponse.results)) ''' except Exception as e: - print('Caught exception: ' + e.message) + print('Caught exception: ' + str(e)) print(e) \ No newline at end of file diff --git a/objsamples/sample_profileattribute.py b/objsamples/sample_profileattribute.py index 7ab5061..111795f 100644 --- a/objsamples/sample_profileattribute.py +++ b/objsamples/sample_profileattribute.py @@ -19,5 +19,5 @@ print('Results: ' + str(postResponse.results)) except Exception as e: - print('Caught exception: ' + e.message) + print('Caught exception: ' + str(e)) print(e) diff --git a/objsamples/sample_sentevent.py b/objsamples/sample_sentevent.py index 81d3b94..1b20e6a 100644 --- a/objsamples/sample_sentevent.py +++ b/objsamples/sample_sentevent.py @@ -22,7 +22,7 @@ print('Results Length: ' + str(len(getResponse.results))) print('Results: ' + str(getResponse.results)) # Since this could potentially return a large number of results, we do not want to print the results - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) while getResponse.more_results: print('>>> Continue Retrieve Filtered SentEvents with GetMoreResults') @@ -36,31 +36,31 @@ # The following request could potentially bring back large amounts of data if run against a production account ''' - print '>>> Retrieve All SentEvents with GetMoreResults' + print('>>> Retrieve All SentEvents with GetMoreResults') getSentEvent = ET_Client.ET_SentEvent() getSentEvent.auth_stub = stubObj getSentEvent.props = ["SendID","SubscriberKey","EventDate","Client.ID","EventType","BatchID","TriggeredSendDefinitionObjectID","ListID","PartnerKey","SubscriberID"] getResponse = getSentEvent.get() - print 'Retrieve Status: ' + str(getResponse.status) - print 'Code: ' + str(getResponse.code) - print 'Message: ' + str(getResponse.message) - print 'MoreResults: ' + str(getResponse.more_results) - print 'RequestID: ' + str(getResponse.request_id) - print 'Results Length: ' + str(len(getResponse.results)) + print('Retrieve Status: ' + str(getResponse.status) + print('Code: ' + str(getResponse.code) + print('Message: ' + str(getResponse.message) + print('MoreResults: ' + str(getResponse.more_results) + print('RequestID: ' + str(getResponse.request_id) + print('Results Length: ' + str(len(getResponse.results)) # Since this could potentially return a large number of results, we do not want to print the results - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) while getResponse.more_results: - print '>>> Continue Retrieve All SentEvents with GetMoreResults' + print('>>> Continue Retrieve All SentEvents with GetMoreResults') getResponse = getSentEvent.getMoreResults() - print 'Retrieve Status: ' + str(getResponse.status) - print 'Code: ' + str(getResponse.code) - print 'Message: ' + str(getResponse.message) - print 'MoreResults: ' + str(getResponse.more_results) - print 'RequestID: ' + str(getResponse.request_id) - print 'Results Length: ' + str(len(getResponse.results)) + print('Retrieve Status: ' + str(getResponse.status) + print('Code: ' + str(getResponse.code) + print('Message: ' + str(getResponse.message) + print('MoreResults: ' + str(getResponse.more_results) + print('RequestID: ' + str(getResponse.request_id) + print('Results Length: ' + str(len(getResponse.results)) ''' except Exception as e: - print('Caught exception: ' + str(e.message)) + print('Caught exception: ' + str(e)) print(e.backtrace) diff --git a/objsamples/sample_subscriber.py b/objsamples/sample_subscriber.py index 79248c1..0790732 100644 --- a/objsamples/sample_subscriber.py +++ b/objsamples/sample_subscriber.py @@ -92,30 +92,30 @@ # Do not run the "Retrieve All Subscribers" request for testing if you have more than 100,000 records in your account as it will take a long time to complete. # Retrieve All Subcribers with GetMoreResults - print '>>> Retrieve All Subcribers with GetMoreResults' + print('>>> Retrieve All Subcribers with GetMoreResults') getSub = ET_Client.ET_Subscriber() getSub.auth_stub = stubObj getSub.props = ["SubscriberKey", "EmailAddress", "Status"] getResponse = getSub.get() - print 'Retrieve Status: ' + str(getResponse.status) - print 'Code: ' + str(getResponse.code) - print 'Message: ' + str(getResponse.message) - print 'MoreResults: ' + str(getResponse.more_results) - print 'RequestID: ' + str(getResponse.request_id) - print 'Results Length: ' + str(len(getResponse.results)) - #print 'Results: ' + str(getResponse.results) + print('Retrieve Status: ' + str(getResponse.status) + print('Code: ' + str(getResponse.code) + print('Message: ' + str(getResponse.message) + print('MoreResults: ' + str(getResponse.more_results) + print('RequestID: ' + str(getResponse.request_id) + print('Results Length: ' + str(len(getResponse.results)) + #print('Results: ' + str(getResponse.results) while getResponse.more_results: - print '>>> Continue Retrieve All Subcribers with GetMoreResults' + print('>>> Continue Retrieve All Subcribers with GetMoreResults') getResponse = getSub.getMoreResults() - print 'Retrieve Status: ' + str(getResponse.status) - print 'Code: ' + str(getResponse.code) - print 'Message: ' + str(getResponse.message) - print 'MoreResults: ' + str(getResponse.more_results) - print 'RequestID: ' + str(getResponse.request_id) - print 'Results Length: ' + str(len(getResponse.results)) + print('Retrieve Status: ' + str(getResponse.status) + print('Code: ' + str(getResponse.code) + print('Message: ' + str(getResponse.message) + print('MoreResults: ' + str(getResponse.more_results) + print('RequestID: ' + str(getResponse.request_id) + print('Results Length: ' + str(len(getResponse.results)) ''' except Exception as e: - print('Caught exception: ' + str(e.message)) + print('Caught exception: ' + str(e)) print(e.backtrace) diff --git a/objsamples/sample_triggeredsend.py b/objsamples/sample_triggeredsend.py index c595e9e..c7821c7 100644 --- a/objsamples/sample_triggeredsend.py +++ b/objsamples/sample_triggeredsend.py @@ -16,7 +16,7 @@ print('Message: ' + str(getResponse.message)) print('MoreResults: ' + str(getResponse.more_results)) print('Results Count: ' + str(len(getResponse.results))) - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) # Specify the name of a TriggeredSend that was setuprint for testing # Do not use a production Triggered Send Definition @@ -119,5 +119,5 @@ print('Results: ' + str(deleteResponse.results)) except Exception as e: - print('Caught exception: ' + e.message) + print('Caught exception: ' + str(e)) print(e) \ No newline at end of file diff --git a/objsamples/sample_unsubevent.py b/objsamples/sample_unsubevent.py index f3a53b0..143b003 100644 --- a/objsamples/sample_unsubevent.py +++ b/objsamples/sample_unsubevent.py @@ -22,7 +22,7 @@ print('Results Length: ' + str(len(getResponse.results))) print('results: ' + str(getResponse.results)) # Since this could potentially return a large number of results, we do not want to print the results - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) while getResponse.more_results: print('>>> Continue Retrieve Filtered UnsubEvents with GetMoreResults') @@ -37,31 +37,31 @@ # The following request could potentially bring back large amounts of data if run against a production account ''' - print '>>> Retrieve All UnsubEvents with GetMoreResults' + print('>>> Retrieve All UnsubEvents with GetMoreResults') getUnsubEvent = ET_Client.ET_UnsubEvent() getUnsubEvent.auth_stub = stubObj getUnsubEvent.props = ["SendID","SubscriberKey","EventDate","Client.ID","EventType","BatchID","TriggeredSendDefinitionObjectID","PartnerKey"] getResponse = getUnsubEvent.get() - print 'Retrieve Status: ' + str(getResponse.status) - print 'Code: ' + str(getResponse.code) - print 'Message: ' + str(getResponse.message) - print 'MoreResults: ' + str(getResponse.more_results) - print 'RequestID: ' + str(getResponse.request_id) - print 'Results Length: ' + str(len(getResponse.results)) + print('Retrieve Status: ' + str(getResponse.status) + print('Code: ' + str(getResponse.code) + print('Message: ' + str(getResponse.message) + print('MoreResults: ' + str(getResponse.more_results) + print('RequestID: ' + str(getResponse.request_id) + print('Results Length: ' + str(len(getResponse.results)) # Since this could potentially return a large number of results, we do not want to print the results - #print 'Results: ' + str(getResponse.results) + #print('Results: ' + str(getResponse.results) while getResponse.more_results: - print '>>> Continue Retrieve All UnsubEvents with GetMoreResults' + print('>>> Continue Retrieve All UnsubEvents with GetMoreResults') getResponse = getUnsubEvent.getMoreResults() - print 'Retrieve Status: ' + str(getResponse.status) - print 'Code: ' + str(getResponse.code) - print 'Message: ' + str(getResponse.message) - print 'MoreResults: ' + str(getResponse.more_results) - print 'RequestID: ' + str(getResponse.request_id) - print 'Results Length: ' + str(len(getResponse.results)) + print('Retrieve Status: ' + str(getResponse.status) + print('Code: ' + str(getResponse.code) + print('Message: ' + str(getResponse.message) + print('MoreResults: ' + str(getResponse.more_results) + print('RequestID: ' + str(getResponse.request_id) + print('Results Length: ' + str(len(getResponse.results)) ''' except Exception as e: - print('Caught exception: ' + e.message) + print('Caught exception: ' + str(e)) print(e) \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..17d7648 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest>=7.0.0 diff --git a/requirements.txt b/requirements.txt index 1b11c7a..91df22f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,3 @@ -PyJWT>=1.5.3 -distribute==0.7.3 +PyJWT>=2.8.0 requests>=2.18.4 -suds-jurko==0.6 -wsgiref==0.1.2 - +suds>=1.2.0 diff --git a/setup.py b/setup.py index 79c3d08..b75df99 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ readme = f.read() setup( - version='1.3.1', + version='2.0.0', name='Fever-FuelSDK', description='Fever Salesforce Marketing Cloud Fuel SDK for Python', long_description=readme, @@ -14,17 +14,27 @@ packages=['FuelSDK'], url='https://github.com/Feverup/FuelSDK-Python', license='MIT', + python_requires='>=3.11', install_requires=[ - 'pyjwt>=1.5.3', + 'PyJWT>=2.8.0', 'requests>=2.18.4', - 'suds-jurko==0.6', + 'suds>=1.2.0', ], + extras_require={ + 'dev': [ + 'pytest>=7.0.0', + ], + }, classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Topic :: Software Development :: Libraries', - 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', ], ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_create_payload.py b/tests/test_create_payload.py new file mode 100644 index 0000000..02790ff --- /dev/null +++ b/tests/test_create_payload.py @@ -0,0 +1,65 @@ +import unittest + +from FuelSDK import ET_Client + + +def make_client(**overrides): + client = ET_Client.__new__(ET_Client) + client.client_id = 'client-id' + client.client_secret = 'client-secret' + client.refreshKey = None + client.account_id = None + client.scope = None + client.authorization_code = 'auth-code' + client.redirect_URI = 'https://example.com/callback' + client.application_type = 'server' + for key, value in overrides.items(): + setattr(client, key, value) + return client + + +class TestCreatePayload(unittest.TestCase): + def test_public_app_payload(self): + client = make_client(application_type='public') + + payload = client.create_payload() + + self.assertEqual(client.client_id, payload['client_id']) + self.assertEqual(client.redirect_URI, payload['redirect_uri']) + self.assertEqual(client.authorization_code, payload['code']) + self.assertEqual('authorization_code', payload['grant_type']) + self.assertNotIn('client_secret', payload) + + def test_web_app_payload(self): + client = make_client(application_type='web') + + payload = client.create_payload() + + self.assertEqual('authorization_code', payload['grant_type']) + self.assertEqual(client.client_id, payload['client_id']) + self.assertEqual(client.client_secret, payload['client_secret']) + self.assertEqual(client.redirect_URI, payload['redirect_uri']) + self.assertEqual(client.authorization_code, payload['code']) + + def test_server_app_payload(self): + client = make_client(application_type='server') + + payload = client.create_payload() + + self.assertEqual('client_credentials', payload['grant_type']) + self.assertEqual(client.client_id, payload['client_id']) + self.assertEqual(client.client_secret, payload['client_secret']) + self.assertNotIn('code', payload) + self.assertNotIn('redirect_uri', payload) + + def test_refresh_token_payload(self): + client = make_client(refreshKey='refresh-key') + + payload = client.create_payload() + + self.assertEqual('refresh_token', payload['grant_type']) + self.assertEqual('refresh-key', payload['refresh_token']) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_et_constructor.py b/tests/test_et_constructor.py new file mode 100644 index 0000000..99c2e24 --- /dev/null +++ b/tests/test_et_constructor.py @@ -0,0 +1,53 @@ +import unittest +from unittest.mock import Mock + +from FuelSDK.rest import ET_Constructor + + +class TestETConstructor(unittest.TestCase): + def test_soap_success_response(self): + response = ( + 200, + { + 'OverallStatus': 'OK', + 'Results': [{'ID': 1}], + 'RequestID': 'req-123', + }, + ) + + result = ET_Constructor(response, rest=False) + + self.assertTrue(result.status) + self.assertEqual(200, result.code) + self.assertEqual('OK', result.message) + self.assertEqual('req-123', result.request_id) + self.assertEqual([{'ID': 1}], result.results) + + def test_soap_more_data_available(self): + response = ( + 200, + { + 'OverallStatus': 'MoreDataAvailable', + 'Results': [{'ID': 1}], + }, + ) + + result = ET_Constructor(response, rest=False) + + self.assertTrue(result.status) + self.assertTrue(result.more_results) + + def test_rest_success_response(self): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = [{'id': 1}] + + result = ET_Constructor(mock_response, rest=True) + + self.assertTrue(result.status) + self.assertEqual(200, result.code) + self.assertEqual([{'id': 1}], result.results) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_import.py b/tests/test_import.py new file mode 100644 index 0000000..19a7ab0 --- /dev/null +++ b/tests/test_import.py @@ -0,0 +1,34 @@ +import os +import unittest + +import FuelSDK +import suds + + +class TestImport(unittest.TestCase): + def test_import_fuelsdk(self): + self.assertEqual(FuelSDK.__version__, '2.0.0') + + def test_suds_version(self): + self.assertTrue(suds.__version__.startswith('1.')) + + def test_suds_patch_applied(self): + from suds.mx import appender as suds_appender + from FuelSDK.suds_patch import _PropertyAppender + + self.assertIs(suds_appender.PropertyAppender, _PropertyAppender) + + def test_compileall_objsamples(self): + import compileall + import pathlib + + root = pathlib.Path(__file__).resolve().parents[1] + success = compileall.compile_dir( + str(root / 'objsamples'), + quiet=1, + ) + self.assertTrue(success) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_jwt_decode.py b/tests/test_jwt_decode.py new file mode 100644 index 0000000..8df72ca --- /dev/null +++ b/tests/test_jwt_decode.py @@ -0,0 +1,49 @@ +import time +import unittest +from unittest.mock import patch + +import jwt + +from FuelSDK import ET_Client + + +class TestJwtDecode(unittest.TestCase): + @patch.object(ET_Client, 'build_soap_client') + def test_jwt_params_populate_auth_fields(self, mock_build_soap): + secret = 'appsignature-secret-with-enough-bytes-for-hs256' + expires_in = 3600 + token = jwt.encode( + { + 'request': { + 'user': { + 'oauthToken': 'oauth-token', + 'internalOauthToken': 'internal-token', + 'expiresIn': expires_in, + 'refreshToken': 'refresh-token', + } + } + }, + secret, + algorithm='HS256', + ) + + client = ET_Client( + False, + False, + { + 'clientid': 'client-id', + 'clientsecret': 'client-secret', + 'appsignature': secret, + 'jwt': token, + }, + ) + + self.assertEqual('oauth-token', client.authToken) + self.assertEqual('internal-token', client.internalAuthToken) + self.assertEqual('refresh-token', client.refreshKey) + self.assertGreater(client.authTokenExpiration, time.time()) + mock_build_soap.assert_called_once() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_live_et_client.py b/tests/test_live_et_client.py new file mode 100644 index 0000000..264b5df --- /dev/null +++ b/tests/test_live_et_client.py @@ -0,0 +1,27 @@ +import os +import unittest + +from FuelSDK import ET_Client + + +@unittest.skipUnless( + os.environ.get('FUELSDK_LIVE_TESTS') == '1', + 'Set FUELSDK_LIVE_TESTS=1 and configure credentials to run live tests.', +) +class TestETClientLive(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.client = ET_Client(False, False) + + def test_auth_token_and_refresh_key_differ_after_forced_refresh(self): + auth_token1 = self.client.authToken + refresh_key1 = self.client.refreshKey + + self.client.refresh_token_with_oAuth2(True) + + self.assertNotEqual(auth_token1, self.client.authToken) + self.assertNotEqual(refresh_key1, self.client.refreshKey) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_objsamples_mock.py b/tests/test_objsamples_mock.py new file mode 100644 index 0000000..199ee9f --- /dev/null +++ b/tests/test_objsamples_mock.py @@ -0,0 +1,131 @@ +import runpy +import sys +from io import StringIO +from pathlib import Path +from unittest import TestCase +from unittest.mock import MagicMock, patch + +ROOT = Path(__file__).resolve().parents[1] +SAMPLES_DIR = ROOT / 'objsamples' + + +class SampleResults: + """Mock results object that supports list- and dict-style access used in samples.""" + + def __getitem__(self, key): + if isinstance(key, int): + return {'NewID': 1, 'id': 1} + if key == 'id': + return 1 + if key == 'count': + return 0 + if key == 'items': + return [] + raise KeyError(key) + + def __contains__(self, key): + return key in ('id', 'NewID', 'count', 'items') + + def __len__(self): + return 1 + + def __iter__(self): + return iter([{'NewID': 1, 'id': 1}]) + + +def make_response(message='OK', status=True, code=200, more_results=False): + response = MagicMock() + response.status = status + response.code = code + response.message = message + response.results = SampleResults() + response.more_results = more_results + response.request_id = 'req-123' + return response + + +class MockETClientModule: + def __init__(self): + self.stub = MagicMock() + self.stub.AddSubscriberToList.return_value = make_response('AddSub OK') + self.stub.CreateDataExtensions.return_value = make_response('Create DE OK') + + def ET_Client(self, *args, **kwargs): + return self.stub + + def __getattr__(self, name): + if name == 'ET_Client': + return self.ET_Client + if name.startswith('ET_'): + return self._object_factory + raise AttributeError(name) + + def _object_factory(self, *args, **kwargs): + obj = MagicMock() + response = make_response() + obj.post.return_value = response + obj.get.return_value = response + obj.patch.return_value = response + obj.delete.return_value = response + obj.getMoreResults.return_value = make_response(more_results=False) + obj.send.return_value = make_response('Send OK') + return obj + + +def run_sample(sample_name): + sample_path = SAMPLES_DIR / sample_name + buffer = StringIO() + old_stdout = sys.stdout + old_path = sys.path[:] + mock_module = MockETClientModule() + + try: + sys.stdout = buffer + sys.path.insert(0, str(ROOT)) + sys.modules['ET_Client'] = mock_module + with patch('builtins.quit'): + runpy.run_path(str(sample_path), run_name='__main__') + finally: + sys.stdout = old_stdout + sys.path = old_path + sys.modules.pop('ET_Client', None) + + return buffer.getvalue() + + +class TestObjSamplesMock(TestCase): + def test_sample_list_prints_response_messages(self): + output = run_sample('sample_list.py') + + self.assertIn('Message: OK', output) + self.assertIn('Post Status: True', output) + self.assertIn('Patch Status: True', output) + self.assertIn('Delete Status: True', output) + self.assertNotIn('Caught exception', output) + + def test_sample_profileattribute_prints_response_message(self): + output = run_sample('sample_profileattribute.py') + + self.assertIn('Message: OK', output) + self.assertNotIn('Caught exception', output) + + def test_sample_helper_add_subscriber_prints_response_messages(self): + output = run_sample('sample_helper_AddSubscriberToList.py') + + self.assertIn('Message: OK', output) + self.assertIn('AddSubResponse Status: True', output) + self.assertNotIn('Caught exception', output) + + def test_all_samples_avoid_corrupted_response_references(self): + for sample_path in sorted(SAMPLES_DIR.glob('sample_*.py')): + with self.subTest(sample=sample_path.name): + output = run_sample(sample_path.name) + + self.assertNotIn('Responsstr', output) + self.assertNotIn("name 'e' is not defined", output) + self.assertNotIn('getResponsstr', output) + self.assertNotIn('postResponsstr', output) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_soap_wsdl.py b/tests/test_soap_wsdl.py new file mode 100644 index 0000000..0e32eb9 --- /dev/null +++ b/tests/test_soap_wsdl.py @@ -0,0 +1,80 @@ +import os +import unittest +from unittest.mock import Mock + +import suds.client +from suds.bindings import document as suds_document +from suds.mx.appender import Content +from suds.sax.element import Element +from suds.sudsobject import Property + +from FuelSDK.suds_patch import _PropertyAppender, _bodycontent + + +class TestSoapPatches(unittest.TestCase): + def test_property_appender_patch_is_active(self): + from suds.mx import appender as suds_appender + + self.assertIs(suds_appender.PropertyAppender, _PropertyAppender) + + def test_document_bodycontent_patch_is_active(self): + self.assertIs(suds_document.Document.bodycontent, _bodycontent) + + def test_property_appender_skips_none_values(self): + mock_marshaller = Mock() + mock_marshaller.node.return_value = Element('Child', ns=('ns0', 'http://example.com')) + appender = _PropertyAppender(mock_marshaller) + parent = Element('Parent', ns=('ns0', 'http://example.com')) + prop = Property('Example') + prop.set(None) + + appender.append(parent, Content(tag='Example', value=prop)) + + self.assertEqual([], list(parent)) + mock_marshaller.node.assert_called_once() + + +class TestSoapWSDL(unittest.TestCase): + @classmethod + def setUpClass(cls): + wsdl_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + '..', + 'FuelSDK', + 'ExactTargetWSDL.xml', + ) + ) + cls.wsdl_url = 'file:///' + wsdl_path.replace('\\', '/') + try: + cls.client = suds.client.Client( + cls.wsdl_url, + faults=False, + cachingpolicy=1, + ) + except Exception as exc: + cls.client = None + cls.setup_error = exc + + def setUp(self): + if self.client is None: + self.skipTest( + 'Bundled WSDL requires network access to resolve imported XSD: ' + f'{self.setup_error}' + ) + + def test_factory_create_retrieve_request(self): + retrieve_request = self.client.factory.create('RetrieveRequest') + self.assertIsNotNone(retrieve_request) + + def test_factory_create_simple_filter_part(self): + filter_part = self.client.factory.create('SimpleFilterPart') + self.assertIsNotNone(filter_part) + + def test_factory_create_array_of_object_definition_request(self): + describe_request = self.client.factory.create('ArrayOfObjectDefinitionRequest') + self.assertIsNotNone(describe_request) + + +if __name__ == '__main__': + unittest.main()