Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for auth using HTTP Authorization request header #20

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var/
*.egg-info/
.installed.cfg
*.egg
.python-version

# PyInstaller
# Usually these files are written by a python script from a template
Expand Down Expand Up @@ -67,3 +68,6 @@ target/
.py2/
.py3/


# PyCharm
.idea
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# braze-client
A Python client for the Braze REST API

[![Build Status](https://travis-ci.com/GoodRx/braze-client.svg?branch=master)](https://travis-ci.com/GoodRx/braze-client)
[![Coverage](https://codecov.io/gh/GoodRx/braze-client/branch/master/graph/badge.svg)](https://codecov.io/gh/GoodRx/braze-client)
[![Build Status](https://travis-ci.com/dtatarkin/braze-client.svg?branch=master)](https://travis-ci.com/dtatarkin/braze-client)
[![Coverage](https://codecov.io/gh/dtatarkin/braze-client/branch/master/graph/badge.svg)](https://codecov.io/gh/dtatarkin/braze-client)

### How to install

Make sure you have Python 2.7.11+ installed and run:
Make sure you have Python 2.7+ or 3.6+ installed and run:

```
$ git clone https://github.com/GoodRx/braze-client
$ git clone https://github.com/dtatarkin/braze-client
$ cd braze-client
$ python setup.py install
```
Expand All @@ -18,7 +18,7 @@ $ python setup.py install

```python
from braze.client import BrazeClient
client = BrazeClient(api_key='YOUR_API_KEY')
client = BrazeClient(api_key='YOUR_API_KEY', use_auth_header=True)

r = client.user_track(
attributes=[{
Expand All @@ -45,6 +45,7 @@ For more examples, check `examples.py`.

### How to test

To run the unit tests, make sure you have the [tox](https://tox.readthedocs.io/en/latest/) module installed and run the following from the repository root directory:
To run the unit tests, make sure you have the [tox](https://tox.readthedocs.io/en/latest/) module installed
and run the following from the repository root directory:

`$ tox`
25 changes: 19 additions & 6 deletions braze/client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import time

import requests
from tenacity import retry
from tenacity import stop_after_attempt
from tenacity import wait_random_exponential
import time

DEFAULT_API_URL = "https://rest.iad-02.braze.com"
USER_TRACK_ENDPOINT = "/users/track"
Expand Down Expand Up @@ -73,7 +72,7 @@ def check(retry_state):

class BrazeClient(object):
"""
Client for Appboy public API. Support user_track.
Client for Braze public API. Support user_track.
usage:
from braze.client import BrazeClient
client = BrazeClient(api_key='Place your API key here')
Expand All @@ -96,10 +95,12 @@ class BrazeClient(object):
print r['errors']
"""

def __init__(self, api_key, api_url=None):
def __init__(self, api_key, api_url=None, use_auth_header=True):
self.api_key = api_key
self.api_url = api_url or DEFAULT_API_URL
self.use_auth_header = use_auth_header
self.session = requests.Session()
self.session.headers.update({"User-Agent": "braze-client python"})
self.request_url = ""

def user_track(self, attributes=None, events=None, purchases=None):
Expand Down Expand Up @@ -187,7 +188,8 @@ def user_export(self, external_ids=None, email=None, fields_to_export=None):

def __create_request(self, payload):

payload["api_key"] = self.api_key
if not self.use_auth_header:
payload["api_key"] = self.api_key

response = {"errors": []}
r = self._post_request_with_retries(payload)
Expand Down Expand Up @@ -221,7 +223,18 @@ def _post_request_with_retries(self, payload):
:param dict payload:
:rtype: requests.Response
"""
r = self.session.post(self.request_url, json=payload, timeout=2)

headers = {}
# Prior to April 2020, API keys would be included as a part of the API request body or within the request URL
# as a parameter. Braze now has updated the way in which we read API keys. API keys are now set with the HTTP
# Authorization request header, making your API keys more secure.
# https://www.braze.com/docs/api/api_key/#how-can-i-use-it
if self.use_auth_header:
headers["Authorization"] = "Bearer {}".format(self.api_key)

r = self.session.post(
self.request_url, json=payload, timeout=2, headers=headers
)
# https://www.braze.com/docs/developer_guide/rest_api/messaging/#fatal-errors
if r.status_code == 429:
reset_epoch_s = float(r.headers.get("X-RateLimit-Reset", 0))
Expand Down
15 changes: 13 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,35 @@
from setuptools import setup

NAME = "braze-client"
VERSION = "2.2.6"
VERSION = "2.3.3"

REQUIRES = ["requests >=2.21.0, <3.0.0", "tenacity >=5.0.0, <6.0.0"]

EXTRAS = {"dev": ["tox"]}

with open("README.md", "r") as fh:
long_description = fh.read()

setup(
name=NAME,
version=VERSION,
description="Braze Python Client",
author_email="azh@hellofresh.com",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/dtatarkin/braze-client",
author_email="mail@dtatarkin.ru",
keywords=["Appboy", "Braze"],
install_requires=REQUIRES,
extras_require=EXTRAS,
packages=find_packages(exclude=("tests",)),
classifiers=[
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
],
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*",
)
37 changes: 29 additions & 8 deletions tests/braze/test_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
from datetime import datetime
from freezegun import freeze_time
import pytest
from pytest import approx
from requests import RequestException
from requests_mock import ANY
from tenacity import Future
from tenacity import RetryCallState
import time
from uuid import uuid4

Expand All @@ -10,13 +17,6 @@
from braze.client import CAMPAIGN_TRIGGER_SCHEDULE_CREATE
from braze.client import MAX_RETRIES
from braze.client import MAX_WAIT_SECONDS
from freezegun import freeze_time
import pytest
from pytest import approx
from requests import RequestException
from requests_mock import ANY
from tenacity import Future
from tenacity import RetryCallState


@pytest.fixture
Expand Down Expand Up @@ -79,6 +79,7 @@ class TestBrazeClient(object):
def test_init(self, braze_client):
assert braze_client.api_key == "API_KEY"
assert braze_client.request_url == ""
assert braze_client.use_auth_header is True

def test_user_track(
self, braze_client, requests_mock, attributes, events, purchases
Expand Down Expand Up @@ -173,7 +174,7 @@ def test_retries_for_rate_limit_errors(

# Ensure the correct wait time is used when rate limited
for i in range(expected_attempts - 1):
assert approx(no_sleep.call_args_list[i][0], reset_delta_seconds)
assert no_sleep.call_args_list[i][0], approx(reset_delta_seconds)

def test_user_export(self, braze_client, requests_mock):
headers = {"Content-Type": "application/json"}
Expand Down Expand Up @@ -245,3 +246,23 @@ def test_standard_case(
assert expected_url == braze_client.request_url
assert response["status_code"] == 201
assert response["message"] == "success"

@pytest.mark.parametrize(
"use_auth_header",
[True, False],
)
def test_auth(self, requests_mock, attributes, use_auth_header):
braze_client = BrazeClient(api_key="API_KEY", use_auth_header=use_auth_header)
headers = {"Content-Type": "application/json"}
mock_json = {"message": "success", "errors": ""}
requests_mock.post(ANY, json=mock_json, status_code=200, headers=headers)

braze_client.user_track(attributes=attributes)
request = requests_mock.last_request
if use_auth_header:
assert "api_key" not in request.json()
assert "Authorization" in request.headers
assert request.headers["Authorization"].startswith("Bearer ")
else:
assert "api_key" in request.json()
assert "Authorization" not in request.headers
5 changes: 3 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from __future__ import absolute_import

from braze.client import BrazeClient
import pytest

from braze.client import BrazeClient


@pytest.fixture
def braze_client():
return BrazeClient(api_key="API_KEY")
return BrazeClient(api_key="API_KEY", use_auth_header=True)


@pytest.fixture(autouse=True)
Expand Down
49 changes: 27 additions & 22 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = style, py27, py37
envlist = style, py27, py37, py38, py39, py310

# Configs
[pytest]
Expand All @@ -9,20 +9,20 @@ addopts = -p no:warnings
[testenv]
deps =
codecov
freezegun == 0.3.11
freezegun
mock
pytest
pytest-cov
pytest-mock
requests-mock >= 1.3, < 2
requests-mock
commands =
pytest {posargs: --cov --cov-report=html}

[testenv:test-coverage]
deps =
coverage>=4.5.0
commands =
coverage report --skip-covered -m --fail-under=100 --include="tests/*" --omit="tests/conftest.py"
coverage report --skip-covered -m --fail-under=100 --include="tests/*" --omit="tests/conftest.py" --omit="./venv/*"


[testenv:py27]
Expand All @@ -35,32 +35,37 @@ basepython = python3.7
deps = {[testenv]deps}
commands = {[testenv]commands}

[testenv:py38]
basepython = python3.8
deps = {[testenv]deps}
commands = {[testenv]commands}

[testenv:py39]
basepython = python3.9
deps = {[testenv]deps}
commands = {[testenv]commands}

[testenv:py310]
basepython = python3.10
deps = {[testenv]deps}
commands = {[testenv]commands}


[testenv:style]
basepython = python3.7
skip_install = true
deps =
flake8 >= 3.0.4
flake8
flake8-docstrings
flake8-comprehensions
flake8-bugbear
{[testenv:format]deps}
isort
black

commands =
; Check style violations
flake8
flake8 --exclude venv,.tox --ignore=D202,D205
; Check that imports are sorted/formatted appropriately
isort --check-only --recursive
isort --extend-skip venv --extend-skip .tox --check-only .
; Check formatting
black --check .


; Run isort and black on a particular file or directory
[testenv:format]
basepython = python3.7
skip_install = true
deps =
isort >= 4.2.14
black
commands =
; Default to the entire codebase
isort --recursive {posargs: --apply}
black {posargs: .}
black --extend-exclude venv --check .