Exemplo n.º 1
0
def get_device_details(accountId,
                       apiKey,
                       email,
                       deviceIdType,
                       deviceId,
                       verbose=False):
    """Gets and prints details for a device.
    Parameters
    ----------
    accountId: str
        The account ID that owns the device
    apiKey: str
        An API Key for the account ID
    email: str
        The email address of the user making this request
    deviceIdType: str
        The type of device ID to query. See
        https://aeriscom.zendesk.com/hc/en-us/articles/360037274334-AerAdmin-5-0-REST-API-Specification for possible
        values.
    deviceId: str
        The ID of the device to query
    verbose: bool, optional
        True if you want extra output printed

    Returns
    -------
    dict
        A dict representing the device's details.

    Raises
    ------
    ApiException
        if there was a problem.
    """
    endpoint = get_endpoint() + 'devices/details'
    payload = {"accountID": accountId, "email": email, deviceIdType: deviceId}
    myparams = {"apiKey": apiKey}
    r = requests.post(endpoint, params=myparams, json=payload)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 200:
        device_details = json.loads(r.text)
        print('Device details:\n' + json.dumps(device_details, indent=4))
        result_code = None
        if 'resultCode' in device_details:
            result_code = device_details['resultCode']
        if result_code == 0:
            return device_details

        raise ApiException('Bad (or missing) resultCode: ' + str(result_code),
                           r)
    else:
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was not 200', r)
Exemplo n.º 2
0
def get_device_network_details(accountId,
                               apiKey,
                               email,
                               deviceIdType,
                               deviceId,
                               verbose=False):
    """Gets and prints details about a device's network attributes (e.g., last registration time)
    Parameters
    ----------
    accountId: str
    apiKey: str
    email: str
    deviceIdType: str
        The type of device ID to query. See
        https://aeriscom.zendesk.com/hc/en-us/articles/360037274334-AerAdmin-5-0-REST-API-Specification for possible
        values.
    deviceId: str
    verbose: bool, optional

    Returns
    -------
    dict
        A dict representing the network details of the device.

    Raises
    ------
    ApiException
        if there was a problem with the API.
    """
    endpoint = get_endpoint() + 'devices/network/details'
    payload = {
        "accountID": accountId,
        "apiKey": apiKey,
        "email": email,
        deviceIdType: deviceId
    }
    print("Payload: " + str(payload))
    r = requests.get(endpoint, params=payload)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 200:
        network_details = json.loads(r.text)
        print('Network details:\n' + json.dumps(network_details, indent=4))
        result_code = None
        if 'resultCode' in network_details:
            result_code = network_details['resultCode']
        if result_code == 0:
            return network_details
        raise ApiException('Bad (or missing) resultCode: ' + str(result_code),
                           r)
    else:
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was not 200', r)
Exemplo n.º 3
0
def get_channel(accountId, apiKey, channelId, verbose=False):
    """Gets details of a channel.

    Parameters
    ----------
    accountId: str
    apiKey: str
    channelId: str
    verbose: bool, optional

    Returns
    -------
    dict
        A dict containing the channel configuration details, or None if the channel was not found

    Raises
    ------
    ApiException
        if there was another problem with the API
    """
    endpoint = get_channel_endpoint(accountId, channelId)
    myparams = {'apiKey': apiKey}
    r = requests.get(endpoint, params=myparams)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 200:
        channelConfig = json.loads(r.text)
        aerisutils.vprint(verbose, json.dumps(channelConfig))
        return channelConfig
    elif r.status_code == 404:
        aerisutils.print_http_error(r)
        return None
    else:
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was ' + str(r.status_code), r)
Exemplo n.º 4
0
def get_application_by_app_id(accountId, apiKey, appId, verbose=False):
    """Gets a specific registered application

    Parameters
    ----------
    accountId : str
        String version of the numerical account ID
    apiKey : str
        String version of the GUID API Key. Can be found in AerPort / Quicklinks / API Keys
    appId : str
        String version of the GUID app ID returned by the create_application call
    verbose : bool
        True to enable verbose printing

    Returns
    -------
    dict
        A dictionary containing configuration information for this application

    """
    endpoint = get_application_endpoint(accountId, appId)  # Get app endpoint based on account ID and appID
    myparams = {'apiKey': apiKey}
    r = requests.get(endpoint, params=myparams)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 200:
        appConfig = json.loads(r.text)
        aerisutils.vprint(verbose, json.dumps(appConfig))
        return appConfig
    else:
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was ' + str(r.status_code), r)
Exemplo n.º 5
0
def delete_channel(accountId, apiKey, channelId, verbose=False):
    """Deletes a channel.

    Parameters
    ----------
    accountId: str
    apiKey: str
    channelId: str
        the channel ID to delete
    verbose: bool, optional

    Returns
    -------
    bool
        True if the channel was deleted, False if the channel did not exist

    Raises
    ------
    ApiException
        if there was a problem with the API.
    """
    endpoint = get_channel_endpoint(accountId, channelId)
    myparams = {"apiKey": apiKey}
    r = requests.delete(endpoint, params=myparams)
    if r.status_code == 204:  # Check for 'no content' http response
        print('Channel successfully deleted.')
        return True
    elif r.status_code == 404:  # Check if no matching channel ID
        print('Channel ID does not match current application.')
        return False
    else:
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was ' + str(r.status_code), r)
Exemplo n.º 6
0
def send_mt_sms(accountId, apiKey, appShortName, imsiDestination, smsText, verbose=False):
    """Sends a Mobile-Terminated Short Message (MT-SM) to a device.

    Parameters
    ----------
    accountId: str
        The account ID that owns the destination device.
    apiKey: str
        An API key for the account.
    appShortName: str
        The application short name.
    imsiDestination: str
        The IMSI of the destination device.
    smsText: str
        The text payload to send to the device.
    verbose: bool, optional
        True to enable verbose printing.

    Returns
    -------
    dict
        A dict containing AerFrame's response, or None if the device was not found or does not support SMS.

    Raises
    ------
    ApiException
        if there was another problem with the API.
    """
    url = aerisconfig.get_aerframe_api_url()
    endpoint = f'{url}/smsmessaging/v2/{accountId}/outbound/{appShortName}/requests'
    address = [imsiDestination]
    outboundSMSTextMessage = {"message": smsText}
    payload = {'address': address,
               'senderAddress': appShortName,
               'outboundSMSTextMessage': outboundSMSTextMessage,
               'clientCorrelator': '123456',
               'senderName': appShortName}
    myparams = {"apiKey": apiKey}
    # print('Payload: \n' + json.dumps(payload, indent=4))
    r = requests.post(endpoint, params=myparams, json=payload)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 201:  # In this case, we get a 201 'created' for success
        sendsmsresponse = json.loads(r.text)
        print('Sent SMS:\n' + json.dumps(sendsmsresponse, indent=4))
        return sendsmsresponse
    elif r.status_code == 404:  # Check if no matching device IMSI or IMSI not support SMS
        print('IMSI is not found or does not support SMS.')
        print(r.text)
        return None
    else:
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was ' + str(r.status_code), r)
Exemplo n.º 7
0
def get_channel_id_by_tag(accountId, apiKey, searchAppTag, verbose=False):
    """Gets a channel's ID by its application tag. If there are multiple channels with the same application tag, returns
    only one of them.
    Parameters
    ----------
    accountId: str
    apiKey: str
    searchAppTag: str
        the application tag that the channel was created with
    verbose: bool

    Returns
    -------
    str
        The ID of a channel that has the same application tag as "searchAppTag", or None if none were found

    Raises
    ------
    ApiException
        if there was an API problem.

    """
    endpoint = get_channel_endpoint(accountId)
    myparams = {'apiKey': apiKey}
    r = requests.get(endpoint, params=myparams)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 200:
        channels = json.loads(r.text)
        aerisutils.vprint(verbose, json.dumps(channels['notificationChannel'], indent=4))  # Print formatted json
        searchAppTagExists = False
        searchAppTagId = None
        sdkchannel = None
        for channel in channels['notificationChannel']:  # Iterate channels to try and find sdk application
            if channel['applicationTag'] == searchAppTag:
                searchAppTagExists = True
                sdkchannel = channel
                searchAppTagId = channel['resourceURL'].split('/channels/', 1)[1]
        if searchAppTagExists:
            print(searchAppTag + ' channel exists. Channel ID: ' + searchAppTagId)
            aerisutils.vprint(verbose, 'Channel config: ' + json.dumps(sdkchannel, indent=4))
            return searchAppTagId
        else:
            print(searchAppTag + ' channel does not exist')
            return searchAppTagId
    else:  # Response code was not 200
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was ' + str(r.status_code), r)
Exemplo n.º 8
0
def get_applications(accountId, apiKey, searchAppShortName, verbose=False):
    """Gets a list of all registered applications for the account.

    Parameters
    ----------
    accountId : str
        String version of the numerical account ID
    apiKey : str
        String version of the GUID API Key. Can be found in AerPort / Quicklinks / API Keys
    searchAppShortName : str
        String short name of the application to search for
    verbose : bool, optional
        True to enable verbose printing

    Returns
    -------
    str
        String version of the GUID app ID for the app short name passed in or None if no match found

    Raises
    ------
    ApiException
        if there was a problem

    """
    endpoint = get_application_endpoint(accountId)  # Get app endpoint based on account ID
    myparams = {'apiKey': apiKey}
    r = requests.get(endpoint, params=myparams)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 200:
        apps = json.loads(r.text)
        aerisutils.vprint(verbose, json.dumps(apps['application'], indent=4))  # Print formatted json
        searchAppShortNameExists = False
        searchAppShortNameId = None
        for app in apps['application']:  # Iterate applications to try and find application we are looking for
            if app['applicationShortName'] == searchAppShortName:
                searchAppShortNameExists = True
                searchAppShortNameId = app['resourceURL'].split('/applications/', 1)[1]
        if searchAppShortNameExists:
            print(searchAppShortName + ' application exists. Application ID: ' + searchAppShortNameId)
            return searchAppShortNameId
        else:
            print(searchAppShortName + ' application does not exist')
            return searchAppShortNameId
    else:  # Response code was not 200
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was ' + str(r.status_code), r)
Exemplo n.º 9
0
def get_inbound_subscription_by_app_short_name(accountId, appApiKey, appShortName, verbose=False):
    """
    Prints and returns the subscription ID of the first inbound subscription for the application given by appShortName

    Parameters
    ----------
    accountId: str
        The account ID that owns the application
    appApiKey: str
        The application API key for the application
    appShortName: str
        The short name of the application
    verbose: bool
        True to print verbose output.

    Returns
    -------
    str
       The ID of the first subscription found, or None if no subscriptions were found for the application.

    Raises
    ------
    ApiException
        if there was another problem with the API.
    """
    endpoint = aerisconfig.get_aerframe_api_url() + '/smsmessaging/v2/' + accountId + '/inbound/subscriptions'
    myparams = {'apiKey': appApiKey}
    r = requests.get(endpoint, params=myparams)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 200:
        subscriptions = json.loads(r.text)
        aerisutils.vprint(verbose, json.dumps(subscriptions['subscription'], indent=4))  # Print formatted json
        if 'subscription' not in subscriptions.keys():
            print(f'No inbound subscriptions for application short name {appShortName}')
            return None
        print('Inbound subscriptions:\n')
        for subscription in subscriptions['subscription']:  # Iterate subscriptions to try and find sdk application
            print(subscription['destinationAddress'])
            if appShortName in subscription['destinationAddress']:
                subscription_id = subscription['resourceURL'].split('/')[-1]
                print(appShortName + ' inbound subscription ID: ' + subscription_id)
                return subscription_id
    else:  # Response code was not 200
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was: ' + str(r.status_code), r)
Exemplo n.º 10
0
def create_outbound_subscription(accountId, appApiKey, appShortName, appChannelId, verbose=False):
    """Creates an outbound subscription.

    Parameters
    ----------
    accountId: str
        The account ID that owns the application identified by appShortName
    appApiKey: str
        The API key of the application identified by appShortName
    appShortName: str
        The short name of an application
    appChannelId: str
        The ID of a notification channel associated with the application identified by appShortName
    verbose: bool, optional
        True to print verbose output.
    Returns
    -------
    dict
        A dict containing the subscription configuration, including the subscription ID.

    Raises
    ------
    An ApiException if there was a problem.
    """
    url = aerisconfig.get_aerframe_api_url()
    endpoint = url + '/smsmessaging/v2/' + accountId + '/outbound/' + appShortName + '/subscriptions'
    callbackReference = {
        'callbackData': appShortName + '-mt',
        'notifyURL': aerisconfig.get_aerframe_api_url() + '/notificationchannel/v2/'
        + accountId + '/channels/' + appChannelId + '/callback'
    }
    payload = {'callbackReference': callbackReference,
               'filterCriteria': 'SP:*',  # Could use SP:Aeris as example of service profile
               'destinationAddress': [appShortName]}
    myparams = {"apiKey": appApiKey}
    r = requests.post(endpoint, params=myparams, json=payload)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 201:  # In this case, we get a 201 'created' for success
        subscriptionConfig = json.loads(r.text)
        print('Created outbound (MT-DR) subscription for ' + appShortName)
        aerisutils.vprint(verbose, 'Subscription info:\n' + json.dumps(subscriptionConfig, indent=4))
        return subscriptionConfig
    else:
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was: ' + str(r.status_code), r)
Exemplo n.º 11
0
def get_outbound_subscription_id_by_app_short_name(accountId, appApiKey, appShortName, verbose=False):
    """Gets the Subscription ID of the first outbound subscription for the application given by appShortName

    Parameters
    ----------
    accountId: str
        The account ID that owns the application
    appApiKey: str
        The application API key for the application
    appShortName: str
        The short name of the application
    verbose: bool
        True to print verbose output.

    Returns
    -------
    str
       The ID of the first subscription found, or None if no subscriptions were found for the application.

    Raises
    ------
    ApiException
        if there was another problem with the API.

    """
    url = aerisconfig.get_aerframe_api_url()
    endpoint = url + '/smsmessaging/v2/' + accountId + '/outbound/' + appShortName + '/subscriptions'
    myparams = {'apiKey': appApiKey}
    r = requests.get(endpoint, params=myparams)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 200:
        subscriptions = json.loads(r.text)
        if 'deliveryReceiptSubscription' in subscriptions.keys():
            aerisutils.vprint(verbose, appShortName + ' has outbound (MT-DR) subscriptions.' + json.dumps(subscriptions,
                                                                                                          indent=4))
            subscriptionId \
                = subscriptions['deliveryReceiptSubscription'][0]['resourceURL'].split('/subscriptions/', 1)[1]
            print(appShortName + ' outbound subscription ID: ' + subscriptionId)
            return subscriptionId
        else:
            print(appShortName + ' has no outbound (MT-DR) subscriptions.')
            return None
    else:  # Response code was not 200
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was: ' + str(r.status_code), r)
Exemplo n.º 12
0
def create_application(accountId, apiKey, appShortName, appDescription='Application for aerframe sdk', verbose=False):
    """Creates a registered application

    Parameters
    ----------
    appDescription
    accountId : str
        String version of the numerical account ID
    apiKey : str
        String version of the GUID API Key. Can be found in AerPort / Quicklinks / API Keys
    appShortName : str
        String to use for the short name of the application
    verbose : bool, optional
        True to print verbose output

    Returns
    -------
    dict
        A dict containing configuration information for this application

    Raises
    ------
    ApiException
        in case of an API error.
    """
    endpoint = get_application_endpoint(accountId)  # Get app endpoint based on account ID
    payload = {'applicationName': appShortName,
               'description': appDescription,
               'applicationShortName': appShortName,
               'applicationTag': appShortName}
    myparams = {"apiKey": apiKey}
    r = requests.post(endpoint, params=myparams, json=payload)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 201:  # Check for 'created' http response
        appConfig = json.loads(r.text)
        print('Created application ' + appShortName)
        aerisutils.vprint(verbose, 'Application info:\n' + json.dumps(appConfig, indent=4))
        return appConfig
    else:
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was ' + str(r.status_code), r)
Exemplo n.º 13
0
def get_outbound_subscription(accountId, appApiKey, appShortName, subscriptionId, verbose=False):
    """Gets the details of an outbound subscription, given its subscription ID
    and the short name of the associated application.

    Parameters
    ----------
    accountId: str
        The account ID that owns the application
    appApiKey: str
        The application API key for the application
    appShortName: str
        The short name of the application
    subscriptionId: str
        The ID of the subscription
    verbose: bool, optional
        True to print verbose output.

    Returns
    -------
    dict
        A dict containing details of the subscription, or None if no subscription was found.

    Raises
    ------
    ApiException
        if there was a problem.
    """
    url = aerisconfig.get_aerframe_api_url()
    endpoint = url + '/smsmessaging/v2/' + accountId + '/outbound/' + appShortName + '/subscriptions/' + subscriptionId
    myparams = {'apiKey': appApiKey}
    r = requests.get(endpoint, params=myparams)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 200:
        subscription = json.loads(r.text)
        return subscription
    if r.status_code == 404:
        return None
    else:  # Response code was not 200
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was: ' + str(r.status_code), r)
Exemplo n.º 14
0
def delete_outbound_subscription(accountId, appApiKey, appShortName, subscriptionId, verbose=False):
    """Deletes an outbound subscription.

    Parameters
    ----------
    accountId: str
        The account ID that owns the application.
    appApiKey: str
        The API key of the application.
    appShortName: str
        The short name of the application.
    subscriptionId: str
        The ID of the subscription to delete.
    verbose: bool, optional
        True to print verbose output.

    Returns
    -------
    bool, optional
        True if the subscription was deleted, or False if no such subscription was found.

    Raises
    ------
    ApiException
        if there was another problem.
    """
    url = aerisconfig.get_aerframe_api_url()
    endpoint = url + '/smsmessaging/v2/' + accountId + '/outbound/' + appShortName + '/subscriptions/' + subscriptionId
    myparams = {"apiKey": appApiKey}
    r = requests.delete(endpoint, params=myparams)
    if r.status_code == 204:  # Check for 'no content' http response
        print('Subscription successfully deleted.')
        return True
    elif r.status_code == 404:  # Check if no matching subscription ID
        print('Subscription ID does not match current application.')
        return False
    else:
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was: ' + str(r.status_code), r)
Exemplo n.º 15
0
def delete_application(accountId, apiKey, appId, verbose=False):
    """Deletes a registered application

    Parameters
    ----------
    accountId : str
        String version of the numerical account ID
    apiKey : str
        String version of the GUID API Key. Can be found in AerPort / Quicklinks / API Keys
    appId : str
        String version of the GUID app ID returned by the create_application call
    verbose : bool, optional
        True to print verbose output; currently unused.

    Returns
    -------
    bool
        True if successfully deleted
        False if the application did not exist

    Raises
    ------
    ApiException
        if there was a problem

    """
    endpoint = get_application_endpoint(accountId, appId)  # Get app endpoint based on account ID and appID
    myparams = {"apiKey": apiKey}
    r = requests.delete(endpoint, params=myparams)
    if r.status_code == 204:  # Check for 'no content' http response
        print('Application successfully deleted.')
        return True
    elif r.status_code == 404:  # Check if no matching app ID
        print('Application ID does not match current application.')
        return False
    else:
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was ' + str(r.status_code), r)
Exemplo n.º 16
0
def create_channel(accountId, apiKey, applicationTag, verbose=False):
    """Creates a channel

    Parameters
    ----------
    accountId: str
    apiKey: str
    applicationTag: str
        a tag for this channel
    verbose: bool, optional

    Returns
    -------
    dict
        A dict containing the channel configuration.

    Raises
    ------
    ApiException
        if there was a problem
    """
    endpoint = get_channel_endpoint(accountId)
    channelData = {'maxNotifications': '15',
                   'type': 'nc:LongPollingData'}
    payload = {'applicationTag': applicationTag,
               'channelData': channelData,
               'channelType': 'LongPolling'}
    myparams = {"apiKey": apiKey}
    r = requests.post(endpoint, params=myparams, json=payload)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 200:  # In this case, we get a 200 for success rather than 201 like for application
        channelConfig = json.loads(r.text)
        print('Created notification channel for ' + applicationTag)
        aerisutils.vprint(verbose, 'Notification channel info:\n' + json.dumps(channelConfig, indent=4))
        return channelConfig
    else:
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was ' + str(r.status_code), r)
Exemplo n.º 17
0
def get_location(accountId, apiKey, deviceIdType, deviceId, verbose=False):
    """Gets information about the location of a device.

    Parameters
    ----------
    accountId: str
        The account ID that owns the device.
    apiKey: str
        An API key for the account ID.
    deviceIdType: str
        The type of device ID supplied. Must be 'MSISDN' or 'IMSI'
    deviceId: str
        The device ID.
    verbose: bool, optional
        True to verbosely print output.

    Returns
    -------
    dict
        representing the device's location.

    Raises
    ------
    ApiException
        if there was a problem.
    """
    url = aerisconfig.get_aerframe_api_url()
    endpoint = f'{url}/networkservices/v2/{accountId}/devices/{deviceIdType}/{deviceId}/networkLocation'
    myparams = {'apiKey': apiKey}
    r = requests.get(endpoint, params=myparams)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 200:
        locationInfo = json.loads(r.text)
        return locationInfo
    else:  # Response code was not 200
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was ' + str(r.status_code), r)
Exemplo n.º 18
0
def poll_notification_channel(accountId, apiKey, channelURL, verbose=False):
    """
    Polls a notification channel for notifications.

    Parameters
    ----------
    accountId: str
        The account ID that owns the notification channel.
    apiKey: str
        An API key of that account.
    channelURL: str
        The URL of the notification channel to poll. See method ``get_channel`` for details of a notification channel.
    verbose: bool, optional
        True to verbosely print.

    Returns
    -------
    dict
        A dict containing zero or more MT-SM delivery receipts and zero or more MO-SMs.

    Raises
    ------
    ApiException
        if there was a problem.
    """
    myparams = {'apiKey': apiKey}
    print('Polling channelURL for polling interval: ' + channelURL)
    r = requests.get(channelURL, params=myparams)
    aerisutils.vprint(verbose, "Response code: " + str(r.status_code))
    if r.status_code == 200:
        notifications = json.loads(r.text)
        aerisutils.vprint(verbose, 'MO SMS and MT SMS DR:\n' + json.dumps(notifications, indent=4))
        return notifications
    else:  # Response code was not 200
        aerisutils.print_http_error(r)
        raise ApiException('HTTP status code was: ' + str(r.status_code), r)