Skip to content

Commit df576ee

Browse files
committed
Make inner HTTP client configurable
1 parent fcba744 commit df576ee

8 files changed

+149
-37
lines changed

Makefile

+6-2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ generate: tools ${OPENAPI_SPEC} ## Generate the Horreum client
8282

8383
##@ Example
8484

85-
.PHONY: run-example
86-
run-example: ## Run basic example
85+
.PHONY: run-basic-example
86+
run-basic-example: ## Run basic example
8787
cd examples && python basic_example.py
88+
89+
.PHONY: run-read-only-example
90+
run-read-only-example: ## Run read-only example
91+
cd examples && python read_only_example.py

docs/GET_STARTED.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ Here a very simple example:
5757
>>> import asyncio
5858

5959
# Import the constructor function
60-
>>> from horreum.horreum_client import new_horreum_client
60+
>>> from horreum.horreum_client import new_horreum_client, HorreumCredentials
6161

6262
# Initialize the client
63-
>>> client = await new_horreum_client(base_url="http://localhost:8080", username="..", password="..")
63+
>>> client = await new_horreum_client(base_url="http://localhost:8080", credentials=HorreumCredentials(username=username, password=password))
6464

6565
# Call the api using the underlying raw client, in this case retrieve the Horreum server version
6666
>>> await client.raw_client.api.config.version.get()
@@ -72,7 +72,7 @@ The previous api call is equivalent to the following `cURL`:
7272
curl --silent -X 'GET' 'http://localhost:8080/api/config/version' -H 'accept: application/json' | jq '.'
7373
```
7474

75-
Other examples can be found in the [test folder](../test), for instance:
75+
Other examples can be found in the [examples folder](../examples), for instance:
7676

7777
```bash
7878
# Import Horreum Test model

examples/basic_example.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33

44
from kiota_abstractions.base_request_configuration import RequestConfiguration
55

6-
from horreum import new_horreum_client
6+
from horreum import HorreumCredentials, new_horreum_client
77
from horreum.horreum_client import HorreumClient
8+
from horreum.raw_client.api.run.test.test_request_builder import TestRequestBuilder
89
from horreum.raw_client.models.extractor import Extractor
910
from horreum.raw_client.models.run import Run
1011
from horreum.raw_client.models.schema import Schema
1112
from horreum.raw_client.models.test import Test
1213
from horreum.raw_client.models.transformer import Transformer
13-
from horreum.raw_client.api.run.test.test_request_builder import TestRequestBuilder
1414

1515
base_url = "http://localhost:8080"
1616
username = "user"
@@ -112,7 +112,7 @@ async def delete_all(client: HorreumClient):
112112

113113

114114
async def example():
115-
client = await new_horreum_client(base_url, username, password)
115+
client = await new_horreum_client(base_url, credentials=HorreumCredentials(username=username, password=password))
116116

117117
if cleanup_data:
118118
await delete_all(client)

examples/read_only_example.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import asyncio
2+
3+
import httpx
4+
5+
from horreum import new_horreum_client, ClientConfiguration
6+
7+
DEFAULT_CONNECTION_TIMEOUT: int = 30
8+
DEFAULT_REQUEST_TIMEOUT: int = 100
9+
10+
base_url = "http://localhost:8080"
11+
username = "user"
12+
password = "secret"
13+
14+
expected_server_version = "0.13.0"
15+
expected_n_schemas = 2
16+
expected_n_tests = 1
17+
enable_assertions = False
18+
19+
20+
async def example():
21+
timeout = httpx.Timeout(DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT)
22+
http_client = httpx.AsyncClient(timeout=timeout, http2=True, verify=False)
23+
client = await new_horreum_client(base_url, client_config=ClientConfiguration(http_client=http_client))
24+
25+
server_version = await client.raw_client.api.config.version.get()
26+
print(server_version)
27+
if enable_assertions:
28+
assert server_version.version == expected_server_version
29+
30+
get_schemas = await client.raw_client.api.schema.get()
31+
print(get_schemas.count)
32+
if enable_assertions:
33+
assert get_schemas.count == expected_n_schemas
34+
35+
get_tests = await client.raw_client.api.test.get()
36+
print(get_tests.count)
37+
if enable_assertions:
38+
assert get_tests.count == expected_n_tests
39+
40+
41+
if __name__ == '__main__':
42+
asyncio.run(example())

src/horreum/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from horreum.configs import HorreumCredentials, ClientConfiguration
12
from horreum.horreum_client import new_horreum_client
23

34
__all__ = [
4-
new_horreum_client
5+
new_horreum_client,
6+
HorreumCredentials,
7+
ClientConfiguration
58
]

src/horreum/configs.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from dataclasses import dataclass
2+
from typing import Optional
3+
4+
import httpx
5+
from kiota_abstractions.request_option import RequestOption
6+
7+
8+
@dataclass(frozen=True)
9+
class HorreumCredentials:
10+
username: str = None
11+
password: str = None
12+
13+
14+
@dataclass
15+
class ClientConfiguration:
16+
# inner http async client that will be used to perform raw requests
17+
http_client: Optional[httpx.AsyncClient] = None
18+
# if true, set default middleware on the provided client
19+
use_default_middlewares: bool = True
20+
# if set use these options for default middlewares
21+
options: Optional[dict[str, RequestOption]] = None

src/horreum/horreum_client.py

+41-19
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
from importlib.metadata import version
2+
from typing import Optional
23

4+
import httpx
35
from kiota_abstractions.authentication import AuthenticationProvider
46
from kiota_abstractions.authentication.access_token_provider import AccessTokenProvider
57
from kiota_abstractions.authentication.anonymous_authentication_provider import AnonymousAuthenticationProvider
68
from kiota_abstractions.authentication.base_bearer_token_authentication_provider import (
79
BaseBearerTokenAuthenticationProvider)
810
from kiota_http.httpx_request_adapter import HttpxRequestAdapter
11+
from kiota_http.kiota_client_factory import KiotaClientFactory
912

13+
from .configs import HorreumCredentials, ClientConfiguration
1014
from .keycloak_access_provider import KeycloakAccessProvider
1115
from .raw_client.horreum_raw_client import HorreumRawClient
1216

17+
DEFAULT_CONNECTION_TIMEOUT: int = 30
18+
DEFAULT_REQUEST_TIMEOUT: int = 100
19+
1320

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

2633
class HorreumClient:
2734
__base_url: str
28-
__username: str
29-
__password: str
35+
__credentials: Optional[HorreumCredentials]
36+
__client_config: Optional[ClientConfiguration]
37+
__http_client: httpx.AsyncClient
3038

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

35-
def __init__(self, base_url: str, username: str = None, password: str = None):
44+
def __init__(self, base_url: str, credentials: Optional[HorreumCredentials],
45+
client_config: Optional[ClientConfiguration]):
3646
self.__base_url = base_url
37-
self.__username = username
38-
self.__password = password
47+
self.__credentials = credentials
48+
self.__client_config = client_config
49+
50+
if client_config and client_config.http_client and client_config.use_default_middlewares:
51+
self.__http_client = KiotaClientFactory.create_with_default_middleware(client=client_config.http_client,
52+
options=client_config.options)
53+
else:
54+
self.__http_client = client_config.http_client if client_config else None
3955

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

45-
if self.__username is not None:
46-
# Bearer token authentication
47-
access_provider = await setup_auth_provider(self.__base_url, self.__username, self.__password)
48-
self.auth_provider = BaseBearerTokenAuthenticationProvider(access_provider)
49-
elif self.__password is not None:
50-
raise RuntimeError("providing password without username, have you missed something?")
61+
if self.__credentials:
62+
if self.__credentials.username is not None:
63+
# Bearer token authentication
64+
access_provider = await setup_auth_provider(self.__base_url, self.__credentials.username,
65+
self.__credentials.password)
66+
self.auth_provider = BaseBearerTokenAuthenticationProvider(access_provider)
67+
elif self.__credentials.password is not None:
68+
raise RuntimeError("providing password without username, have you missed something?")
69+
70+
if self.__http_client:
71+
req_adapter = HttpxRequestAdapter(authentication_provider=self.auth_provider,
72+
http_client=self.__http_client)
5173
else:
52-
# Anonymous authentication
53-
self.auth_provider = AnonymousAuthenticationProvider()
74+
# rely on the Kiota default is not provided by user
75+
req_adapter = HttpxRequestAdapter(authentication_provider=self.auth_provider)
5476

55-
req_adapter = HttpxRequestAdapter(self.auth_provider)
5677
req_adapter.base_url = self.__base_url
5778

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

6889

69-
async def new_horreum_client(base_url: str, username: str = None, password: str = None) -> HorreumClient:
90+
async def new_horreum_client(base_url: str, credentials: Optional[HorreumCredentials] = None,
91+
client_config: Optional[ClientConfiguration] = None) -> HorreumClient:
7092
"""
7193
Initialize the horreum client
7294
:param base_url: horreum api base url
73-
:param username: auth username
74-
:param password: auth password
95+
:param credentials: horreum credentials in the form of username and pwd
96+
:param client_config: inner http client configuration
7597
:return: HorreumClient instance
7698
"""
77-
client = HorreumClient(base_url, username, password)
99+
client = HorreumClient(base_url, credentials=credentials, client_config=client_config)
78100
await client.setup()
79101

80102
return client

test/horreum_client_it.py

+29-9
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66
from kiota_abstractions.method import Method
77
from kiota_abstractions.request_information import RequestInformation
88

9+
from horreum import HorreumCredentials, ClientConfiguration
910
from horreum.horreum_client import new_horreum_client, HorreumClient
1011
from horreum.raw_client.api.test.test_request_builder import TestRequestBuilder
1112
from horreum.raw_client.models.protected_type_access import ProtectedType_access
1213
from horreum.raw_client.models.test import Test
1314

14-
username = "user"
15-
password = "secret"
15+
DEFAULT_CONNECTION_TIMEOUT: int = 30
16+
DEFAULT_REQUEST_TIMEOUT: int = 100
17+
18+
USERNAME = "user"
19+
PASSWORD = "secret"
1620

1721

1822
@pytest.fixture()
@@ -35,7 +39,23 @@ async def anonymous_client() -> HorreumClient:
3539
@pytest.fixture()
3640
async def authenticated_client() -> HorreumClient:
3741
print("Setting up authenticated client")
38-
client = await new_horreum_client(base_url="http://localhost:8080", username=username, password=password)
42+
client = await new_horreum_client(base_url="http://localhost:8080",
43+
credentials=HorreumCredentials(username=USERNAME, password=PASSWORD))
44+
try:
45+
await client.raw_client.api.config.version.get()
46+
except httpx.ConnectError:
47+
pytest.fail("Unable to fetch Horreum version, is Horreum running in the background?")
48+
return client
49+
50+
51+
@pytest.fixture()
52+
async def custom_authenticated_client() -> HorreumClient:
53+
print("Setting up custom authenticated client")
54+
timeout = httpx.Timeout(DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT)
55+
client = await new_horreum_client(base_url="http://localhost:8080",
56+
credentials=HorreumCredentials(username=USERNAME, password=PASSWORD),
57+
client_config=ClientConfiguration(
58+
http_client=httpx.AsyncClient(timeout=timeout, http2=True, verify=False)))
3959
try:
4060
await client.raw_client.api.config.version.get()
4161
except httpx.ConnectError:
@@ -68,7 +88,7 @@ async def test_check_auth_token(authenticated_client: HorreumClient):
6888
@pytest.mark.asyncio
6989
async def test_missing_username_with_password():
7090
with pytest.raises(RuntimeError) as ex:
71-
await new_horreum_client(base_url="http://localhost:8080", password=password)
91+
await new_horreum_client(base_url="http://localhost:8080", credentials=HorreumCredentials(password=PASSWORD))
7292
assert str(ex.value) == "providing password without username, have you missed something?"
7393

7494

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

81101

82102
@pytest.mark.asyncio
83-
async def test_check_create_test(authenticated_client: HorreumClient):
103+
async def test_check_create_test(custom_authenticated_client: HorreumClient):
84104
# Create new test
85105
t = Test(name="TestName", description="Simple test", owner="dev-team", access=ProtectedType_access.PUBLIC)
86-
created = await authenticated_client.raw_client.api.test.post(t)
106+
created = await custom_authenticated_client.raw_client.api.test.post(t)
87107
assert created is not None
88-
assert (await authenticated_client.raw_client.api.test.get()).count == 1
108+
assert (await custom_authenticated_client.raw_client.api.test.get()).count == 1
89109

90110
# TODO: we could automate setup/teardown process
91111
# Delete test
92-
await authenticated_client.raw_client.api.test.by_id(created.id).delete()
93-
assert (await authenticated_client.raw_client.api.test.get()).count == 0
112+
await custom_authenticated_client.raw_client.api.test.by_id(created.id).delete()
113+
assert (await custom_authenticated_client.raw_client.api.test.get()).count == 0

0 commit comments

Comments
 (0)