From 67ab18a245ebd65eacae223c4206b461986af0e0 Mon Sep 17 00:00:00 2001 From: Dmitriy Tatarkin Date: Tue, 22 Dec 2020 20:01:10 +0300 Subject: [PATCH 1/5] Add support for auth using HTTP Authorization request header --- .gitignore | 3 +++ README.md | 2 +- braze/client.py | 19 ++++++++++++++++--- tests/braze/test_client.py | 21 +++++++++++++++++++++ tests/conftest.py | 2 +- tox.ini | 19 ++++--------------- 6 files changed, 46 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 86d70d9..16ec057 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ target/ .py2/ .py3/ + +# PyCharm +.idea diff --git a/README.md b/README.md index 8c32c51..f3967cd 100644 --- a/README.md +++ b/README.md @@ -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=[{ diff --git a/braze/client.py b/braze/client.py index 2f3c3fb..a3281a9 100644 --- a/braze/client.py +++ b/braze/client.py @@ -96,9 +96,10 @@ 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=False): 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.request_url = "" @@ -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/tests/braze/test_client.py b/tests/braze/test_client.py index 20998bc..ef7d566 100644 --- a/tests/braze/test_client.py +++ b/tests/braze/test_client.py @@ -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 @@ -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..61a7370 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ @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..4e10ca8 100644 --- a/tox.ini +++ b/tox.ini @@ -43,24 +43,13 @@ deps = flake8-docstrings flake8-comprehensions flake8-bugbear - {[testenv:format]deps} + isort < 5.0.0 + black + commands = ; Check style violations - flake8 + flake8 --ignore=D202,D205 ; Check that imports are sorted/formatted appropriately isort --check-only --recursive ; 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: .} From 02c39aeda5fcdb9e7ebc08289b9fb081e51f1806 Mon Sep 17 00:00:00 2001 From: Dmitriy Tatarkin Date: Mon, 28 Dec 2020 16:04:18 +0300 Subject: [PATCH 2/5] Release v2.3.1 --- README.md | 8 ++++---- setup.py | 12 ++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f3967cd..6761fe8 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/GoodRx/dtatarkin/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 ``` diff --git a/setup.py b/setup.py index c111818..6666325 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.1" REQUIRES = ["requests >=2.21.0, <3.0.0", "tenacity >=5.0.0, <6.0.0"] EXTRAS = {"dev": ["tox"]} +with open("README.md", "r", encoding="utf-8") 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,8 @@ classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", ], + python_requires=">=3.6", ) From 0e1537832996b1099d21f8ca7722edbac9da684c Mon Sep 17 00:00:00 2001 From: Dmitriy Tatarkin Date: Fri, 5 Aug 2022 19:18:05 +0300 Subject: [PATCH 3/5] Add support for Python 3.8, 3.9 and 3.10 Make `use_auth_header` default to True --- .gitignore | 1 + README.md | 5 +++-- braze/client.py | 7 +++---- setup.py | 9 ++++++--- tests/braze/test_client.py | 16 ++++++++-------- tests/conftest.py | 3 ++- tox.ini | 34 +++++++++++++++++++++++++--------- 7 files changed, 48 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 16ec057..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 diff --git a/README.md b/README.md index 6761fe8..f2c68cc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Python client for the Braze REST API [![Build Status](https://travis-ci.com/dtatarkin/braze-client.svg?branch=master)](https://travis-ci.com/dtatarkin/braze-client) -[![Coverage](https://codecov.io/gh/GoodRx/dtatarkin/branch/master/graph/badge.svg)](https://codecov.io/gh/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 @@ -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 a3281a9..1676060 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,7 +95,7 @@ class BrazeClient(object): print r['errors'] """ - def __init__(self, api_key, api_url=None, use_auth_header=False): + 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 diff --git a/setup.py b/setup.py index 6666325..6404b11 100644 --- a/setup.py +++ b/setup.py @@ -2,13 +2,13 @@ from setuptools import setup NAME = "braze-client" -VERSION = "2.3.1" +VERSION = "2.3.2" REQUIRES = ["requests >=2.21.0, <3.0.0", "tenacity >=5.0.0, <6.0.0"] EXTRAS = {"dev": ["tox"]} -with open("README.md", "r", encoding="utf-8") as fh: +with open("README.md", "r") as fh: long_description = fh.read() setup( @@ -28,6 +28,9 @@ "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=">=3.6", + 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 ef7d566..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 @@ -174,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"} diff --git a/tests/conftest.py b/tests/conftest.py index 61a7370..c129608 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,9 @@ from __future__ import absolute_import -from braze.client import BrazeClient import pytest +from braze.client import BrazeClient + @pytest.fixture def braze_client(): diff --git a/tox.ini b/tox.ini index 4e10ca8..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,21 +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 - isort < 5.0.0 + isort black commands = ; Check style violations - flake8 --ignore=D202,D205 + 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 . + black --extend-exclude venv --check . From 8aeb4aea3a0d9aa53cc8f81c5d99ed616d7e2f3e Mon Sep 17 00:00:00 2001 From: Derek Schultz Date: Wed, 26 Jan 2022 11:39:38 -0500 Subject: [PATCH 4/5] Set user agent --- braze/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/braze/client.py b/braze/client.py index 1676060..5195b4f 100644 --- a/braze/client.py +++ b/braze/client.py @@ -100,6 +100,7 @@ def __init__(self, api_key, api_url=None, use_auth_header=True): 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): From 4d8e02d3693ffb63be8a7442c47013bf046661b7 Mon Sep 17 00:00:00 2001 From: Dmitriy Tatarkin Date: Fri, 23 Sep 2022 18:19:31 +0300 Subject: [PATCH 5/5] Fix python_requires in setup.py --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 6404b11..a23cac6 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup NAME = "braze-client" -VERSION = "2.3.2" +VERSION = "2.3.3" REQUIRES = ["requests >=2.21.0, <3.0.0", "tenacity >=5.0.0, <6.0.0"] @@ -32,5 +32,5 @@ "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.*", + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*", )