mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 15:27:17 +00:00
add retry on authorization errros
This commit is contained in:
parent
82d1be52a8
commit
6bc7353514
2
.github/workflows/setup.sh
vendored
2
.github/workflows/setup.sh
vendored
@ -10,7 +10,7 @@ echo -e '[arcanisrepo]\nServer = http://repo.arcanis.me/$arch\nSigLevel = Never'
|
|||||||
# refresh the image
|
# refresh the image
|
||||||
pacman --noconfirm -Syu
|
pacman --noconfirm -Syu
|
||||||
# main dependencies
|
# main dependencies
|
||||||
pacman --noconfirm -Sy base-devel devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo python-systemd sudo
|
pacman --noconfirm -Sy base-devel devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo python-tenacity sudo
|
||||||
# make dependencies
|
# make dependencies
|
||||||
pacman --noconfirm -Sy python-build python-flit python-installer python-wheel
|
pacman --noconfirm -Sy python-build python-flit python-installer python-wheel
|
||||||
# optional dependencies
|
# optional dependencies
|
||||||
|
@ -31,7 +31,7 @@ RUN useradd -m -d "/home/build" -s "/usr/bin/nologin" build && \
|
|||||||
COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
|
COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
|
||||||
## install package dependencies
|
## install package dependencies
|
||||||
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
|
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
|
||||||
RUN pacman -Sy --noconfirm --asdeps devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo && \
|
RUN pacman -Sy --noconfirm --asdeps devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo python-tenacity && \
|
||||||
pacman -Sy --noconfirm --asdeps python-build python-flit python-installer python-wheel && \
|
pacman -Sy --noconfirm --asdeps python-build python-flit python-installer python-wheel && \
|
||||||
pacman -Sy --noconfirm --asdeps breezy mercurial python-aiohttp python-aiohttp-cors python-boto3 python-cryptography python-jinja python-requests-unixsocket python-systemd rsync subversion && \
|
pacman -Sy --noconfirm --asdeps breezy mercurial python-aiohttp python-aiohttp-cors python-boto3 python-cryptography python-jinja python-requests-unixsocket python-systemd rsync subversion && \
|
||||||
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-apispec-git python-aiohttp-jinja2 \
|
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-apispec-git python-aiohttp-jinja2 \
|
||||||
|
@ -7,7 +7,7 @@ pkgdesc="ArcH linux ReposItory MANager"
|
|||||||
arch=('any')
|
arch=('any')
|
||||||
url="https://github.com/arcan1s/ahriman"
|
url="https://github.com/arcan1s/ahriman"
|
||||||
license=('GPL3')
|
license=('GPL3')
|
||||||
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-requests' 'python-srcinfo')
|
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-requests' 'python-srcinfo' 'python-tenacity')
|
||||||
makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel')
|
makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel')
|
||||||
optdepends=('breezy: -bzr packages support'
|
optdepends=('breezy: -bzr packages support'
|
||||||
'darcs: -darcs packages support'
|
'darcs: -darcs packages support'
|
||||||
|
@ -21,6 +21,7 @@ dependencies = [
|
|||||||
"passlib",
|
"passlib",
|
||||||
"requests",
|
"requests",
|
||||||
"srcinfo",
|
"srcinfo",
|
||||||
|
"tenacity",
|
||||||
]
|
]
|
||||||
|
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
@ -19,8 +19,10 @@
|
|||||||
#
|
#
|
||||||
import contextlib
|
import contextlib
|
||||||
import requests
|
import requests
|
||||||
|
import tenacity
|
||||||
|
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from typing import Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from ahriman import __version__
|
from ahriman import __version__
|
||||||
@ -57,6 +59,34 @@ class SyncAhrimanClient(SyncHttpClient):
|
|||||||
|
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_retry_allowed(exception: BaseException) -> bool:
|
||||||
|
"""
|
||||||
|
check if retry is allowed for the exception
|
||||||
|
|
||||||
|
Args:
|
||||||
|
exception(BaseException): exception raised
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True in case if exception is in white list and false otherwise
|
||||||
|
"""
|
||||||
|
if not isinstance(exception, requests.RequestException):
|
||||||
|
return False # not a request exception
|
||||||
|
status_code = exception.response.status_code if exception.response is not None else None
|
||||||
|
return status_code in (401,)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def on_retry(state: tenacity.RetryCallState) -> None:
|
||||||
|
"""
|
||||||
|
action to be called before retry
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state(tenacity.RetryCallState): current retry call state
|
||||||
|
"""
|
||||||
|
instance = next(arg for arg in state.args if isinstance(arg, SyncAhrimanClient))
|
||||||
|
if hasattr(instance, "session"): # only if it was initialized
|
||||||
|
del instance.session # clear session
|
||||||
|
|
||||||
def _login(self, session: requests.Session) -> None:
|
def _login(self, session: requests.Session) -> None:
|
||||||
"""
|
"""
|
||||||
process login to the service
|
process login to the service
|
||||||
@ -83,3 +113,22 @@ class SyncAhrimanClient(SyncHttpClient):
|
|||||||
str: full url for web service to log in
|
str: full url for web service to log in
|
||||||
"""
|
"""
|
||||||
return f"{self.address}/api/v1/login"
|
return f"{self.address}/api/v1/login"
|
||||||
|
|
||||||
|
@tenacity.retry(
|
||||||
|
stop=tenacity.stop_after_attempt(2),
|
||||||
|
retry=tenacity.retry_if_exception(is_retry_allowed),
|
||||||
|
after=on_retry,
|
||||||
|
reraise=True,
|
||||||
|
)
|
||||||
|
def make_request(self, *args: Any, **kwargs: Any) -> requests.Response:
|
||||||
|
"""
|
||||||
|
perform request with specified parameters
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*args(Any): request method positional arguments
|
||||||
|
**kwargs(Any): request method keyword arguments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
requests.Response: response object
|
||||||
|
"""
|
||||||
|
return SyncHttpClient.make_request(self, *args, **kwargs)
|
||||||
|
@ -77,12 +77,12 @@ class SyncHttpClient(LazyLogging):
|
|||||||
return session
|
return session
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def exception_response_text(exception: requests.exceptions.RequestException) -> str:
|
def exception_response_text(exception: requests.RequestException) -> str:
|
||||||
"""
|
"""
|
||||||
safe response exception text generation
|
safe response exception text generation
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
exception(requests.exceptions.RequestException): exception raised
|
exception(requests.RequestException): exception raised
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: text of the response if it is not None and empty string otherwise
|
str: text of the response if it is not None and empty string otherwise
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
import requests_unixsocket
|
import requests_unixsocket
|
||||||
|
import tenacity
|
||||||
|
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
from unittest.mock import call as MockCall
|
||||||
|
|
||||||
from ahriman.core.http import SyncAhrimanClient
|
from ahriman.core.http import SyncAhrimanClient
|
||||||
from ahriman.models.user import User
|
from ahriman.models.user import User
|
||||||
@ -30,6 +32,37 @@ def test_session_unix_socket(ahriman_client: SyncAhrimanClient, mocker: MockerFi
|
|||||||
login_mock.assert_not_called()
|
login_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_retry_allowed() -> None:
|
||||||
|
"""
|
||||||
|
must allow retries on 401 errors
|
||||||
|
"""
|
||||||
|
assert not SyncAhrimanClient.is_retry_allowed(Exception())
|
||||||
|
|
||||||
|
response = requests.Response()
|
||||||
|
response.status_code = 400
|
||||||
|
assert not SyncAhrimanClient.is_retry_allowed(requests.HTTPError(response=response))
|
||||||
|
|
||||||
|
response.status_code = 401
|
||||||
|
assert SyncAhrimanClient.is_retry_allowed(requests.HTTPError(response=response))
|
||||||
|
|
||||||
|
response.status_code = 403
|
||||||
|
assert not SyncAhrimanClient.is_retry_allowed(requests.HTTPError(response=response))
|
||||||
|
|
||||||
|
|
||||||
|
def test_on_retry(ahriman_client: SyncAhrimanClient) -> None:
|
||||||
|
"""
|
||||||
|
must remove session on retry
|
||||||
|
"""
|
||||||
|
SyncAhrimanClient.on_retry(
|
||||||
|
tenacity.RetryCallState(
|
||||||
|
retry_object=tenacity.Retrying(),
|
||||||
|
fn=SyncAhrimanClient.make_request,
|
||||||
|
args=(ahriman_client,),
|
||||||
|
kwargs={},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_login(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
def test_login(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must login user
|
must login user
|
||||||
@ -79,3 +112,35 @@ def test_login_url(ahriman_client: SyncAhrimanClient) -> None:
|
|||||||
"""
|
"""
|
||||||
assert ahriman_client._login_url().startswith(ahriman_client.address)
|
assert ahriman_client._login_url().startswith(ahriman_client.address)
|
||||||
assert ahriman_client._login_url().endswith("/api/v1/login")
|
assert ahriman_client._login_url().endswith("/api/v1/login")
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_request_retry(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must retry HTTP request
|
||||||
|
"""
|
||||||
|
response_ok = requests.Response()
|
||||||
|
response_ok.status_code = 200
|
||||||
|
response_error = requests.Response()
|
||||||
|
response_error.status_code = 401
|
||||||
|
# login on init -> request with error -> login on error -> request without error
|
||||||
|
request_mock = mocker.patch("requests.Session.request", side_effect=[
|
||||||
|
response_ok,
|
||||||
|
response_error,
|
||||||
|
response_ok,
|
||||||
|
response_ok,
|
||||||
|
])
|
||||||
|
|
||||||
|
ahriman_client.auth = ("username", "password")
|
||||||
|
assert ahriman_client.make_request("GET", "url") is not None
|
||||||
|
request_mock.assert_has_calls([
|
||||||
|
MockCall("POST", "http://127.0.0.1:8080/api/v1/login", params=None, data=None, headers=None, files=None,
|
||||||
|
json={"username": "username", "password": "password"},
|
||||||
|
auth=ahriman_client.auth, timeout=ahriman_client.timeout),
|
||||||
|
MockCall("GET", "url", params=None, data=None, headers=None, files=None, json=None,
|
||||||
|
auth=ahriman_client.auth, timeout=ahriman_client.timeout),
|
||||||
|
MockCall("POST", "http://127.0.0.1:8080/api/v1/login", params=None, data=None, headers=None, files=None,
|
||||||
|
json={"username": "username", "password": "password"},
|
||||||
|
auth=ahriman_client.auth, timeout=ahriman_client.timeout),
|
||||||
|
MockCall("GET", "url", params=None, data=None, headers=None, files=None, json=None,
|
||||||
|
auth=ahriman_client.auth, timeout=ahriman_client.timeout),
|
||||||
|
])
|
||||||
|
Loading…
Reference in New Issue
Block a user