Skip to content

Commit 3e73fbf

Browse files
committed
Prefer httpx.Auth Instead of api_token
It is still possible to use the api_token parameter. In this case assume an API token was copied from the user profile of a Dataverse instance. If both are specified, an api_token and an explicit auth method, warn the user and use the auth method. Closes #192.
1 parent 0702ee8 commit 3e73fbf

File tree

3 files changed

+123
-34
lines changed

3 files changed

+123
-34
lines changed

pyDataverse/api.py

Lines changed: 91 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from typing import Any, Dict, Optional
55
import httpx
66
import subprocess as sp
7+
from warnings import warn
78

89
from httpx import ConnectError, Response
910

11+
from pyDataverse.auth import ApiTokenAuth
1012
from pyDataverse.exceptions import (
1113
ApiAuthorizationError,
1214
ApiUrlError,
@@ -41,6 +43,8 @@ def __init__(
4143
base_url: str,
4244
api_token: Optional[str] = None,
4345
api_version: str = "latest",
46+
*,
47+
auth: Optional[httpx.Auth] = None,
4448
):
4549
"""Init an Api() class.
4650
@@ -51,17 +55,55 @@ def __init__(
5155
----------
5256
base_url : str
5357
Base url for Dataverse api.
54-
api_token : str
55-
Api token for Dataverse api.
56-
58+
api_token : str | None
59+
API token for Dataverse API. If you provide an :code:`api_token`, we
60+
assume it is an API token as retrieved via your Dataverse instance
61+
user profile.
62+
We recommend using the :code:`auth` argument instead.
63+
To retain the current behaviour with the :code:`auth` argument, change
64+
65+
.. code-block:: python
66+
67+
Api("https://demo.dataverse.org", "my_token")
68+
69+
to
70+
71+
.. code-block:: python
72+
73+
from pyDataverse.auth import ApiTokenAuth
74+
75+
Api("https://demo.dataverse.org", auth=ApiTokenAuth("my_token"))
76+
77+
If you are using an OIDC/OAuth 2.0 Bearer token, please use the :code:`auth`
78+
parameter with the :py:class:`.auth.BearerTokenAuth`.
79+
api_version : str
80+
The version string of the Dataverse API or :code:`latest`, e.g.,
81+
:code:`v1`. Defaults to :code:`latest`, which drops the version from
82+
the API urls.
83+
auth : httpx.Auth | None
84+
You can provide any authentication mechanism you like to connect to
85+
your Dataverse instance. The most common mechanisms are implemented
86+
in :py:mod:`.auth`, but if one is missing, you can use your own
87+
`httpx.Auth`-compatible class. For more information, have a look at
88+
`httpx' Authentication docs
89+
<https://www.python-httpx.org/advanced/authentication/>`_.
5790
Examples
5891
-------
5992
Create an Api connection::
6093
94+
.. code-block::
95+
6196
>>> from pyDataverse.api import Api
6297
>>> base_url = 'http://demo.dataverse.org'
6398
>>> api = Api(base_url)
6499
100+
.. code-block::
101+
102+
>>> from pyDataverse.api import Api
103+
>>> from pyDataverse.auth import ApiTokenAuth
104+
>>> base_url = 'http://demo.dataverse.org'
105+
>>> api = Api(base_url, ApiTokenAuth('my_api_token'))
106+
65107
"""
66108
if not isinstance(base_url, str):
67109
raise ApiUrlError("base_url {0} is not a string.".format(base_url))
@@ -73,10 +115,19 @@ def __init__(
73115
raise ApiUrlError("api_version {0} is not a string.".format(api_version))
74116
self.api_version = api_version
75117

76-
if api_token:
77-
if not isinstance(api_token, str):
78-
raise ApiAuthorizationError("Api token passed is not a string.")
118+
self.auth = auth
79119
self.api_token = api_token
120+
if api_token is not None:
121+
if auth is None:
122+
self.auth = ApiTokenAuth(api_token)
123+
else:
124+
self.api_token = None
125+
warn(
126+
UserWarning(
127+
"You provided both, an api_token and a custom auth "
128+
"method. We will only use the auth method."
129+
)
130+
)
80131

81132
if self.base_url:
82133
if self.api_version == "latest":
@@ -119,21 +170,21 @@ def get_request(self, url, params=None, auth=False):
119170
Response object of requests library.
120171
121172
"""
122-
params = {}
123-
params["User-Agent"] = "pydataverse"
124-
if self.api_token:
125-
params["key"] = str(self.api_token)
173+
headers = {}
174+
headers["User-Agent"] = "pydataverse"
126175

127176
if self.client is None:
128177
return self._sync_request(
129178
method=httpx.get,
130179
url=url,
180+
headers=headers,
131181
params=params,
132182
)
133183
else:
134184
return self._async_request(
135185
method=self.client.get,
136186
url=url,
187+
headers=headers,
137188
params=params,
138189
)
139190

@@ -162,10 +213,8 @@ def post_request(self, url, data=None, auth=False, params=None, files=None):
162213
Response object of requests library.
163214
164215
"""
165-
params = {}
166-
params["User-Agent"] = "pydataverse"
167-
if self.api_token:
168-
params["key"] = self.api_token
216+
headers = {}
217+
headers["User-Agent"] = "pydataverse"
169218

170219
if isinstance(data, str):
171220
data = json.loads(data)
@@ -175,6 +224,7 @@ def post_request(self, url, data=None, auth=False, params=None, files=None):
175224
method=httpx.post,
176225
url=url,
177226
json=data,
227+
headers=headers,
178228
params=params,
179229
files=files,
180230
)
@@ -183,6 +233,7 @@ def post_request(self, url, data=None, auth=False, params=None, files=None):
183233
method=self.client.post,
184234
url=url,
185235
json=data,
236+
headers=headers,
186237
params=params,
187238
files=files,
188239
)
@@ -208,10 +259,8 @@ def put_request(self, url, data=None, auth=False, params=None):
208259
Response object of requests library.
209260
210261
"""
211-
params = {}
212-
params["User-Agent"] = "pydataverse"
213-
if self.api_token:
214-
params["key"] = self.api_token
262+
headers = {}
263+
headers["User-Agent"] = "pydataverse"
215264

216265
if isinstance(data, str):
217266
data = json.loads(data)
@@ -221,13 +270,15 @@ def put_request(self, url, data=None, auth=False, params=None):
221270
method=httpx.put,
222271
url=url,
223272
json=data,
273+
headers=headers,
224274
params=params,
225275
)
226276
else:
227277
return self._async_request(
228278
method=self.client.put,
229279
url=url,
230280
json=data,
281+
headers=headers,
231282
params=params,
232283
)
233284

@@ -250,21 +301,21 @@ def delete_request(self, url, auth=False, params=None):
250301
Response object of requests library.
251302
252303
"""
253-
params = {}
254-
params["User-Agent"] = "pydataverse"
255-
if self.api_token:
256-
params["key"] = self.api_token
304+
headers = {}
305+
headers["User-Agent"] = "pydataverse"
257306

258307
if self.client is None:
259308
return self._sync_request(
260309
method=httpx.delete,
261310
url=url,
311+
headers=headers,
262312
params=params,
263313
)
264314
else:
265315
return self._async_request(
266316
method=self.client.delete,
267317
url=url,
318+
headers=headers,
268319
params=params,
269320
)
270321

@@ -292,7 +343,7 @@ def _sync_request(
292343
kwargs = self._filter_kwargs(kwargs)
293344

294345
try:
295-
resp = method(**kwargs, follow_redirects=True, timeout=None)
346+
resp = method(**kwargs, auth=self.auth, follow_redirects=True, timeout=None)
296347
if resp.status_code == 401:
297348
error_msg = resp.json()["message"]
298349
raise ApiAuthorizationError(
@@ -335,7 +386,7 @@ async def _async_request(
335386
kwargs = self._filter_kwargs(kwargs)
336387

337388
try:
338-
resp = await method(**kwargs)
389+
resp = await method(**kwargs, auth=self.auth)
339390

340391
if resp.status_code == 401:
341392
error_msg = resp.json()["message"]
@@ -408,9 +459,9 @@ class DataAccessApi(Api):
408459
409460
"""
410461

411-
def __init__(self, base_url, api_token=None):
462+
def __init__(self, base_url, api_token=None, *, auth=None):
412463
"""Init an DataAccessApi() class."""
413-
super().__init__(base_url, api_token)
464+
super().__init__(base_url, api_token, auth=auth)
414465
if base_url:
415466
self.base_url_api_data_access = "{0}/access".format(self.base_url_api)
416467
else:
@@ -628,9 +679,9 @@ class MetricsApi(Api):
628679
629680
"""
630681

631-
def __init__(self, base_url, api_token=None, api_version="latest"):
682+
def __init__(self, base_url, api_token=None, api_version="latest", *, auth=None):
632683
"""Init an MetricsApi() class."""
633-
super().__init__(base_url, api_token, api_version)
684+
super().__init__(base_url, api_token, api_version, auth=auth)
634685
if base_url:
635686
self.base_url_api_metrics = "{0}/api/info/metrics".format(self.base_url)
636687
else:
@@ -729,7 +780,7 @@ class NativeApi(Api):
729780
730781
"""
731782

732-
def __init__(self, base_url: str, api_token=None, api_version="v1"):
783+
def __init__(self, base_url: str, api_token=None, api_version="v1", *, auth=None):
733784
"""Init an Api() class.
734785
735786
Scheme, host and path combined create the base-url for the api.
@@ -741,7 +792,7 @@ def __init__(self, base_url: str, api_token=None, api_version="v1"):
741792
Api version of Dataverse native api. Default is `v1`.
742793
743794
"""
744-
super().__init__(base_url, api_token, api_version)
795+
super().__init__(base_url, api_token, api_version, auth=auth)
745796
self.base_url_api_native = self.base_url_api
746797

747798
def get_dataverse(self, identifier, auth=False):
@@ -2402,9 +2453,9 @@ class SearchApi(Api):
24022453
24032454
"""
24042455

2405-
def __init__(self, base_url, api_token=None, api_version="latest"):
2456+
def __init__(self, base_url, api_token=None, api_version="latest", *, auth=None):
24062457
"""Init an SearchApi() class."""
2407-
super().__init__(base_url, api_token, api_version)
2458+
super().__init__(base_url, api_token, api_version, auth=auth)
24082459
if base_url:
24092460
self.base_url_api_search = "{0}/search?q=".format(self.base_url_api)
24102461
else:
@@ -2479,7 +2530,13 @@ class SwordApi(Api):
24792530
"""
24802531

24812532
def __init__(
2482-
self, base_url, api_version="v1.1", api_token=None, sword_api_version="v1.1"
2533+
self,
2534+
base_url,
2535+
api_version="v1.1",
2536+
api_token=None,
2537+
sword_api_version="v1.1",
2538+
*,
2539+
auth=None,
24832540
):
24842541
"""Init a :class:`SwordApi <pyDataverse.api.SwordApi>` instance.
24852542
@@ -2489,7 +2546,7 @@ def __init__(
24892546
Api version of Dataverse SWORD API.
24902547
24912548
"""
2492-
super().__init__(base_url, api_token, api_version)
2549+
super().__init__(base_url, api_token, api_version, auth=auth)
24932550
if not isinstance(sword_api_version, ("".__class__, "".__class__)):
24942551
raise ApiUrlError(
24952552
"sword_api_version {0} is not a string.".format(sword_api_version)

pyDataverse/docs/source/reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Access all of Dataverse APIs.
1717

1818
.. automodule:: pyDataverse.api
1919
:members:
20+
:special-members:
2021

2122

2223
Models Interface

tests/api/test_api.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from httpx import Response
44
from time import sleep
55
from pyDataverse.api import DataAccessApi, NativeApi
6+
from pyDataverse.auth import ApiTokenAuth
67
from pyDataverse.exceptions import ApiAuthorizationError
78
from pyDataverse.exceptions import ApiUrlError
89
from pyDataverse.models import Dataset
@@ -34,6 +35,36 @@ def test_api_connect_base_url_wrong(self):
3435
NativeApi(None)
3536

3637

38+
class TestApiTokenAndAuthBehavior:
39+
def test_api_token_none_and_auth_none(self):
40+
api = NativeApi("https://demo.dataverse.org")
41+
assert api.api_token is None
42+
assert api.auth is None
43+
44+
def test_api_token_none_and_auth(self):
45+
auth = ApiTokenAuth("mytoken")
46+
api = NativeApi("https://demo.dataverse.org", auth=auth)
47+
assert api.api_token is None
48+
assert api.auth is auth
49+
50+
def test_api_token_and_auth(self):
51+
auth = ApiTokenAuth("mytoken")
52+
# Only one, api_token or auth, should be specified
53+
with pytest.warns(UserWarning):
54+
api = NativeApi(
55+
"https://demo.dataverse.org", api_token="sometoken", auth=auth
56+
)
57+
assert api.api_token is None
58+
assert api.auth is auth
59+
60+
def test_api_token_and_auth_none(self):
61+
api_token = "mytoken"
62+
api = NativeApi("https://demo.dataverse.org", api_token)
63+
assert api.api_token == api_token
64+
assert isinstance(api.auth, ApiTokenAuth)
65+
assert api.auth.api_token == api_token
66+
67+
3768
class TestApiRequests(object):
3869
"""Test the native_api requests."""
3970

0 commit comments

Comments
 (0)