Skip to content

Commit 9368928

Browse files
author
Aaron Gonzales
authored
Credential handling updates (#20)
* add a seperate option for premium accounts to use consumer_key and secret values and inherently generate the bearer token on startup * b64 encode expects a byte string * after some more testing, better understood the required headers * updated docsstrings around credentials allowing consumer key/secrets * updated docs for credentials * simplifying bearer token retrieval * updated base readme with new docs * update version
1 parent 7014785 commit 9368928

File tree

6 files changed

+111
-52
lines changed

6 files changed

+111
-52
lines changed

README.rst

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,23 @@ Credential Handling
4949

5050
The premium and enterprise Search APIs use different authentication
5151
methods and we attempt to provide a seamless way to handle
52-
authentication for all customers.
52+
authentication for all customers. We know credentials can be tricking or
53+
annoying - please read this in its entirety.
5354

5455
Premium clients will require the ``bearer_token`` and ``endpoint``
5556
fields; Enterprise clients require ``username``, ``password``, and
5657
``endpoint``. If you do not specify the ``account_type``, we attempt to
5758
discern the account type and declare a warning about this behavior.
5859

5960
For premium search products, we are using app-only authentication and
60-
the bearer tokens are not delivered with an expiration time. They can be
61-
invalidated. Please see
61+
the bearer tokens are not delivered with an expiration time. You can
62+
provide either: - your application key and secret (the library will
63+
handle bearer-token authentication) - a bearer token that you get
64+
yourself
65+
66+
Many developers might find providing your application key and secret
67+
more straightforward and letting this library manage your bearer token
68+
generation for you. Please see
6269
`here <https://developer.twitter.com/en/docs/basics/authentication/overview/application-only>`__
6370
for an overview of the premium authentication method.
6471

@@ -77,7 +84,8 @@ this:
7784
search_tweets_api:
7885
account_type: premium
7986
endpoint: <FULL_URL_OF_ENDPOINT>
80-
bearer_token: <TOKEN>
87+
consumer_key: <CONSUMER_KEY>
88+
consumer_secret: <CONSUMER_SECRET>
8189
8290
For enterprise customers, the simplest credential file should look like
8391
this:
@@ -109,7 +117,10 @@ line app. An example:
109117
search_tweets_30_day_dev:
110118
account_type: premium
111119
endpoint: <FULL_URL_OF_ENDPOINT>
112-
bearer_token: <TOKEN>
120+
consumer_key: <KEY>
121+
consumer_secret: <SECRET>
122+
(optional) bearer_token: <TOKEN>
123+
113124
114125
search_tweets_30_day_prod:
115126
account_type: premium
@@ -139,6 +150,8 @@ can set the appropriate variables for your product of the following:
139150
export SEARCHTWEETS_PASSWORD=
140151
export SEARCHTWEETS_BEARER_TOKEN=
141152
export SEARCHTWEETS_ACCOUNT_TYPE=
153+
export SEARCHTWEETS_CONSUMER_KEY=
154+
export SEARCHTWEETS_CONSUMER_SECRET=
142155

143156
The ``load_credentials`` function will attempt to find these variables
144157
if it cannot load fields from the YAML file, and it will **overwrite any

docs/source/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@
6666
# built documents.
6767
#
6868
# The short X.Y version.
69-
version = '1.2'
69+
version = '1.3'
7070
# The full version, including alpha/beta/rc tags.
71-
release = '1.2.1'
71+
release = '1.3.0'
7272

7373
# The language for content autogenerated by Sphinx. Refer to documentation
7474
# for a list of supported languages.

examples/credential_handling.ipynb

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@
66
"source": [
77
"# Credential Handling\n",
88
"\n",
9-
"The premium and enterprise Search APIs use different authentication methods and we attempt to provide a seamless way to handle authentication for all customers. \n",
9+
"The premium and enterprise Search APIs use different authentication methods and we attempt to provide a seamless way to handle authentication for all customers. We know credentials can be tricking or annoying - please read this in its entirety.\n",
10+
"\n",
1011
"\n",
1112
"Premium clients will require the `bearer_token` and `endpoint` fields; Enterprise clients require `username`, `password`, and `endpoint`.\n",
1213
"If you do not specify the `account_type`, we attempt to discern the account type and declare a warning about this behavior.\n",
1314
"\n",
14-
"For premium search products, we are using app-only authentication and the bearer tokens are not delivered with an expiration time. They can be invalidated. Please see [here](https://developer.twitter.com/en/docs/basics/authentication/overview/application-only) for an overview of the premium authentication method.\n",
15+
"For premium search products, we are using app-only authentication and the bearer tokens are not delivered with an expiration time. You can provide either:\n",
16+
"- your application key and secret (the library will handle bearer-token authentication)\n",
17+
"- a bearer token that you get yourself\n",
18+
"\n",
19+
"Many developers might find providing your application key and secret more straightforward and letting this library manage your bearer token generation for you. Please see [here](https://developer.twitter.com/en/docs/basics/authentication/overview/application-only) for an overview of the premium authentication method.\n",
1520
"\n",
1621
"We support both YAML-file based methods and environment variables for storing credentials, and provide flexible handling with sensible defaults.\n",
1722
"\n",
@@ -25,7 +30,8 @@
2530
"search_tweets_api:\n",
2631
" account_type: premium\n",
2732
" endpoint: <FULL_URL_OF_ENDPOINT>\n",
28-
" bearer_token: <TOKEN>\n",
33+
" consumer_key: <CONSUMER_KEY>\n",
34+
" consumer_secret: <CONSUMER_SECRET>\n",
2935
"```\n",
3036
"\n",
3137
"For enterprise customers, the simplest credential file should look like this:\n",
@@ -37,14 +43,18 @@
3743
" endpoint: <FULL_URL_OF_ENDPOINT>\n",
3844
" username: <USERNAME>\n",
3945
" password: <PW>\n",
40-
"```\n",
41-
"\n",
46+
"```"
47+
]
48+
},
49+
{
50+
"cell_type": "markdown",
51+
"metadata": {},
52+
"source": [
4253
"By default, this library expects this file at `\"~/.twitter_keys.yaml\"`, but you can pass the relevant location as needed, either with the ``--credential-file`` flag for the command-line app or as demonstrated below in a Python program.\n",
4354
"\n",
4455
"Both above examples require no special command-line arguments or in-program arguments. The credential parsing methods, unless otherwise specified, will look for a YAML key called `search_tweets_api`.\n",
4556
"\n",
4657
"\n",
47-
"\n",
4858
"For developers who have multiple endpoints and/or search products, you can keep all credentials in the same file and specify specific keys to use. `--credential-file-key` specifies this behavior in the command line app. An example:\n",
4959
"\n",
5060
"\n",
@@ -53,7 +63,10 @@
5363
"search_tweets_30_day_dev:\n",
5464
" account_type: premium\n",
5565
" endpoint: <FULL_URL_OF_ENDPOINT>\n",
56-
" bearer_token: <TOKEN>\n",
66+
" consumer_key: <KEY>\n",
67+
" consumer_secret: <SECRET>\n",
68+
" (optional) bearer_token: <TOKEN>\n",
69+
" \n",
5770
" \n",
5871
"search_tweets_30_day_prod:\n",
5972
" account_type: premium\n",
@@ -87,6 +100,8 @@
87100
"export SEARCHTWEETS_PASSWORD=\n",
88101
"export SEARCHTWEETS_BEARER_TOKEN=\n",
89102
"export SEARCHTWEETS_ACCOUNT_TYPE=\n",
103+
"export SEARCHTWEETS_CONSUMER_KEY=\n",
104+
"export SEARCHTWEETS_CONSUMER_SECRET=\n",
90105
"```\n",
91106
"\n",
92107
"The `load_credentials` function will attempt to find these variables if it cannot load fields from the YAML file, and it will **overwrite any credentials from the YAML file that are present as environment variables** if they have been parsed. This behavior can be changed by setting the `load_credentials` parameter `env_overwrite` to `False`.\n",
@@ -97,7 +112,7 @@
97112
},
98113
{
99114
"cell_type": "code",
100-
"execution_count": 1,
115+
"execution_count": 10,
101116
"metadata": {
102117
"collapsed": true
103118
},
@@ -108,7 +123,7 @@
108123
},
109124
{
110125
"cell_type": "code",
111-
"execution_count": 2,
126+
"execution_count": 14,
112127
"metadata": {},
113128
"outputs": [
114129
{
@@ -119,7 +134,7 @@
119134
" 'username': '<MY_USERNAME>'}"
120135
]
121136
},
122-
"execution_count": 2,
137+
"execution_count": 14,
123138
"metadata": {},
124139
"output_type": "execute_result"
125140
}
@@ -132,7 +147,7 @@
132147
},
133148
{
134149
"cell_type": "code",
135-
"execution_count": 3,
150+
"execution_count": 15,
136151
"metadata": {},
137152
"outputs": [
138153
{
@@ -142,7 +157,7 @@
142157
" 'endpoint': 'https://api.twitter.com/1.1/tweets/search/30day/dev.json'}"
143158
]
144159
},
145-
"execution_count": 3,
160+
"execution_count": 15,
146161
"metadata": {},
147162
"output_type": "execute_result"
148163
}
@@ -164,7 +179,7 @@
164179
},
165180
{
166181
"cell_type": "code",
167-
"execution_count": 5,
182+
"execution_count": 16,
168183
"metadata": {},
169184
"outputs": [
170185
{
@@ -183,7 +198,7 @@
183198
" 'username': '<ENV_USERNAME>'}"
184199
]
185200
},
186-
"execution_count": 5,
201+
"execution_count": 16,
187202
"metadata": {},
188203
"output_type": "execute_result"
189204
}

examples/credential_handling.rst

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@ Credential Handling
44

55
The premium and enterprise Search APIs use different authentication
66
methods and we attempt to provide a seamless way to handle
7-
authentication for all customers.
7+
authentication for all customers. We know credentials can be tricking or
8+
annoying - please read this in its entirety.
89

910
Premium clients will require the ``bearer_token`` and ``endpoint``
1011
fields; Enterprise clients require ``username``, ``password``, and
1112
``endpoint``. If you do not specify the ``account_type``, we attempt to
1213
discern the account type and declare a warning about this behavior.
1314

1415
For premium search products, we are using app-only authentication and
15-
the bearer tokens are not delivered with an expiration time. They can be
16-
invalidated. Please see
16+
the bearer tokens are not delivered with an expiration time. You can
17+
provide either: - your application key and secret (the library will
18+
handle bearer-token authentication) - a bearer token that you get
19+
yourself
20+
21+
Many developers might find providing your application key and secret
22+
more straightforward and letting this library manage your bearer token
23+
generation for you. Please see
1724
`here <https://developer.twitter.com/en/docs/basics/authentication/overview/application-only>`__
1825
for an overview of the premium authentication method.
1926

@@ -33,7 +40,8 @@ this:
3340
search_tweets_api:
3441
account_type: premium
3542
endpoint: <FULL_URL_OF_ENDPOINT>
36-
bearer_token: <TOKEN>
43+
consumer_key: <CONSUMER_KEY>
44+
consumer_secret: <CONSUMER_SECRET>
3745
3846
For enterprise customers, the simplest credential file should look like
3947
this:
@@ -67,7 +75,10 @@ line app. An example:
6775
search_tweets_30_day_dev:
6876
account_type: premium
6977
endpoint: <FULL_URL_OF_ENDPOINT>
70-
bearer_token: <TOKEN>
78+
consumer_key: <KEY>
79+
consumer_secret: <SECRET>
80+
(optional) bearer_token: <TOKEN>
81+
7182
7283
search_tweets_30_day_prod:
7384
account_type: premium
@@ -98,6 +109,8 @@ can set the appropriate variables for your product of the following:
98109
export SEARCHTWEETS_PASSWORD=
99110
export SEARCHTWEETS_BEARER_TOKEN=
100111
export SEARCHTWEETS_ACCOUNT_TYPE=
112+
export SEARCHTWEETS_CONSUMER_KEY=
113+
export SEARCHTWEETS_CONSUMER_SECRET=
101114

102115
The ``load_credentials`` function will attempt to find these variables
103116
if it cannot load fields from the YAML file, and it will **overwrite any

searchtweets/credentials.py

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111
import os
1212
import logging
1313
import yaml
14+
import requests
15+
import base64
1416
from .utils import merge_dicts
1517

18+
OAUTH_ENDPOINT = 'https://api.twitter.com/oauth2/token'
19+
1620
__all__ = ["load_credentials"]
1721

1822
logger = logging.getLogger(__name__)
@@ -45,7 +49,10 @@ def _load_env_credentials():
4549
"SEARCHTWEETS_USERNAME",
4650
"SEARCHTWEETS_PASSWORD",
4751
"SEARCHTWEETS_BEARER_TOKEN",
48-
"SEARCHTWEETS_ACCOUNT_TYPE"]
52+
"SEARCHTWEETS_ACCOUNT_TYPE",
53+
"SEARCHTWEETS_CONSUMER_KEY",
54+
"SEARCHTWEETS_CONSUMER_SECRET"
55+
]
4956
renamed = [var.replace("SEARCHTWEETS_", '').lower() for var in vars_]
5057

5158
parsed = {r: os.environ.get(var) for (var, r) in zip(vars_, renamed)}
@@ -76,8 +83,16 @@ def _parse_credentials(search_creds, account_type):
7683

7784
try:
7885
if account_type == "premium":
79-
search_args = {"bearer_token": search_creds["bearer_token"],
80-
"endpoint": search_creds["endpoint"]}
86+
if "bearer_token" not in search_creds:
87+
if "consumer_key" in search_creds \
88+
and "consumer_secret" in search_creds:
89+
search_creds["bearer_token"] = _generate_bearer_token(
90+
search_creds["consumer_key"],
91+
search_creds["consumer_secret"])
92+
93+
search_args = {
94+
"bearer_token": search_creds["bearer_token"],
95+
"endpoint": search_creds["endpoint"]}
8196
if account_type == "enterprise":
8297
search_args = {"username": search_creds["username"],
8398
"password": search_creds["password"],
@@ -96,35 +111,21 @@ def load_credentials(filename=None, account_type=None,
96111
"""
97112
Handles credential management. Supports both YAML files and environment
98113
variables. A YAML file is preferred for simplicity and configureability.
99-
A YAML credential file should look like this:
114+
A YAML credential file should look something like this:
100115
101116
.. code:: yaml
102117
103118
<KEY>:
104119
endpoint: <FULL_URL_OF_ENDPOINT>
105120
username: <USERNAME>
106121
password: <PW>
122+
consumer_key: <KEY>
123+
consumer_secret: <SECRET>
107124
bearer_token: <TOKEN>
108125
account_type: <enterprise OR premium>
109126
110127
with the appropriate fields filled out for your account. The top-level key
111-
defaults to ``search_tweets_api`` but can be flexible, e.g.:
112-
113-
.. code:: yaml
114-
115-
premium_dev:
116-
account_type: premium
117-
endpoint: <FULL_URL_OF_ENDPOINT>
118-
bearer_token: <TOKEN>
119-
120-
enterprise_dev:
121-
account_type: enterprise
122-
endpoint: <FULL_URL_OF_ENDPOINT>
123-
username: <MY_USERNAME>
124-
password: <MY_PASSWORD>
125-
126-
as this method supports a flexible interface for reading the
127-
credential files. You can keep all of your credentials in the same file.
128+
defaults to ``search_tweets_api`` but can be flexible.
128129
129130
If a YAML file is not found or is missing keys, this function will check
130131
for this information in the environment variables that correspond to
@@ -136,9 +137,10 @@ def load_credentials(filename=None, account_type=None,
136137
SEARCHTWEETS_PASSWORD
137138
SEARCHTWEETS_BEARER_TOKEN
138139
SEARCHTWEETS_ACCOUNT_TYPE
140+
...
139141
140142
Again, set the variables that correspond to your account information and
141-
type.
143+
type. See the main documentation for details and more examples.
142144
143145
144146
Args:
@@ -162,12 +164,12 @@ def load_credentials(filename=None, account_type=None,
162164
dict_keys(['bearer_token', 'endpoint'])
163165
>>> import os
164166
>>> os.environ["SEARCHTWEETS_ENDPOINT"] = "https://endpoint"
165-
>>> os.environ["SEARCHTWEETS_USERNAME"] = "azaleszzz"
167+
>>> os.environ["SEARCHTWEETS_USERNAME"] = "areallybadpassword"
166168
>>> os.environ["SEARCHTWEETS_PASSWORD"] = "<PW>"
167169
>>> load_credentials()
168170
{'endpoint': 'https://endpoint',
169171
'password': '<PW>',
170-
'username': 'azaleszzz'}
172+
'username': 'areallybadpassword'}
171173
172174
"""
173175
yaml_key = yaml_key if yaml_key is not None else "search_tweets_api"
@@ -183,3 +185,19 @@ def load_credentials(filename=None, account_type=None,
183185
else merge_dicts(env_vars, yaml_vars))
184186
parsed_vars = _parse_credentials(merged_vars, account_type=account_type)
185187
return parsed_vars
188+
189+
190+
def _generate_bearer_token(consumer_key, consumer_secret):
191+
"""
192+
Return the bearer token for a given pair of consumer key and secret values.
193+
"""
194+
data = [('grant_type', 'client_credentials')]
195+
resp = requests.post(OAUTH_ENDPOINT,
196+
data=data,
197+
auth=(consumer_key, consumer_secret))
198+
logger.warning("Grabbing bearer token from OAUTH")
199+
if resp.status_code >= 400:
200+
logger.error(resp.text)
201+
resp.raise_for_status()
202+
203+
return resp.json()['access_token']

0 commit comments

Comments
 (0)