From c49ff414feb82ffaed5a49d8353019f20223f6bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbj=C3=B8rn?= Date: Mon, 23 Feb 2026 15:01:18 +0100 Subject: [PATCH 1/5] Add backoff to auth requests, to try handling 400 errors from Kinde --- src/cradl/backoff.py | 9 +++++++++ src/cradl/client.py | 13 ++++--------- src/cradl/credentials.py | 7 ++++++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/cradl/backoff.py b/src/cradl/backoff.py index 3b2913c..02d9a94 100644 --- a/src/cradl/backoff.py +++ b/src/cradl/backoff.py @@ -2,6 +2,15 @@ import time from typing import Optional, Union, Type, Callable +import requests +from requests.exceptions import RequestException # type: ignore + + +def fatal_code(e: RequestException): + if isinstance(e.response, requests.Response) and isinstance(e.response.status_code, int): + return 400 <= e.response.status_code < 500 + raise e + def exponential_backoff( exceptions: Union[tuple[Type[Exception]], Type[Exception]], diff --git a/src/cradl/client.py b/src/cradl/client.py index dc13349..07b70a7 100644 --- a/src/cradl/client.py +++ b/src/cradl/client.py @@ -3,20 +3,18 @@ from base64 import b64encode from datetime import datetime from pathlib import Path - from typing import Callable, Dict, List, Optional, Sequence, Union from urllib.parse import urlparse, quote import requests # type: ignore from requests.exceptions import RequestException # type: ignore -from .credentials import Credentials, guess_credentials +from .backoff import exponential_backoff, fatal_code from .content import parse_content +from .credentials import Credentials, guess_credentials from .log import setup_logging -from .backoff import exponential_backoff from .response import decode_response, TooManyRequestsException, EmptyRequestError - logger = setup_logging(__name__) Content = Union[bytes, bytearray, str, Path, io.IOBase] Queryparam = Union[str, List[str]] @@ -35,10 +33,7 @@ def dictstrip(d): return {k: v for k, v in d.items() if v is not None} -def _fatal_code(e: RequestException): - if isinstance(e.response, requests.Response) and isinstance(e.response.status_code, int): - return 400 <= e.response.status_code < 500 - raise e + class Client: @@ -81,7 +76,7 @@ def _make_request( return decode_response(response) @exponential_backoff(TooManyRequestsException, max_tries=4) - @exponential_backoff(RequestException, max_tries=3, giveup=_fatal_code) + @exponential_backoff(RequestException, max_tries=3, giveup=fatal_code) def _make_fileserver_request( self, requests_fn: Callable, diff --git a/src/cradl/credentials.py b/src/cradl/credentials.py index 954dbb2..469585a 100644 --- a/src/cradl/credentials.py +++ b/src/cradl/credentials.py @@ -7,9 +7,11 @@ from typing import Optional, Tuple import requests # type: ignore +from requests.exceptions import RequestException # type: ignore +from .backoff import exponential_backoff, fatal_code from .log import setup_logging - +from .response import TooManyRequestsException, BadRequest logger = setup_logging(__name__) NULL_TOKEN = '', 0 @@ -82,6 +84,9 @@ def access_token(self) -> str: return access_token + # Backoff on BadRequest since Kinde seems to sometimes give bogus 400 responses + @exponential_backoff(exceptions=(TooManyRequestsException, BadRequest), max_tries=4) + @exponential_backoff(RequestException, max_tries=3, giveup=fatal_code) def _get_client_credentials(self) -> Tuple[str, int]: if any(endpoint in self.auth_endpoint for endpoint in ['auth.lucidtech.io', 'auth.cradl.ai', 'kinde.com']): data = { From 300120a1d3664413899bc93b5682090ea32c1b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbj=C3=B8rn?= Date: Mon, 23 Feb 2026 15:03:23 +0100 Subject: [PATCH 2/5] Changelog --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e205fcc..0dddfe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 0.6.1 - 2026-02-23 + +- Add exponential backoff to calls toward the auth endpoint + ## Version 0.6.0 - 2026-01-27 - Add update_validation method diff --git a/pyproject.toml b/pyproject.toml index d11bdba..84ffeb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cradl" -version = "0.6.0" +version = "0.6.1" description = "Python SDK for Cradl" authors = [{ name = "Cradl", email = "hello@cradl.ai" }] readme = "README.md" From 03b43b05ddb5bbc728d3eb96448b289d558a26c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbj=C3=B8rn?= Date: Mon, 23 Feb 2026 15:05:18 +0100 Subject: [PATCH 3/5] Lint --- src/cradl/client.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/cradl/client.py b/src/cradl/client.py index 07b70a7..a449855 100644 --- a/src/cradl/client.py +++ b/src/cradl/client.py @@ -33,9 +33,6 @@ def dictstrip(d): return {k: v for k, v in d.items() if v is not None} - - - class Client: """A low level client to invoke api methods from Cradl.""" def __init__(self, credentials: Optional[Credentials] = None, profile=None): @@ -44,7 +41,7 @@ def __init__(self, credentials: Optional[Credentials] = None, profile=None): self.credentials = credentials or guess_credentials(profile) @exponential_backoff(TooManyRequestsException, max_tries=4) - @exponential_backoff(RequestException, max_tries=3, giveup=_fatal_code) + @exponential_backoff(RequestException, max_tries=3, giveup=fatal_code) def _make_request( self, requests_fn: Callable, From ee7812249401e5f3c5c152fae4d247e2e6d98acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbj=C3=B8rn?= Date: Mon, 23 Feb 2026 15:08:10 +0100 Subject: [PATCH 4/5] Lint --- src/cradl/backoff.py | 2 +- src/cradl/credentials.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cradl/backoff.py b/src/cradl/backoff.py index 02d9a94..f490e14 100644 --- a/src/cradl/backoff.py +++ b/src/cradl/backoff.py @@ -2,7 +2,7 @@ import time from typing import Optional, Union, Type, Callable -import requests +import requests # type: ignore from requests.exceptions import RequestException # type: ignore diff --git a/src/cradl/credentials.py b/src/cradl/credentials.py index 469585a..67dd24b 100644 --- a/src/cradl/credentials.py +++ b/src/cradl/credentials.py @@ -85,7 +85,7 @@ def access_token(self) -> str: return access_token # Backoff on BadRequest since Kinde seems to sometimes give bogus 400 responses - @exponential_backoff(exceptions=(TooManyRequestsException, BadRequest), max_tries=4) + @exponential_backoff(exceptions=(TooManyRequestsException, BadRequest), max_tries=4) # noqa @exponential_backoff(RequestException, max_tries=3, giveup=fatal_code) def _get_client_credentials(self) -> Tuple[str, int]: if any(endpoint in self.auth_endpoint for endpoint in ['auth.lucidtech.io', 'auth.cradl.ai', 'kinde.com']): From ab0f4bf41b02b4b96c363b1c49ba8675a0909e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbj=C3=B8rn?= Date: Mon, 23 Feb 2026 15:09:38 +0100 Subject: [PATCH 5/5] Lint --- src/cradl/credentials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cradl/credentials.py b/src/cradl/credentials.py index 67dd24b..3313161 100644 --- a/src/cradl/credentials.py +++ b/src/cradl/credentials.py @@ -85,7 +85,7 @@ def access_token(self) -> str: return access_token # Backoff on BadRequest since Kinde seems to sometimes give bogus 400 responses - @exponential_backoff(exceptions=(TooManyRequestsException, BadRequest), max_tries=4) # noqa + @exponential_backoff(exceptions=(TooManyRequestsException, BadRequest), max_tries=4) # type: ignore @exponential_backoff(RequestException, max_tries=3, giveup=fatal_code) def _get_client_credentials(self) -> Tuple[str, int]: if any(endpoint in self.auth_endpoint for endpoint in ['auth.lucidtech.io', 'auth.cradl.ai', 'kinde.com']):