Skip to content

Commit a88fc17

Browse files
committed
Make inner HTTP client configurable
1 parent a276e1c commit a88fc17

8 files changed

+285
-32
lines changed

Makefile

+11
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,14 @@ generate: tools ${OPENAPI_SPEC} ## Generate the Horreum client
7878
set -e ;\
7979
${PROJECT_BIN}/kiota generate -l python -c HorreumRawClient -n raw_client -d ${OPENAPI_PATH}/openapi.yaml -o ${GENERATED_CLIENT_PATH} ;\
8080
}
81+
82+
83+
##@ Example
84+
85+
.PHONY: run-basic-example
86+
run-basic-example: ## Run basic example
87+
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

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import asyncio
2+
import json
3+
4+
from kiota_abstractions.base_request_configuration import RequestConfiguration
5+
6+
from horreum import HorreumCredentials, new_horreum_client
7+
from horreum.horreum_client import HorreumClient
8+
from horreum.raw_client.api.run.test.test_request_builder import TestRequestBuilder
9+
from horreum.raw_client.models.extractor import Extractor
10+
from horreum.raw_client.models.run import Run
11+
from horreum.raw_client.models.schema import Schema
12+
from horreum.raw_client.models.test import Test
13+
from horreum.raw_client.models.transformer import Transformer
14+
15+
base_url = "http://localhost:8080"
16+
username = "user"
17+
password = "secret"
18+
19+
cleanup_data = True
20+
21+
22+
async def create_schema(client: HorreumClient, data_path: str) -> int:
23+
print(f"creating schema from {data_path}")
24+
schema_data = json.load(open(data_path), object_hook=lambda d: Schema(**d))
25+
print(schema_data)
26+
27+
schema_id = await client.raw_client.api.schema.post(schema_data)
28+
assert schema_id > 0
29+
return schema_id
30+
31+
32+
async def create_schema_transformers(client: HorreumClient, schema_id: int, data_path: str,
33+
extractors_data_path: str) -> int:
34+
print(f"creating transformer from {data_path}")
35+
transformer_data = json.load(open(data_path), object_hook=lambda d: Transformer(**d))
36+
print(transformer_data)
37+
38+
print(f"creating extractors from {extractors_data_path}")
39+
extractors_data = json.load(open(extractors_data_path),
40+
object_hook=lambda d: Extractor(**d))
41+
print(extractors_data)
42+
43+
transformer_data.extractors = extractors_data
44+
45+
transformer_id = await client.raw_client.api.schema.by_id_id(schema_id).transformers.post(transformer_data)
46+
assert transformer_id > 0
47+
return transformer_id
48+
49+
50+
async def create_test(client: HorreumClient, data_path: str) -> Test:
51+
print(f"creating test from {data_path}")
52+
53+
test_data = json.load(open(data_path), object_hook=lambda d: Test(**d))
54+
print(test_data)
55+
56+
test = await client.raw_client.api.test.post(test_data)
57+
assert test.id > 0
58+
return test
59+
60+
61+
async def set_test_transformers(client: HorreumClient, test_id: int, transformers: list[int]):
62+
await client.raw_client.api.test.by_id(test_id).transformers.post(transformers)
63+
64+
65+
async def upload_run(client: HorreumClient, test_id: int, run_path: str, run_data_path: str):
66+
print(f"uploading run from {run_path}")
67+
68+
run = json.load(open(run_path), object_hook=lambda d: Run(**d))
69+
run_data = json.load(open(run_data_path))
70+
run.data = json.dumps(run_data)
71+
print(run)
72+
73+
query_params = TestRequestBuilder.TestRequestBuilderPostQueryParameters(test=str(test_id))
74+
config = RequestConfiguration(query_parameters=query_params)
75+
await client.raw_client.api.run.test.post(run, config)
76+
77+
78+
async def setup_roadrunner_test(client: HorreumClient):
79+
print("creating roadrunner test")
80+
81+
acme_benchmark_schema_id = await create_schema(client, "./data/acme_benchmark_schema.json")
82+
acme_horreum_schema_id = await create_schema(client, "./data/acme_horreum_schema.json")
83+
84+
acme_transformers_id = await create_schema_transformers(client, acme_benchmark_schema_id,
85+
"./data/acme_transformer.json",
86+
"./data/acme_transformer_extractors.json")
87+
88+
roadrunner_test = await create_test(client, "./data/roadrunner_test.json")
89+
await set_test_transformers(client, roadrunner_test.id, [acme_transformers_id])
90+
91+
await upload_run(client, roadrunner_test.id, "./data/roadrunner_run.json", "./data/roadrunner_run_data.json")
92+
93+
94+
async def delete_all(client: HorreumClient):
95+
""" cleanup all Horreum data """
96+
97+
print("cleaning up tests")
98+
get_tests = await client.raw_client.api.test.get()
99+
for t in get_tests.tests:
100+
await client.raw_client.api.test.by_id(t.id).delete()
101+
102+
get_tests = await client.raw_client.api.test.get()
103+
assert get_tests.count == 0
104+
105+
print("cleaning up schemas")
106+
get_schemas = await client.raw_client.api.schema.get()
107+
for s in get_schemas.schemas:
108+
await client.raw_client.api.schema.by_id_id(s.id).delete()
109+
110+
get_schemas = await client.raw_client.api.schema.get()
111+
assert get_schemas.count == 0
112+
113+
114+
async def example():
115+
client = await new_horreum_client(base_url, credentials=HorreumCredentials(username=username, password=password))
116+
117+
if cleanup_data:
118+
await delete_all(client)
119+
120+
await setup_roadrunner_test(client)
121+
122+
# check data is properly injected in the server
123+
get_schemas = await client.raw_client.api.schema.get()
124+
assert get_schemas.count == 2
125+
126+
get_tests = await client.raw_client.api.test.get()
127+
assert get_tests.count == 1
128+
129+
get_runs = await client.raw_client.api.run.list_.get()
130+
assert get_runs.total == 1
131+
132+
133+
if __name__ == '__main__':
134+
asyncio.run(example())

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

0 commit comments

Comments
 (0)