Skip to content

Commit 8e59377

Browse files
authored
Add handling for API Rate limit error (#121)
* Add handling for API Rate limit error * extract raise rate limit exception logic to function * update pre-commit file with newer poetry version + update poetry lock file with latest changes * update poetry version in docker file * update the error message key * update tests with the new name * raise RateLimit exception also when validating token + update Readme file * set the reference url to be markdown link
1 parent 21b2a52 commit 8e59377

10 files changed

+801
-561
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ repos:
2929
hooks:
3030
- id: flake8
3131
- repo: https://github.com/python-poetry/poetry
32-
rev: 1.2.1 # add version here
32+
rev: 1.3.2 # add version here
3333
hooks:
3434
- id: poetry-check
3535
- id: poetry-lock

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ RUN apt-get update \
2323
git
2424

2525
# Install Poetry - respects $POETRY_VERSION & $POETRY_HOME
26-
ENV POETRY_VERSION=1.2.0
26+
ENV POETRY_VERSION=1.3.2
2727
RUN curl -sSL https://install.python-poetry.org | python
2828

2929
# We copy our Python requirements here to cache them

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,23 @@ updated_jwt = client.mgmt.jwt.updateJWT(
615615
)
616616
```
617617

618+
## API Rate limits
619+
620+
Handle API rate limits by comparing the exception to the APIRateLimitExceeded exception, which includes the RateLimitParameters map with the key "Retry-After." This key indicates how many seconds until the next valid API call can take place. More information on Descope's rate limit is covered here: [Descope rate limit reference page](https://docs.descope.com/rate-limit)
621+
622+
```python
623+
try:
624+
descope_client.magiclink.sign_up_or_in(
625+
method=DeliveryMethod.EMAIL,
626+
login_id="[email protected]",
627+
uri="http://myapp.com/verify-magic-link",
628+
)
629+
except RateLimitException as e:
630+
retry_after_seconds = e.rate_limit_parameters.get(API_RATE_LIMIT_RETRY_AFTER_HEADER)
631+
# This variable indicates how many seconds until the next valid API call can take place.
632+
```
633+
634+
618635
## Code Samples
619636

620637
You can find various usage samples in the [samples folder](https://github.com/descope/python-sdk/blob/main/samples).

descope/__init__.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
DeliveryMethod,
88
)
99
from descope.descope_client import DescopeClient
10-
from descope.exceptions import AuthException
10+
from descope.exceptions import (
11+
API_RATE_LIMIT_RETRY_AFTER_HEADER,
12+
ERROR_TYPE_API_RATE_LIMIT,
13+
AuthException,
14+
RateLimitException,
15+
)
1116
from descope.management.common import AssociatedTenant
1217
from descope.management.sso_settings import AttributeMapping, RoleMapping

descope/auth.py

+28
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@
2222
EndpointsV2,
2323
)
2424
from descope.exceptions import (
25+
API_RATE_LIMIT_RETRY_AFTER_HEADER,
26+
ERROR_TYPE_API_RATE_LIMIT,
2527
ERROR_TYPE_INVALID_ARGUMENT,
2628
ERROR_TYPE_INVALID_PUBLIC_KEY,
2729
ERROR_TYPE_INVALID_TOKEN,
2830
ERROR_TYPE_SERVER_ERROR,
2931
AuthException,
32+
RateLimitException,
3033
)
3134

3235

@@ -73,6 +76,20 @@ def __init__(
7376
kid, pub_key, alg = self._validate_and_load_public_key(public_key)
7477
self.public_keys = {kid: (pub_key, alg)}
7578

79+
def _raise_rate_limit_exception(self, response):
80+
resp = response.json()
81+
raise RateLimitException(
82+
resp.get("errorCode", "429"),
83+
ERROR_TYPE_API_RATE_LIMIT,
84+
resp.get("errorDescription", ""),
85+
resp.get("errorMessage", ""),
86+
rate_limit_parameters={
87+
API_RATE_LIMIT_RETRY_AFTER_HEADER: int(
88+
response.headers.get(API_RATE_LIMIT_RETRY_AFTER_HEADER, 0)
89+
)
90+
},
91+
)
92+
7693
def do_get(
7794
self,
7895
uri: str,
@@ -88,6 +105,8 @@ def do_get(
88105
verify=self.secure,
89106
)
90107
if not response.ok:
108+
if response.status_code == 429:
109+
self._raise_rate_limit_exception(response) # Raise RateLimitException
91110
raise AuthException(
92111
response.status_code, ERROR_TYPE_SERVER_ERROR, response.text
93112
)
@@ -105,6 +124,9 @@ def do_post(
105124
params=params,
106125
)
107126
if not response.ok:
127+
if response.status_code == 429:
128+
self._raise_rate_limit_exception(response) # Raise RateLimitException
129+
108130
raise AuthException(
109131
response.status_code, ERROR_TYPE_SERVER_ERROR, response.text
110132
)
@@ -296,6 +318,8 @@ def _fetch_public_keys(self) -> None:
296318
)
297319

298320
if not response.ok:
321+
if response.status_code == 429:
322+
self._raise_rate_limit_exception(response) # Raise RateLimitException
299323
raise AuthException(
300324
response.status_code,
301325
ERROR_TYPE_SERVER_ERROR,
@@ -485,6 +509,8 @@ def validate_session(self, session_token: str) -> dict:
485509
try:
486510
res = self._validate_token(session_token)
487511
return self.adjust_properties(res, True)
512+
except RateLimitException as e:
513+
raise e
488514
except Exception as e:
489515
raise AuthException(
490516
401, ERROR_TYPE_INVALID_TOKEN, f"Invalid session token: {e}"
@@ -500,6 +526,8 @@ def refresh_session(self, refresh_token: str) -> dict:
500526

501527
try:
502528
self._validate_token(refresh_token)
529+
except RateLimitException as e:
530+
raise e
503531
except Exception as e:
504532
# Refresh is invalid
505533
raise AuthException(

descope/exceptions.py

+26
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
ERROR_TYPE_SERVER_ERROR = "server error"
33
ERROR_TYPE_INVALID_PUBLIC_KEY = "invalid public key"
44
ERROR_TYPE_INVALID_TOKEN = "invalid token"
5+
ERROR_TYPE_API_RATE_LIMIT = "API rate limit exceeded"
6+
7+
API_RATE_LIMIT_RETRY_AFTER_HEADER = "Retry-After"
58

69

710
class AuthException(Exception):
@@ -21,3 +24,26 @@ def __repr__(self):
2124

2225
def __str__(self):
2326
return str(self.__dict__)
27+
28+
29+
class RateLimitException(Exception):
30+
def __init__(
31+
self,
32+
status_code: int = None,
33+
error_type: str = None,
34+
error_description: str = None,
35+
error_message: str = None,
36+
rate_limit_parameters: dict = {},
37+
**kwargs,
38+
):
39+
self.status_code = status_code
40+
self.error_type = error_type
41+
self.error_description = error_description
42+
self.error_message = error_message
43+
self.rate_limit_parameters = rate_limit_parameters
44+
45+
def __repr__(self):
46+
return f"Error {self.__dict__}"
47+
48+
def __str__(self):
49+
return str(self.__dict__)

0 commit comments

Comments
 (0)