Skip to content
Open
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
3 changes: 2 additions & 1 deletion facebook_business/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,10 @@ def init(
timeout=None,
debug=False,
crash_log=True,
retry_strategy=None,
):
session = FacebookSession(app_id, app_secret, access_token, proxies,
timeout)
timeout, retry_strategy=retry_strategy)
api = cls(session, api_version, enable_debug_logger=debug)
cls.set_default_api(api)

Expand Down
31 changes: 31 additions & 0 deletions facebook_business/apiconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,40 @@

# This source code is licensed under the license found in the
# LICENSE file in the root directory of this source tree.
from six.moves import http_client
from urllib3 import Retry


ads_api_config = {
'API_VERSION': 'v22.0',
'SDK_VERSION': 'v22.0.4',
'STRICT_MODE': False

# Whether to enable a retry strategy on any API calls being made. When set
# to True, a default strategy is used, which is also configurable in this
# config.
'RETRY_MODE': False,
'RETRY_STRATEGY': {
'DEFAULT_RETRIES': 5,
'DEFAULT_BACKOFF_FACTOR': 0.5, # Time doubles between API calls.
'RETRY_HTTP_CODES': [
http_client.REQUEST_TIMEOUT,
http_client.TOO_MANY_REQUESTS,
http_client.INTERNAL_SERVER_ERROR,
http_client.SERVICE_UNAVAILABLE,
http_client.GATEWAY_TIMEOUT,
],
}
}


def get_default_retry_strategy():
"""Gets the default retry strategy, based on the API config."""
retry_config = ads_api_config['RETRY_STRATEGY']
return Retry(
total=retry_config["DEFAULT_RETRIES"],
status_forcelist=retry_config["RETRY_HTTP_CODES"],
backoff_factor=retry_config["DEFAULT_BACKOFF_FACTOR"],
raise_on_status=False, # To allow consistent handling of response.
respect_retry_after_header=True,
)
40 changes: 38 additions & 2 deletions facebook_business/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
import requests
import os

from facebook_business.apiconfig import get_default_retry_strategy, ads_api_config


class FacebookSession(object):
"""
FacebookSession manages the the Graph API authentication and https
FacebookSession manages the Graph API authentication and https
connection.

Attributes:
Expand All @@ -26,13 +28,16 @@ class FacebookSession(object):
access_token: The access token.
appsecret_proof: The application secret proof.
proxies: Object containing proxies for 'http' and 'https'
retry_strategy (Optional[urllib3.Retry]): A optional retry strategy to
apply to the API call. If `RETRY_MODE` is True in the `apiconfig`
then we'll use a default Retry strategy.
requests: The python requests object through which calls to the api can
be made.
"""
GRAPH = 'https://graph.facebook.com'

def __init__(self, app_id=None, app_secret=None, access_token=None,
proxies=None, timeout=None, debug=False):
proxies=None, timeout=None, retry_strategy=None, debug=False):
"""
Initializes and populates the instance attributes with app_id,
app_secret, access_token, appsecret_proof, proxies, timeout and requests
Expand All @@ -59,6 +64,8 @@ def __init__(self, app_id=None, app_secret=None, access_token=None,
if self.proxies:
self.requests.proxies.update(self.proxies)

self.retry_strategy = self._mount_retry_strategy(retry_strategy)

def _gen_appsecret_proof(self):
h = hmac.new(
self.app_secret.encode('utf-8'),
Expand All @@ -69,4 +76,33 @@ def _gen_appsecret_proof(self):
self.appsecret_proof = h.hexdigest()
return self.appsecret_proof

def _mount_retry_strategy(self, retry_strategy):
"""
Mounts any available retry strategy to the request's session.

Provides ability to fully specify a Retry strategy, or if RETRY_MODE
is set on the API Config, then a default retry strategy will be used,
which is partially configurable.

Attributes:
retry_strategy (Optional[urllib3.Retry]): The retry strategy to
apply to the session and will be used for all API calls
against the session.
"""
retry_mode = ads_api_config["RETRY_MODE"]
if retry_mode and not retry_strategy:
retry_strategy = get_default_retry_strategy()

# Return early if no retry strategy was found.
if not retry_strategy:
return

# Inject the Retry strategy into the session directly.
adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy)
self.requests.mount("https://", adapter)
self.requests.mount("http://", adapter)

return retry_strategy


__all__ = ['FacebookSession']
163 changes: 163 additions & 0 deletions facebook_business/test/integration_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import unittest
from unittest.mock import patch

import responses
from six.moves import http_client
from urllib3 import Retry

from facebook_business import FacebookAdsApi, apiconfig
from facebook_business.apiconfig import get_default_retry_strategy
from facebook_business.exceptions import FacebookRequestError
from facebook_business.test.integration_utils import IntegrationTestCase


class FacebookAdsApiTestCase(IntegrationTestCase):

def setUp(self):
"""
We're accessing the low-level parts of the API functionality here, so don't want to mock
the requests in the same way, but want to at least partially conform.
"""
self.facebook_ads_api = FacebookAdsApi.init(access_token='access_token', crash_log=False)
self.facebook_ads_api_retry = FacebookAdsApi.init(
access_token='access_token',
crash_log=False,
retry_strategy=get_default_retry_strategy()
)
self.url = "http://facebook.com/some/path"

def tearDown(self):
...

@responses.activate
def test_is_success_200(self):
"""
Simple test to show the API call will respond with a 200 status code
"""
# Arrange - Override the low-level API calls. We just need to make sure these return 200
# as no real API calls should be made.
responses.add(responses.GET, self.url, status=http_client.OK)

# Act
facebook_response = self.facebook_ads_api.call(method="GET", path=self.url)

# Assert
self.assertEqual(facebook_response.status(), http_client.OK)
self.assertTrue(facebook_response.is_success())

@responses.activate
def test_failure_raised_after_service_unavailable(self):
"""
Tests that the API call will raise an error when getting a non 2xx error code.

Default is without a Retry strategy.
"""
# Arrange - Override the low-level API calls. Make sure we start with a 500 then a 200.
responses.add(responses.GET, self.url, status=http_client.INTERNAL_SERVER_ERROR)
responses.add(responses.GET, self.url, status=http_client.OK)

# Act
with self.assertRaises(FacebookRequestError):
self.facebook_ads_api.call(method="GET", path=self.url)

@responses.activate
def test_success_after_service_unavailable_with_implicit_retry_strategy(self):
"""
Tests that the API call will return a 200 after an initial service issue.

Using the default retry strategy.
"""
# Arrange - Override the low-level API calls. Make sure we start with a 500 then a 200.
responses.add(responses.GET, self.url, status=http_client.INTERNAL_SERVER_ERROR)
responses.add(responses.GET, self.url, status=http_client.OK)

# Act
facebook_response = self.facebook_ads_api_retry.call(method="GET", path=self.url)

# Assert
self.assertEqual(facebook_response.status(), http_client.OK)
self.assertTrue(facebook_response.is_success())

@responses.activate
@patch.dict(apiconfig.ads_api_config["RETRY_STRATEGY"], {"DEFAULT_RETRIES": 1})
def test_failure_after_service_unavailable_more_than_default_retry_strategy_allows(self):
"""
Tests that the API call will still raise a `FacebookRequestError` after exhausting retries.

Using the default retry strategy.
"""
facebook_ads_api_retry = FacebookAdsApi.init(
access_token='access_token',
crash_log=False,
)

# Arrange - Override the low-level API calls. Make sure we start with a 500 then a 200.
responses.add(responses.GET, self.url, status=http_client.INTERNAL_SERVER_ERROR)
responses.add(responses.GET, self.url, status=http_client.INTERNAL_SERVER_ERROR)
responses.add(responses.GET, self.url, status=http_client.OK)

# Ac
with self.assertRaises(FacebookRequestError):
facebook_ads_api_retry.call(method="GET", path=self.url)

@responses.activate
def test_success_after_service_unavailable_with_explicit_retry_strategy(self):
"""
Tests that the API call will return a 200 after an initial service issue.

Using ta custom retry strategy.
"""
# Arrange - Define the custom retry.
retry_strategy = Retry(
total=1,
status_forcelist=[http_client.INTERNAL_SERVER_ERROR],
raise_on_status=False, # To allow consistent handling of response.
)
facebook_ads_api_retry = FacebookAdsApi.init(
access_token='access_token',
crash_log=False,
retry_strategy=retry_strategy
)

# Arrange - Override the low-level API calls. Make sure we start with a 500 then a 200.
responses.add(responses.GET, self.url, status=http_client.INTERNAL_SERVER_ERROR)
responses.add(responses.GET, self.url, status=http_client.OK)

# Act
facebook_response = facebook_ads_api_retry.call(method="GET", path=self.url)

# Assert
self.assertEqual(facebook_response.status(), http_client.OK)
self.assertTrue(facebook_response.is_success())

@responses.activate
def test_failure_after_service_unavailable_more_than_explicit_retry_strategy_allows(self):
"""
Tests that the API call will still raise a `FacebookRequestError` after exhausting retries.

Using a custom retry strategy.
"""
# Arrange - Define the custom retry.
retry_strategy = Retry(
total=1,
status_forcelist=[http_client.INTERNAL_SERVER_ERROR],
raise_on_status=False, # To allow consistent handling of response.
)
facebook_ads_api_retry = FacebookAdsApi.init(
access_token='access_token',
crash_log=False,
retry_strategy=retry_strategy
)

# Arrange - Override the low-level API calls. Make sure we start with a 500 then a 200.
responses.add(responses.GET, self.url, status=http_client.INTERNAL_SERVER_ERROR)
responses.add(responses.GET, self.url, status=http_client.INTERNAL_SERVER_ERROR)
responses.add(responses.GET, self.url, status=http_client.OK)

# Ac
with self.assertRaises(FacebookRequestError):
facebook_ads_api_retry.call(method="GET", path=self.url)


if __name__ == '__main__':
unittest.main()
1 change: 1 addition & 0 deletions requirements-tests.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mock >= 1.0.1
responses >= 0.25.6
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ six >= 1.7.3
curlify >= 2.1.0
pycountry >= 19.8.18
aiohttp; python_version >= '3.5.3'
urllib3 >= 1.9.0 # Minimum version that supports `Retry`.