mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
feat: add separated web client for ahriman web services
This commit is contained in:
parent
2d21c999d1
commit
de7184fc3a
@ -4,6 +4,14 @@ ahriman.core.http package
|
|||||||
Submodules
|
Submodules
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
ahriman.core.http.sync\_ahriman\_client module
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.http.sync_ahriman_client
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.http.sync\_http\_client module
|
ahriman.core.http.sync\_http\_client module
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
|
|
||||||
|
@ -17,4 +17,5 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from ahriman.core.http.sync_ahriman_client import SyncAhrimanClient
|
||||||
from ahriman.core.http.sync_http_client import MultipartType, SyncHttpClient
|
from ahriman.core.http.sync_http_client import MultipartType, SyncHttpClient
|
||||||
|
85
src/ahriman/core/http/sync_ahriman_client.py
Normal file
85
src/ahriman/core/http/sync_ahriman_client.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import contextlib
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from functools import cached_property
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from ahriman import __version__
|
||||||
|
from ahriman.core.http.sync_http_client import SyncHttpClient
|
||||||
|
|
||||||
|
|
||||||
|
class SyncAhrimanClient(SyncHttpClient):
|
||||||
|
"""
|
||||||
|
wrapper for ahriman web service
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
address(str): address of the web service
|
||||||
|
"""
|
||||||
|
|
||||||
|
address: str
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def session(self) -> requests.Session:
|
||||||
|
"""
|
||||||
|
get or create session
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
request.Session: created session object
|
||||||
|
"""
|
||||||
|
if urlparse(self.address).scheme == "http+unix":
|
||||||
|
import requests_unixsocket # type: ignore[import-untyped]
|
||||||
|
session: requests.Session = requests_unixsocket.Session()
|
||||||
|
session.headers["User-Agent"] = f"ahriman/{__version__}"
|
||||||
|
return session
|
||||||
|
|
||||||
|
session = requests.Session()
|
||||||
|
session.headers["User-Agent"] = f"ahriman/{__version__}"
|
||||||
|
self._login(session)
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
def _login(self, session: requests.Session) -> None:
|
||||||
|
"""
|
||||||
|
process login to the service
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session(requests.Session): request session to login
|
||||||
|
"""
|
||||||
|
if self.auth is None:
|
||||||
|
return # no auth configured
|
||||||
|
|
||||||
|
username, password = self.auth
|
||||||
|
payload = {
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
self.make_request("POST", self._login_url(), json=payload, session=session)
|
||||||
|
|
||||||
|
def _login_url(self) -> str:
|
||||||
|
"""
|
||||||
|
get url for the login api
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: full url for web service to log in
|
||||||
|
"""
|
||||||
|
return f"{self.address}/api/v1/login"
|
@ -19,14 +19,11 @@
|
|||||||
#
|
#
|
||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import requests
|
|
||||||
|
|
||||||
from functools import cached_property
|
from urllib.parse import quote_plus as urlencode
|
||||||
from urllib.parse import quote_plus as urlencode, urlparse
|
|
||||||
|
|
||||||
from ahriman import __version__
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.http import SyncHttpClient
|
from ahriman.core.http import SyncAhrimanClient
|
||||||
from ahriman.core.status.client import Client
|
from ahriman.core.status.client import Client
|
||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.internal_status import InternalStatus
|
from ahriman.models.internal_status import InternalStatus
|
||||||
@ -35,12 +32,11 @@ from ahriman.models.package import Package
|
|||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
class WebClient(Client, SyncHttpClient):
|
class WebClient(Client, SyncAhrimanClient):
|
||||||
"""
|
"""
|
||||||
build status reporter web client
|
build status reporter web client
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
address(str): address of the web service
|
|
||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -56,30 +52,10 @@ class WebClient(Client, SyncHttpClient):
|
|||||||
suppress_errors = configuration.getboolean( # read old-style first and then fallback to new style
|
suppress_errors = configuration.getboolean( # read old-style first and then fallback to new style
|
||||||
"settings", "suppress_http_log_errors",
|
"settings", "suppress_http_log_errors",
|
||||||
fallback=configuration.getboolean("status", "suppress_http_log_errors", fallback=False))
|
fallback=configuration.getboolean("status", "suppress_http_log_errors", fallback=False))
|
||||||
SyncHttpClient.__init__(self, configuration, section, suppress_errors=suppress_errors)
|
SyncAhrimanClient.__init__(self, configuration, section, suppress_errors=suppress_errors)
|
||||||
|
|
||||||
self.repository_id = repository_id
|
self.repository_id = repository_id
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def session(self) -> requests.Session:
|
|
||||||
"""
|
|
||||||
get or create session
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
request.Session: created session object
|
|
||||||
"""
|
|
||||||
if urlparse(self.address).scheme == "http+unix":
|
|
||||||
import requests_unixsocket # type: ignore[import-untyped]
|
|
||||||
session: requests.Session = requests_unixsocket.Session()
|
|
||||||
session.headers["User-Agent"] = f"ahriman/{__version__}"
|
|
||||||
return session
|
|
||||||
|
|
||||||
session = requests.Session()
|
|
||||||
session.headers["User-Agent"] = f"ahriman/{__version__}"
|
|
||||||
self._login(session)
|
|
||||||
|
|
||||||
return session
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_address(configuration: Configuration) -> tuple[str, str]:
|
def parse_address(configuration: Configuration) -> tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
@ -107,33 +83,6 @@ class WebClient(Client, SyncHttpClient):
|
|||||||
address = f"http://{host}:{port}"
|
address = f"http://{host}:{port}"
|
||||||
return "web", address
|
return "web", address
|
||||||
|
|
||||||
def _login(self, session: requests.Session) -> None:
|
|
||||||
"""
|
|
||||||
process login to the service
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session(requests.Session): request session to login
|
|
||||||
"""
|
|
||||||
if self.auth is None:
|
|
||||||
return # no auth configured
|
|
||||||
|
|
||||||
username, password = self.auth
|
|
||||||
payload = {
|
|
||||||
"username": username,
|
|
||||||
"password": password,
|
|
||||||
}
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
self.make_request("POST", self._login_url(), json=payload, session=session)
|
|
||||||
|
|
||||||
def _login_url(self) -> str:
|
|
||||||
"""
|
|
||||||
get url for the login api
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: full url for web service to log in
|
|
||||||
"""
|
|
||||||
return f"{self.address}/api/v1/login"
|
|
||||||
|
|
||||||
def _logs_url(self, package_base: str) -> str:
|
def _logs_url(self, package_base: str) -> str:
|
||||||
"""
|
"""
|
||||||
get url for the logs api
|
get url for the logs api
|
||||||
|
21
tests/ahriman/core/http/conftest.py
Normal file
21
tests/ahriman/core/http/conftest.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.http import SyncAhrimanClient
|
||||||
|
from ahriman.core.status.web_client import WebClient
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ahriman_client(configuration: Configuration) -> SyncAhrimanClient:
|
||||||
|
"""
|
||||||
|
ahriman web client fixture
|
||||||
|
|
||||||
|
Args:
|
||||||
|
configuration(Configuration): configuration fixture
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SyncAhrimanClient: ahriman web client test instance
|
||||||
|
"""
|
||||||
|
configuration.set("web", "port", "8080")
|
||||||
|
_, repository_id = configuration.check_loaded()
|
||||||
|
return WebClient(repository_id, configuration)
|
81
tests/ahriman/core/http/test_sync_ahriman_client.py
Normal file
81
tests/ahriman/core/http/test_sync_ahriman_client.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
import requests_unixsocket
|
||||||
|
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from ahriman.core.http import SyncAhrimanClient
|
||||||
|
from ahriman.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
def test_session(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must create normal requests session
|
||||||
|
"""
|
||||||
|
login_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient._login")
|
||||||
|
|
||||||
|
assert isinstance(ahriman_client.session, requests.Session)
|
||||||
|
assert not isinstance(ahriman_client.session, requests_unixsocket.Session)
|
||||||
|
login_mock.assert_called_once_with(pytest.helpers.anyvar(int))
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_unix_socket(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must create unix socket session
|
||||||
|
"""
|
||||||
|
login_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient._login")
|
||||||
|
ahriman_client.address = "http+unix://path"
|
||||||
|
|
||||||
|
assert isinstance(ahriman_client.session, requests_unixsocket.Session)
|
||||||
|
login_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_login(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must login user
|
||||||
|
"""
|
||||||
|
ahriman_client.auth = (user.username, user.password)
|
||||||
|
requests_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient.make_request")
|
||||||
|
payload = {
|
||||||
|
"username": user.username,
|
||||||
|
"password": user.password
|
||||||
|
}
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
ahriman_client._login(session)
|
||||||
|
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload, session=session)
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_failed(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must suppress any exception happened during login
|
||||||
|
"""
|
||||||
|
ahriman_client.user = user
|
||||||
|
mocker.patch("requests.Session.request", side_effect=Exception())
|
||||||
|
ahriman_client._login(requests.Session())
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_failed_http_error(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must suppress HTTP exception happened during login
|
||||||
|
"""
|
||||||
|
ahriman_client.user = user
|
||||||
|
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
|
||||||
|
ahriman_client._login(requests.Session())
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_skip(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must skip login if no user set
|
||||||
|
"""
|
||||||
|
requests_mock = mocker.patch("requests.Session.request")
|
||||||
|
ahriman_client._login(requests.Session())
|
||||||
|
requests_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_url(ahriman_client: SyncAhrimanClient) -> None:
|
||||||
|
"""
|
||||||
|
must generate login url correctly
|
||||||
|
"""
|
||||||
|
assert ahriman_client._login_url().startswith(ahriman_client.address)
|
||||||
|
assert ahriman_client._login_url().endswith("/api/v1/login")
|
@ -2,7 +2,6 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
import requests_unixsocket
|
|
||||||
|
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
@ -12,29 +11,6 @@ from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
|||||||
from ahriman.models.internal_status import InternalStatus
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.user import User
|
|
||||||
|
|
||||||
|
|
||||||
def test_session(web_client: WebClient, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must create normal requests session
|
|
||||||
"""
|
|
||||||
login_mock = mocker.patch("ahriman.core.status.web_client.WebClient._login")
|
|
||||||
|
|
||||||
assert isinstance(web_client.session, requests.Session)
|
|
||||||
assert not isinstance(web_client.session, requests_unixsocket.Session)
|
|
||||||
login_mock.assert_called_once_with(pytest.helpers.anyvar(int))
|
|
||||||
|
|
||||||
|
|
||||||
def test_session_unix_socket(web_client: WebClient, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must create unix socket session
|
|
||||||
"""
|
|
||||||
login_mock = mocker.patch("ahriman.core.status.web_client.WebClient._login")
|
|
||||||
web_client.address = "http+unix://path"
|
|
||||||
|
|
||||||
assert isinstance(web_client.session, requests_unixsocket.Session)
|
|
||||||
login_mock.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_address(configuration: Configuration) -> None:
|
def test_parse_address(configuration: Configuration) -> None:
|
||||||
@ -55,57 +31,6 @@ def test_parse_address(configuration: Configuration) -> None:
|
|||||||
assert WebClient.parse_address(configuration) == ("status", "http://localhost:8082")
|
assert WebClient.parse_address(configuration) == ("status", "http://localhost:8082")
|
||||||
|
|
||||||
|
|
||||||
def test_login(web_client: WebClient, user: User, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must login user
|
|
||||||
"""
|
|
||||||
web_client.auth = (user.username, user.password)
|
|
||||||
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
|
|
||||||
payload = {
|
|
||||||
"username": user.username,
|
|
||||||
"password": user.password
|
|
||||||
}
|
|
||||||
session = requests.Session()
|
|
||||||
|
|
||||||
web_client._login(session)
|
|
||||||
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload, session=session)
|
|
||||||
|
|
||||||
|
|
||||||
def test_login_failed(web_client: WebClient, user: User, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must suppress any exception happened during login
|
|
||||||
"""
|
|
||||||
web_client.user = user
|
|
||||||
mocker.patch("requests.Session.request", side_effect=Exception())
|
|
||||||
web_client._login(requests.Session())
|
|
||||||
|
|
||||||
|
|
||||||
def test_login_failed_http_error(web_client: WebClient, user: User, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must suppress HTTP exception happened during login
|
|
||||||
"""
|
|
||||||
web_client.user = user
|
|
||||||
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
|
|
||||||
web_client._login(requests.Session())
|
|
||||||
|
|
||||||
|
|
||||||
def test_login_skip(web_client: WebClient, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must skip login if no user set
|
|
||||||
"""
|
|
||||||
requests_mock = mocker.patch("requests.Session.request")
|
|
||||||
web_client._login(requests.Session())
|
|
||||||
requests_mock.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
def test_login_url(web_client: WebClient) -> None:
|
|
||||||
"""
|
|
||||||
must generate login url correctly
|
|
||||||
"""
|
|
||||||
assert web_client._login_url().startswith(web_client.address)
|
|
||||||
assert web_client._login_url().endswith("/api/v1/login")
|
|
||||||
|
|
||||||
|
|
||||||
def test_status_url(web_client: WebClient) -> None:
|
def test_status_url(web_client: WebClient) -> None:
|
||||||
"""
|
"""
|
||||||
must generate package status url correctly
|
must generate package status url correctly
|
||||||
@ -377,11 +302,13 @@ def test_status_update(web_client: WebClient, mocker: MockerFixture) -> None:
|
|||||||
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
|
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
|
||||||
|
|
||||||
web_client.status_update(BuildStatusEnum.Unknown)
|
web_client.status_update(BuildStatusEnum.Unknown)
|
||||||
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True),
|
requests_mock.assert_called_once_with(
|
||||||
params=web_client.repository_id.query(),
|
"POST", pytest.helpers.anyvar(str, True),
|
||||||
json={
|
params=web_client.repository_id.query(),
|
||||||
"status": BuildStatusEnum.Unknown.value,
|
json={
|
||||||
})
|
"status": BuildStatusEnum.Unknown.value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_status_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> None:
|
def test_status_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user