Skip to content

Commit

Permalink
Make inner HTTP client configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
lampajr committed Apr 24, 2024
1 parent fcba744 commit d2c3a76
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 34 deletions.
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ generate: tools ${OPENAPI_SPEC} ## Generate the Horreum client

##@ Example

.PHONY: run-example
run-example: ## Run basic example
.PHONY: run-basic-example
run-basic-example: ## Run basic example
cd examples && python basic_example.py

.PHONY: run-read-only-example
run-read-only-example: ## Run read-only example
cd examples && python read_only_example.py
6 changes: 3 additions & 3 deletions examples/basic_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@

from kiota_abstractions.base_request_configuration import RequestConfiguration

from horreum import new_horreum_client
from horreum import HorreumCredentials, new_horreum_client
from horreum.horreum_client import HorreumClient
from horreum.raw_client.api.run.test.test_request_builder import TestRequestBuilder
from horreum.raw_client.models.extractor import Extractor
from horreum.raw_client.models.run import Run
from horreum.raw_client.models.schema import Schema
from horreum.raw_client.models.test import Test
from horreum.raw_client.models.transformer import Transformer
from horreum.raw_client.api.run.test.test_request_builder import TestRequestBuilder

base_url = "http://localhost:8080"
username = "user"
Expand Down Expand Up @@ -112,7 +112,7 @@ async def delete_all(client: HorreumClient):


async def example():
client = await new_horreum_client(base_url, username, password)
client = await new_horreum_client(base_url, credentials=HorreumCredentials(username=username, password=password))

if cleanup_data:
await delete_all(client)
Expand Down
42 changes: 42 additions & 0 deletions examples/read_only_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import asyncio

import httpx

from horreum import new_horreum_client, ClientConfiguration

DEFAULT_CONNECTION_TIMEOUT: int = 30
DEFAULT_REQUEST_TIMEOUT: int = 100

base_url = "http://localhost:8080"
username = "user"
password = "secret"

expected_server_version = "0.13.0"
expected_n_schemas = 2
expected_n_tests = 1
enable_assertions = False


async def example():
timeout = httpx.Timeout(DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT)
http_client = httpx.AsyncClient(timeout=timeout, http2=True, verify=False)
client = await new_horreum_client(base_url, client_config=ClientConfiguration(http_client=http_client))

server_version = await client.raw_client.api.config.version.get()
print(server_version)
if enable_assertions:
assert server_version.version == expected_server_version

get_schemas = await client.raw_client.api.schema.get()
print(get_schemas.count)
if enable_assertions:
assert get_schemas.count == expected_n_schemas

get_tests = await client.raw_client.api.test.get()
print(get_tests.count)
if enable_assertions:
assert get_tests.count == expected_n_tests


if __name__ == '__main__':
asyncio.run(example())
5 changes: 4 additions & 1 deletion src/horreum/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from horreum.configs import HorreumCredentials, ClientConfiguration
from horreum.horreum_client import new_horreum_client

__all__ = [
new_horreum_client
new_horreum_client,
HorreumCredentials,
ClientConfiguration
]
21 changes: 21 additions & 0 deletions src/horreum/configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from dataclasses import dataclass
from typing import Optional

import httpx
from kiota_abstractions.request_option import RequestOption


@dataclass(frozen=True)
class HorreumCredentials:
username: str = None
password: str = None


@dataclass
class ClientConfiguration:
# inner http async client that will be used to perform raw requests
http_client: Optional[httpx.AsyncClient] = None
# if true, set default middleware on the provided client
use_default_middlewares: bool = True
# if set use these options for default middlewares
options: Optional[dict[str, RequestOption]] = None
60 changes: 41 additions & 19 deletions src/horreum/horreum_client.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
from importlib.metadata import version
from typing import Optional

import httpx
from kiota_abstractions.authentication import AuthenticationProvider
from kiota_abstractions.authentication.access_token_provider import AccessTokenProvider
from kiota_abstractions.authentication.anonymous_authentication_provider import AnonymousAuthenticationProvider
from kiota_abstractions.authentication.base_bearer_token_authentication_provider import (
BaseBearerTokenAuthenticationProvider)
from kiota_http.httpx_request_adapter import HttpxRequestAdapter
from kiota_http.kiota_client_factory import KiotaClientFactory

from .configs import HorreumCredentials, ClientConfiguration
from .keycloak_access_provider import KeycloakAccessProvider
from .raw_client.horreum_raw_client import HorreumRawClient

DEFAULT_CONNECTION_TIMEOUT: int = 30
DEFAULT_REQUEST_TIMEOUT: int = 100


async def setup_auth_provider(base_url: str, username: str, password: str) -> AccessTokenProvider:
# Use not authenticated client to fetch the auth mechanism
Expand All @@ -25,34 +32,48 @@ async def setup_auth_provider(base_url: str, username: str, password: str) -> Ac

class HorreumClient:
__base_url: str
__username: str
__password: str
__credentials: Optional[HorreumCredentials]
__client_config: Optional[ClientConfiguration]
__http_client: httpx.AsyncClient

# Raw client, this could be used to interact with the low-level api
raw_client: HorreumRawClient
auth_provider: AuthenticationProvider
# By default, set as anonymous authentication
auth_provider: AuthenticationProvider = AnonymousAuthenticationProvider()

def __init__(self, base_url: str, username: str = None, password: str = None):
def __init__(self, base_url: str, credentials: Optional[HorreumCredentials],
client_config: Optional[ClientConfiguration]):
self.__base_url = base_url
self.__username = username
self.__password = password
self.__credentials = credentials
self.__client_config = client_config

if client_config and client_config.http_client and client_config.use_default_middlewares:
self.__http_client = KiotaClientFactory.create_with_default_middleware(client=client_config.http_client,
options=client_config.options)
else:
self.__http_client = client_config.http_client if client_config else None

async def setup(self):
"""
Set up the authentication provider, based on the Horreum configuration, and the low-level horreum api client
"""

if self.__username is not None:
# Bearer token authentication
access_provider = await setup_auth_provider(self.__base_url, self.__username, self.__password)
self.auth_provider = BaseBearerTokenAuthenticationProvider(access_provider)
elif self.__password is not None:
raise RuntimeError("providing password without username, have you missed something?")
if self.__credentials:
if self.__credentials.username is not None:
# Bearer token authentication
access_provider = await setup_auth_provider(self.__base_url, self.__credentials.username,
self.__credentials.password)
self.auth_provider = BaseBearerTokenAuthenticationProvider(access_provider)
elif self.__credentials.password is not None:
raise RuntimeError("providing password without username, have you missed something?")

if self.__http_client:
req_adapter = HttpxRequestAdapter(authentication_provider=self.auth_provider,
http_client=self.__http_client)
else:
# Anonymous authentication
self.auth_provider = AnonymousAuthenticationProvider()
# rely on the Kiota default is not provided by user
req_adapter = HttpxRequestAdapter(authentication_provider=self.auth_provider)

req_adapter = HttpxRequestAdapter(self.auth_provider)
req_adapter.base_url = self.__base_url

self.raw_client = HorreumRawClient(req_adapter)
Expand All @@ -66,15 +87,16 @@ def version() -> str:
return version("horreum")


async def new_horreum_client(base_url: str, username: str = None, password: str = None) -> HorreumClient:
async def new_horreum_client(base_url: str, credentials: Optional[HorreumCredentials] = None,
client_config: Optional[ClientConfiguration] = None) -> HorreumClient:
"""
Initialize the horreum client
:param base_url: horreum api base url
:param username: auth username
:param password: auth password
:param credentials: horreum credentials in the form of username and pwd
:param client_config: inner http client configuration
:return: HorreumClient instance
"""
client = HorreumClient(base_url, username, password)
client = HorreumClient(base_url, credentials=credentials, client_config=client_config)
await client.setup()

return client
38 changes: 29 additions & 9 deletions test/horreum_client_it.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
from kiota_abstractions.method import Method
from kiota_abstractions.request_information import RequestInformation

from horreum import HorreumCredentials, ClientConfiguration
from horreum.horreum_client import new_horreum_client, HorreumClient
from horreum.raw_client.api.test.test_request_builder import TestRequestBuilder
from horreum.raw_client.models.protected_type_access import ProtectedType_access
from horreum.raw_client.models.test import Test

username = "user"
password = "secret"
DEFAULT_CONNECTION_TIMEOUT: int = 30
DEFAULT_REQUEST_TIMEOUT: int = 100

USERNAME = "user"
PASSWORD = "secret"


@pytest.fixture()
Expand All @@ -35,7 +39,23 @@ async def anonymous_client() -> HorreumClient:
@pytest.fixture()
async def authenticated_client() -> HorreumClient:
print("Setting up authenticated client")
client = await new_horreum_client(base_url="http://localhost:8080", username=username, password=password)
client = await new_horreum_client(base_url="http://localhost:8080",
credentials=HorreumCredentials(username=USERNAME, password=PASSWORD))
try:
await client.raw_client.api.config.version.get()
except httpx.ConnectError:
pytest.fail("Unable to fetch Horreum version, is Horreum running in the background?")
return client


@pytest.fixture()
async def custom_authenticated_client() -> HorreumClient:
print("Setting up custom authenticated client")
timeout = httpx.Timeout(DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT)
client = await new_horreum_client(base_url="http://localhost:8080",
credentials=HorreumCredentials(username=USERNAME, password=PASSWORD),
client_config=ClientConfiguration(
http_client=httpx.AsyncClient(timeout=timeout, http2=True, verify=False)))
try:
await client.raw_client.api.config.version.get()
except httpx.ConnectError:
Expand Down Expand Up @@ -68,7 +88,7 @@ async def test_check_auth_token(authenticated_client: HorreumClient):
@pytest.mark.asyncio
async def test_missing_username_with_password():
with pytest.raises(RuntimeError) as ex:
await new_horreum_client(base_url="http://localhost:8080", password=password)
await new_horreum_client(base_url="http://localhost:8080", credentials=HorreumCredentials(password=PASSWORD))
assert str(ex.value) == "providing password without username, have you missed something?"


Expand All @@ -80,14 +100,14 @@ async def test_check_no_tests(authenticated_client: HorreumClient):


@pytest.mark.asyncio
async def test_check_create_test(authenticated_client: HorreumClient):
async def test_check_create_test(custom_authenticated_client: HorreumClient):
# Create new test
t = Test(name="TestName", description="Simple test", owner="dev-team", access=ProtectedType_access.PUBLIC)
created = await authenticated_client.raw_client.api.test.post(t)
created = await custom_authenticated_client.raw_client.api.test.post(t)
assert created is not None
assert (await authenticated_client.raw_client.api.test.get()).count == 1
assert (await custom_authenticated_client.raw_client.api.test.get()).count == 1

# TODO: we could automate setup/teardown process
# Delete test
await authenticated_client.raw_client.api.test.by_id(created.id).delete()
assert (await authenticated_client.raw_client.api.test.get()).count == 0
await custom_authenticated_client.raw_client.api.test.by_id(created.id).delete()
assert (await custom_authenticated_client.raw_client.api.test.get()).count == 0

0 comments on commit d2c3a76

Please sign in to comment.