Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔄 Rate limit retry #51

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ site/
.pdm-python
.token
.cred
.env
.cache_ggshield

# common name for local Sony Ci config file
Expand Down
14 changes: 2 additions & 12 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dependencies = [
"requests-oauth2client~=1.6",
"loguru~=0.7",
]
requires-python = '>=3.8'
requires-python = '>=3.9'
readme = 'README.md'
license = { text = 'MIT' }
dynamic = ['version']
Expand Down
2 changes: 1 addition & 1 deletion sonyci/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.3.0'
__version__ = '0.4.0'
3 changes: 1 addition & 2 deletions sonyci/cli.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from json import dumps, loads
from pathlib import Path
from typing import Optional
from typing import Annotated, Optional
from urllib.request import urlretrieve

from requests_oauth2client.tokens import BearerToken, BearerTokenSerializer
from typer import Argument, Context, Exit, Option, Typer
from typer.main import get_group
from typing_extensions import Annotated

from sonyci import SonyCi
from sonyci.log import log
Expand Down
2 changes: 2 additions & 0 deletions sonyci/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class RetryError(Exception):
"""Raised when a function fails after retrying a set number of times."""
5 changes: 4 additions & 1 deletion sonyci/sonyci.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from sonyci.config import Config
from sonyci.log import log
from sonyci.utils import get_token, json
from sonyci.utils import get_token, json, retry


class SonyCi(Config):
Expand Down Expand Up @@ -95,12 +95,15 @@ def asset_download(self, asset_id: str, **kwargs) -> dict:
return self.get(f'/assets/{asset_id}/download', params=kwargs)

@json
@retry
def get(self, *args, **kwargs):
log.debug(f'GET {args} {kwargs}')
return self.client.get(*args, **kwargs)

@json
@retry
def post(self, *args, **kwargs):
log.debug(f'POST {args} {kwargs}')
return self.client.post(*args, **kwargs)

def __call__(self, path: str, **kwds: Any) -> Any:
Expand Down
31 changes: 31 additions & 0 deletions sonyci/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from requests_oauth2client.tokens import BearerToken, BearerTokenSerializer

from sonyci.config import TOKEN_URL
from sonyci.exceptions import RetryError
from sonyci.log import log


Expand Down Expand Up @@ -48,3 +49,33 @@ def inner(*args, **kwargs):
return func(*args, **kwargs).json()

return inner


def retry(func):
"""Decorator for retrying a function call after a rate limit error."""
from requests import HTTPError

def inner(*args, **kwargs):
tries: int = 0
max_tries: int = 5
while tries < max_tries:
try:
return func(*args, **kwargs)
except HTTPError as e:
if e.response.status_code != 429:
log.error(f'HTTPError {e.response.status_code}: {e}')
raise e
from time import sleep

# Get the retry-after header, if it exists
retry_after = e.response.headers.get('Retry-After')
if not retry_after:
log.error('No Retry-After header found')
raise e
log.warning(f'Rate limited. Retrying after {retry_after} seconds...')
sleep(int(retry_after + 1))
tries += 1
log.error(f'Failed after {max_tries} tries')
raise RetryError(f'Failed after {max_tries} tries')

return inner
7 changes: 4 additions & 3 deletions tests/cli/test_cli_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ def test_bad_login(error_runner):
assert 'invalid_client' in str(result.exception)


def test_missing_username(runner):
result = runner.invoke(
def test_missing_username(error_runner):
result = error_runner.invoke(
app,
[
'--client-id',
Expand All @@ -59,4 +59,5 @@ def test_missing_username(runner):
],
)
assert result.exit_code == 2
assert 'username' in result.stdout
assert 'Missing option' in result.stderr
assert 'username' in result.stderr