Skip to content

Commit feca18c

Browse files
authored
Merge pull request #106 from Colin-b/feature/base_exc
Add base exception
2 parents 5344e3d + 9ead1a9 commit feca18c

13 files changed

+76
-48
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Changed
1313
- Requires [`httpx`](https://www.python-httpx.org)==0.28.\*
14+
- Exceptions issued by `httpx_auth` are now inheriting from `httpx_auth.HttpxAuthException`, itself inheriting from `httpx.HTTPError`, instead of `Exception`.
1415

1516
### Added
1617
- Explicit support for python `3.13`.

httpx_auth/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
InvalidToken,
4141
TokenExpiryNotProvided,
4242
InvalidGrantRequest,
43+
HttpxAuthException,
4344
)
4445
from httpx_auth.version import __version__
4546

@@ -67,6 +68,7 @@
6768
"JsonTokenFileCache",
6869
"TokenMemoryCache",
6970
"AWS4Auth",
71+
"HttpxAuthException",
7072
"GrantNotProvided",
7173
"TimeoutOccurred",
7274
"AuthenticationFailed",

httpx_auth/_errors.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,42 @@
44
import httpx
55

66

7-
class AuthenticationFailed(Exception):
7+
class HttpxAuthException(httpx.HTTPError): ...
8+
9+
10+
class AuthenticationFailed(HttpxAuthException):
811
"""User was not authenticated."""
912

1013
def __init__(self):
11-
Exception.__init__(self, "User was not authenticated.")
14+
HttpxAuthException.__init__(self, "User was not authenticated.")
1215

1316

14-
class TimeoutOccurred(Exception):
17+
class TimeoutOccurred(HttpxAuthException):
1518
"""No response within timeout interval."""
1619

1720
def __init__(self, timeout: float):
18-
Exception.__init__(
21+
HttpxAuthException.__init__(
1922
self, f"User authentication was not received within {timeout} seconds."
2023
)
2124

2225

23-
class InvalidToken(Exception):
26+
class InvalidToken(HttpxAuthException):
2427
"""Token is invalid."""
2528

2629
def __init__(self, token_name: str):
27-
Exception.__init__(self, f"{token_name} is invalid.")
30+
HttpxAuthException.__init__(self, f"{token_name} is invalid.")
2831

2932

30-
class GrantNotProvided(Exception):
33+
class GrantNotProvided(HttpxAuthException):
3134
"""Grant was not provided."""
3235

3336
def __init__(self, grant_name: str, dictionary_without_grant: dict):
34-
Exception.__init__(
37+
HttpxAuthException.__init__(
3538
self, f"{grant_name} not provided within {dictionary_without_grant}."
3639
)
3740

3841

39-
class InvalidGrantRequest(Exception):
42+
class InvalidGrantRequest(HttpxAuthException):
4043
"""
4144
If the request failed client authentication or is invalid, the authorization server returns an error response as described in https://tools.ietf.org/html/rfc6749#section-5.2
4245
"""
@@ -64,7 +67,7 @@ class InvalidGrantRequest(Exception):
6467
}
6568

6669
def __init__(self, response: Union[httpx.Response, dict]):
67-
Exception.__init__(self, InvalidGrantRequest.to_message(response))
70+
HttpxAuthException.__init__(self, InvalidGrantRequest.to_message(response))
6871

6972
@staticmethod
7073
def to_message(response: Union[httpx.Response, dict]) -> str:
@@ -114,17 +117,19 @@ def _pop(key: str) -> str:
114117
return message
115118

116119

117-
class StateNotProvided(Exception):
120+
class StateNotProvided(HttpxAuthException):
118121
"""State was not provided."""
119122

120123
def __init__(self, dictionary_without_state: dict):
121-
Exception.__init__(
124+
HttpxAuthException.__init__(
122125
self, f"state not provided within {dictionary_without_state}."
123126
)
124127

125128

126-
class TokenExpiryNotProvided(Exception):
129+
class TokenExpiryNotProvided(HttpxAuthException):
127130
"""Token expiry was not provided."""
128131

129132
def __init__(self, token_body: dict):
130-
Exception.__init__(self, f"Expiry (exp) is not provided in {token_body}.")
133+
HttpxAuthException.__init__(
134+
self, f"Expiry (exp) is not provided in {token_body}."
135+
)

tests/features/token_cache/test_json_token_file_cache.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import pathlib
44

5+
import httpx
56
import pytest
67
import jwt
78

@@ -78,6 +79,8 @@ def failing_dump(*args):
7879
with pytest.raises(httpx_auth.AuthenticationFailed) as exception_info:
7980
same_cache.get_token("key1")
8081
assert str(exception_info.value) == "User was not authenticated."
82+
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
83+
assert isinstance(exception_info.value, httpx.HTTPError)
8184

8285
assert caplog.messages == [
8386
"Cannot save tokens.",

tests/oauth2/authorization_code/test_oauth2_authorization_code_async.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ async def test_oauth2_authorization_code_flow_is_able_to_reuse_client(
171171
json={
172172
"access_token": "2YotnFZFEjr1zCsicMWpAA",
173173
"token_type": "example",
174-
"expires_in": 10,
174+
"expires_in": 2,
175175
"example_parameter": "example_value",
176176
},
177177
match_content=b"grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA",
@@ -190,7 +190,7 @@ async def test_oauth2_authorization_code_flow_is_able_to_reuse_client(
190190

191191
tab.assert_success()
192192

193-
time.sleep(10)
193+
time.sleep(2)
194194
tab = browser_mock.add_response(
195195
opened_url="https://provide_code?response_type=code&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
196196
reply_url="http://localhost:5000#code=SplxlOBeZQQYbYS6WxSbIA&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5",
@@ -240,7 +240,7 @@ async def test_oauth2_authorization_code_flow_is_able_to_reuse_client_with_token
240240
json={
241241
"access_token": "2YotnFZFEjr1zCsicMWpAA",
242242
"token_type": "example",
243-
"expires_in": 10,
243+
"expires_in": 2,
244244
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
245245
"example_parameter": "example_value",
246246
},
@@ -260,7 +260,7 @@ async def test_oauth2_authorization_code_flow_is_able_to_reuse_client_with_token
260260

261261
tab.assert_success()
262262

263-
time.sleep(10)
263+
time.sleep(2)
264264

265265
# response for refresh token grant
266266
httpx_mock.add_response(

tests/oauth2/authorization_code/test_oauth2_authorization_code_sync.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,12 @@ def test_with_invalid_request_error_uses_custom_failure(
141141
)
142142

143143
with httpx.Client() as client:
144-
with pytest.raises(httpx_auth.InvalidGrantRequest):
144+
with pytest.raises(httpx_auth.InvalidGrantRequest) as exception_info:
145145
client.get("https://authorized_only", auth=auth)
146146

147+
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
148+
assert isinstance(exception_info.value, httpx.HTTPError)
149+
147150
tab.assert_failure(
148151
"invalid_request: The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."
149152
)
@@ -166,7 +169,7 @@ def test_oauth2_authorization_code_flow_is_able_to_reuse_client(
166169
json={
167170
"access_token": "2YotnFZFEjr1zCsicMWpAA",
168171
"token_type": "example",
169-
"expires_in": 10,
172+
"expires_in": 2,
170173
"example_parameter": "example_value",
171174
},
172175
match_content=b"grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA",
@@ -185,7 +188,7 @@ def test_oauth2_authorization_code_flow_is_able_to_reuse_client(
185188

186189
tab.assert_success()
187190

188-
time.sleep(10)
191+
time.sleep(2)
189192
tab = browser_mock.add_response(
190193
opened_url="https://provide_code?response_type=code&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
191194
reply_url="http://localhost:5000#code=SplxlOBeZQQYbYS6WxSbIA&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5",
@@ -234,7 +237,7 @@ def test_oauth2_authorization_code_flow_is_able_to_reuse_client_with_token_refre
234237
json={
235238
"access_token": "2YotnFZFEjr1zCsicMWpAA",
236239
"token_type": "example",
237-
"expires_in": 10,
240+
"expires_in": 2,
238241
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
239242
"example_parameter": "example_value",
240243
},
@@ -254,7 +257,7 @@ def test_oauth2_authorization_code_flow_is_able_to_reuse_client_with_token_refre
254257

255258
tab.assert_success()
256259

257-
time.sleep(10)
260+
time.sleep(2)
258261

259262
# response for refresh token grant
260263
httpx_mock.add_response(
@@ -631,6 +634,8 @@ def test_empty_token_is_invalid(
631634
str(exception_info.value)
632635
== "access_token not provided within {'access_token': '', 'token_type': 'example', 'expires_in': 3600, 'refresh_token': 'tGzv3JOkF0XG5Qx2TlKWIA', 'example_parameter': 'example_value'}."
633636
)
637+
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
638+
assert isinstance(exception_info.value, httpx.HTTPError)
634639
tab.assert_success()
635640

636641

tests/oauth2/authorization_code_pkce/test_oauth2_authorization_code_pkce_async.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ async def test_oauth2_pkce_flow_is_able_to_reuse_client(
169169
json={
170170
"access_token": "2YotnFZFEjr1zCsicMWpAA",
171171
"token_type": "example",
172-
"expires_in": 10,
172+
"expires_in": 2,
173173
"example_parameter": "example_value",
174174
},
175175
match_content=b"code_verifier=MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA",
@@ -187,7 +187,7 @@ async def test_oauth2_pkce_flow_is_able_to_reuse_client(
187187
await client.get("https://authorized_only", auth=auth)
188188

189189
tab.assert_success()
190-
time.sleep(10)
190+
time.sleep(2)
191191
tab = browser_mock.add_response(
192192
opened_url="https://provide_code?response_type=code&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
193193
reply_url="http://localhost:5000#code=SplxlOBeZQQYbYS6WxSbIA&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5",
@@ -236,7 +236,7 @@ async def test_oauth2_pkce_flow_is_able_to_reuse_client_with_token_refresh(
236236
json={
237237
"access_token": "2YotnFZFEjr1zCsicMWpAA",
238238
"token_type": "example",
239-
"expires_in": 10,
239+
"expires_in": 2,
240240
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
241241
"example_parameter": "example_value",
242242
},
@@ -255,7 +255,7 @@ async def test_oauth2_pkce_flow_is_able_to_reuse_client_with_token_refresh(
255255
await client.get("https://authorized_only", auth=auth)
256256

257257
tab.assert_success()
258-
time.sleep(10)
258+
time.sleep(2)
259259

260260
httpx_mock.add_response(
261261
method="POST",

tests/oauth2/authorization_code_pkce/test_oauth2_authorization_code_pkce_sync.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def test_oauth2_pkce_flow_is_able_to_reuse_client(
164164
json={
165165
"access_token": "2YotnFZFEjr1zCsicMWpAA",
166166
"token_type": "example",
167-
"expires_in": 10,
167+
"expires_in": 2,
168168
"example_parameter": "example_value",
169169
},
170170
match_content=b"code_verifier=MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA",
@@ -182,7 +182,7 @@ def test_oauth2_pkce_flow_is_able_to_reuse_client(
182182
client.get("https://authorized_only", auth=auth)
183183

184184
tab.assert_success()
185-
time.sleep(10)
185+
time.sleep(2)
186186
tab = browser_mock.add_response(
187187
opened_url="https://provide_code?response_type=code&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
188188
reply_url="http://localhost:5000#code=SplxlOBeZQQYbYS6WxSbIA&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5",
@@ -230,7 +230,7 @@ def test_oauth2_pkce_flow_is_able_to_reuse_client_with_token_refresh(
230230
json={
231231
"access_token": "2YotnFZFEjr1zCsicMWpAA",
232232
"token_type": "example",
233-
"expires_in": 10,
233+
"expires_in": 2,
234234
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
235235
"example_parameter": "example_value",
236236
},
@@ -249,7 +249,7 @@ def test_oauth2_pkce_flow_is_able_to_reuse_client_with_token_refresh(
249249
client.get("https://authorized_only", auth=auth)
250250

251251
tab.assert_success()
252-
time.sleep(10)
252+
time.sleep(2)
253253

254254
httpx_mock.add_response(
255255
method="POST",

tests/oauth2/implicit/test_oauth2_implicit_sync.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,8 @@ def open(self, url, new):
349349
str(exception_info.value)
350350
== "User authentication was not received within 0.1 seconds."
351351
)
352+
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
353+
assert isinstance(exception_info.value, httpx.HTTPError)
352354

353355

354356
def test_browser_error(token_cache, httpx_mock: HTTPXMock, monkeypatch):
@@ -380,6 +382,8 @@ def open(self, url, new):
380382
str(exception_info.value)
381383
== "User authentication was not received within 0.1 seconds."
382384
)
385+
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
386+
assert isinstance(exception_info.value, httpx.HTTPError)
383387

384388

385389
def test_state_change(token_cache, httpx_mock: HTTPXMock, browser_mock: BrowserMock):
@@ -416,9 +420,13 @@ def test_empty_token_is_invalid(token_cache, browser_mock: BrowserMock):
416420
)
417421

418422
with httpx.Client() as client:
419-
with pytest.raises(httpx_auth.InvalidToken, match=" is invalid."):
423+
with pytest.raises(
424+
httpx_auth.InvalidToken, match=" is invalid."
425+
) as exception_info:
420426
client.get("https://authorized_only", auth=auth)
421427

428+
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
429+
assert isinstance(exception_info.value, httpx.HTTPError)
422430
tab.assert_success()
423431

424432

@@ -435,6 +443,8 @@ def test_token_without_expiry_is_invalid(token_cache, browser_mock: BrowserMock)
435443
client.get("https://authorized_only", auth=auth)
436444

437445
assert str(exception_info.value) == "Expiry (exp) is not provided in None."
446+
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
447+
assert isinstance(exception_info.value, httpx.HTTPError)
438448
tab.assert_success()
439449

440450

@@ -701,6 +711,8 @@ def test_oauth2_implicit_flow_post_failure_if_state_is_not_provided(
701711
str(exception_info.value)
702712
== f"state not provided within {{'access_token': ['{token}']}}."
703713
)
714+
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
715+
assert isinstance(exception_info.value, httpx.HTTPError)
704716
tab.assert_failure(f"state not provided within {{'access_token': ['{token}']}}.")
705717

706718

tests/oauth2/resource_owner_password/okta/test_oauth2_resource_owner_password_okta_async.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ async def test_oauth2_password_credentials_flow_is_able_to_reuse_client(
7171
json={
7272
"access_token": "2YotnFZFEjr1zCsicMWpAA",
7373
"token_type": "example",
74-
"expires_in": 10,
74+
"expires_in": 2,
7575
"example_parameter": "example_value",
7676
},
7777
match_content=b"grant_type=password&username=test_user&password=test_pwd&scope=openid",
@@ -91,7 +91,7 @@ async def test_oauth2_password_credentials_flow_is_able_to_reuse_client(
9191
async with httpx.AsyncClient() as client:
9292
await client.get("https://authorized_only", auth=auth)
9393

94-
time.sleep(10)
94+
time.sleep(2)
9595

9696
httpx_mock.add_response(
9797
method="POST",
@@ -139,7 +139,7 @@ async def test_oauth2_password_credentials_flow_is_able_to_reuse_client_with_tok
139139
json={
140140
"access_token": "2YotnFZFEjr1zCsicMWpAA",
141141
"token_type": "example",
142-
"expires_in": 10,
142+
"expires_in": 2,
143143
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
144144
"example_parameter": "example_value",
145145
},
@@ -160,7 +160,7 @@ async def test_oauth2_password_credentials_flow_is_able_to_reuse_client_with_tok
160160
async with httpx.AsyncClient() as client:
161161
await client.get("https://authorized_only", auth=auth)
162162

163-
time.sleep(10)
163+
time.sleep(2)
164164

165165
httpx_mock.add_response(
166166
method="POST",

0 commit comments

Comments
 (0)