Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 8 additions & 2 deletions FuelSDK/Public_WebAppTests/test_ET_Client.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
2 changes: 1 addition & 1 deletion FuelSDK/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
28 changes: 17 additions & 11 deletions FuelSDK/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from suds.sax.element import Element


from FuelSDK.constants import USER_AGENT
from FuelSDK.objects import ET_DataExtension,ET_Subscriber


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)


Expand All @@ -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'))
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
})

Expand Down
2 changes: 2 additions & 0 deletions FuelSDK/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__version__ = '2.0.0'
USER_AGENT = 'FuelSDK-Python-v' + __version__
10 changes: 6 additions & 4 deletions FuelSDK/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import json
import copy

from FuelSDK.constants import USER_AGENT


########
##
Expand Down Expand Up @@ -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)


Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
89 changes: 34 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
# FuelSDK-Python v1.3.0
# FuelSDK-Python v2.0.0

Feverup's fork of Salesforce Marketing Cloud Fuel SDK for Python

## Overview

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
Expand Down Expand Up @@ -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))
```


Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
32 changes: 16 additions & 16 deletions objsamples/sample_bounceevent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion objsamples/sample_campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,5 @@
print( '-----------------------------')

except Exception as e:
print( 'Caught exception: ' + e.message)
print( 'Caught exception: ' + str(e))
print( e )
Loading
Loading