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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@
import json
import copy

from FuelSDK.constants import USER_AGENT


########
##
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@
import json
import copy

from FuelSDK.constants import USER_AGENT


########
##
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@
import json
import copy

from FuelSDK.constants import USER_AGENT


########
##
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@
import json
import copy

from FuelSDK.constants import USER_AGENT


########
##
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@
import json
import copy

from FuelSDK.constants import USER_AGENT


########
##
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@
import json
import copy

from FuelSDK.constants import USER_AGENT


########
##
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@
import json
import copy

from FuelSDK.constants import USER_AGENT


########
##
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@
import json
import copy

from FuelSDK.constants import USER_AGENT


########
##
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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