diff --git a/.gitignore b/.gitignore index 86d70d9..e1455a6 100644 --- a/.gitignore +++ b/.gitignore @@ -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 @@ -67,3 +68,6 @@ target/ .py2/ .py3/ + +# PyCharm +.idea diff --git a/README.md b/README.md index 8c32c51..f2c68cc 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -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=[{ @@ -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` diff --git a/braze/client.py b/braze/client.py index 2f3c3fb..5195b4f 100644 --- a/braze/client.py +++ b/braze/client.py @@ -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" @@ -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') @@ -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): @@ -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) @@ -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)) diff --git a/setup.py b/setup.py index c111818..a23cac6 100644 --- a/setup.py +++ b/setup.py @@ -2,17 +2,23 @@ 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, @@ -20,6 +26,11 @@ 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.*", ) diff --git a/tests/braze/test_client.py b/tests/braze/test_client.py index 20998bc..f56d2ae 100644 --- a/tests/braze/test_client.py +++ b/tests/braze/test_client.py @@ -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 @@ -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 @@ -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 @@ -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"} @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index d12ed9e..c129608 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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) diff --git a/tox.ini b/tox.ini index 32924f5..42e0d2b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = style, py27, py37 +envlist = style, py27, py37, py38, py39, py310 # Configs [pytest] @@ -9,12 +9,12 @@ 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} @@ -22,7 +22,7 @@ commands = 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] @@ -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 .