Skip to content

Commit fcbc31a

Browse files
Add configclass
1 parent 0bb9f60 commit fcbc31a

File tree

5 files changed

+198
-19
lines changed

5 files changed

+198
-19
lines changed

USAGE.md

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,70 @@ connect_async_client: Client = new_client(
2929
True)
3030
```
3131

32+
## Client Configuration
33+
34+
The SDK provides a `ClientConfig` class that allows you to configure the underlying httpx client. This includes SSL certificate verification and all other httpx client options.
35+
36+
### SSL Certificate Verification
37+
38+
When connecting to a 1Password Connect server using HTTPS, you may need to configure SSL certificate verification:
39+
40+
```python
41+
from onepasswordconnectsdk.config import ClientConfig
42+
43+
# Verify SSL using a custom CA certificate
44+
config = ClientConfig(cafile="path/to/ca.pem")
45+
client = new_client("https://connect.example.com", "your-token", config=config)
46+
47+
# Disable SSL verification (not recommended for production)
48+
config = ClientConfig(verify=False)
49+
client = new_client("https://connect.example.com", "your-token", config=config)
50+
```
51+
52+
### Additional Configuration Options
53+
54+
The ClientConfig class accepts all httpx client options as keyword arguments. These options are passed directly to the underlying httpx client:
55+
56+
```python
57+
# Configure timeouts and redirects
58+
config = ClientConfig(
59+
cafile="path/to/ca.pem",
60+
timeout=30.0, # 30 second timeout
61+
follow_redirects=True, # Follow HTTP redirects
62+
max_redirects=5 # Maximum number of redirects to follow
63+
)
64+
65+
# Configure proxy settings
66+
config = ClientConfig(
67+
proxies={
68+
"http://": "http://proxy.example.com",
69+
"https://": "https://proxy.example.com"
70+
}
71+
)
72+
73+
# Configure custom headers
74+
config = ClientConfig(
75+
headers={
76+
"User-Agent": "CustomApp/1.0",
77+
"X-Custom-Header": "value"
78+
}
79+
)
80+
```
81+
82+
### Async Client Configuration
83+
84+
The same configuration options work for both synchronous and asynchronous clients:
85+
86+
```python
87+
config = ClientConfig(
88+
cafile="path/to/ca.pem",
89+
timeout=30.0
90+
)
91+
async_client = new_client("https://connect.example.com", "your-token", is_async=True, config=config)
92+
```
93+
94+
For a complete list of available configuration options, see the [httpx client documentation](https://www.python-httpx.org/api/#client).
95+
3296
## Environment Variables
3397

3498
- **OP_CONNECT_TOKEN** – The token to be used to authenticate with the 1Password Connect API.
@@ -166,4 +230,4 @@ async def main():
166230
await async_client.session.aclose() # close the client gracefully when you are done
167231

168232
asyncio.run(main())
169-
```
233+
```

src/onepasswordconnectsdk/async_client.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Python AsyncClient for connecting to 1Password Connect"""
22
import httpx
33
from httpx import HTTPError
4-
from typing import Dict, List, Union
4+
from typing import Dict, List, Union, Optional
55
import os
66

77
from onepasswordconnectsdk.serializer import Serializer
8+
from onepasswordconnectsdk.config import ClientConfig
89
from onepasswordconnectsdk.utils import build_headers, is_valid_uuid, PathBuilder, get_timeout
910
from onepasswordconnectsdk.errors import (
1011
FailedToRetrieveItemException,
@@ -16,15 +17,29 @@
1617
class AsyncClient:
1718
"""Python Async Client Class"""
1819

19-
def __init__(self, url: str, token: str) -> None:
20-
"""Initialize async client"""
20+
def __init__(self, url: str, token: str, config: Optional[ClientConfig] = None) -> None:
21+
"""Initialize async client
22+
23+
Args:
24+
url (str): The url of the 1Password Connect API
25+
token (str): The 1Password Service Account token
26+
config (Optional[ClientConfig]): Optional configuration for httpx client
27+
"""
2128
self.url = url
2229
self.token = token
30+
self.config = config
2331
self.session = self.create_session(url, token)
2432
self.serializer = Serializer()
2533

2634
def create_session(self, url: str, token: str) -> httpx.AsyncClient:
27-
return httpx.AsyncClient(base_url=url, headers=self.build_headers(token), timeout=get_timeout())
35+
headers = self.build_headers(token)
36+
timeout = get_timeout()
37+
38+
if self.config:
39+
client_args = self.config.get_client_args(url, headers, timeout)
40+
return httpx.AsyncClient(**client_args)
41+
42+
return httpx.AsyncClient(base_url=url, headers=headers, timeout=timeout)
2843

2944
def build_headers(self, token: str) -> Dict[str, str]:
3045
return build_headers(token)

src/onepasswordconnectsdk/client.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
import httpx
33
from httpx import HTTPError, USE_CLIENT_DEFAULT
44
import json
5-
from typing import Dict, List, Union
5+
from typing import Dict, List, Union, Optional
66
import os
77

88
from onepasswordconnectsdk.async_client import AsyncClient
9+
from onepasswordconnectsdk.config import ClientConfig
910
from onepasswordconnectsdk.serializer import Serializer
1011
from onepasswordconnectsdk.utils import build_headers, is_valid_uuid, PathBuilder, get_timeout
1112
from onepasswordconnectsdk.errors import (
@@ -24,15 +25,29 @@
2425
class Client:
2526
"""Python Client Class"""
2627

27-
def __init__(self, url: str, token: str) -> None:
28-
"""Initialize client"""
28+
def __init__(self, url: str, token: str, config: Optional[ClientConfig] = None) -> None:
29+
"""Initialize client
30+
31+
Args:
32+
url (str): The url of the 1Password Connect API
33+
token (str): The 1Password Service Account token
34+
config (Optional[ClientConfig]): Optional configuration for httpx client
35+
"""
2936
self.url = url
3037
self.token = token
38+
self.config = config
3139
self.session = self.create_session(url, token)
3240
self.serializer = Serializer()
3341

3442
def create_session(self, url: str, token: str) -> httpx.Client:
35-
return httpx.Client(base_url=url, headers=self.build_headers(token), timeout=get_timeout())
43+
headers = self.build_headers(token)
44+
timeout = get_timeout()
45+
46+
if self.config:
47+
client_args = self.config.get_client_args(url, headers, timeout)
48+
return httpx.Client(**client_args)
49+
50+
return httpx.Client(base_url=url, headers=headers, timeout=timeout)
3651

3752
def build_headers(self, token: str) -> Dict[str, str]:
3853
return build_headers(token)
@@ -381,19 +396,21 @@ def sanitize_for_serialization(self, obj):
381396
return self.serializer.sanitize_for_serialization(obj)
382397

383398

384-
def new_client(url: str, token: str, is_async: bool = False) -> Union[AsyncClient, Client]:
399+
def new_client(url: str, token: str, is_async: bool = False, config: Optional[ClientConfig] = None) -> Union[AsyncClient, Client]:
385400
"""Builds a new client for interacting with 1Password Connect
386-
Parameters:
387-
url: The url of the 1Password Connect API
388-
token: The 1Password Service Account token
389-
is_async: Initialize async or sync client
390-
401+
402+
Args:
403+
url (str): The url of the 1Password Connect API
404+
token (str): The 1Password Service Account token
405+
is_async (bool): Initialize async or sync client
406+
config (Optional[ClientConfig]): Optional configuration for httpx client
407+
391408
Returns:
392-
Client: The 1Password Connect client
409+
Union[AsyncClient, Client]: The 1Password Connect client
393410
"""
394411
if is_async:
395-
return AsyncClient(url, token)
396-
return Client(url, token)
412+
return AsyncClient(url, token, config)
413+
return Client(url, token, config)
397414

398415

399416
def new_client_from_environment(url: str = None) -> Union[AsyncClient, Client]:

src/onepasswordconnectsdk/config.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import shlex
3-
from typing import List, Dict
3+
from typing import List, Dict, Optional
4+
import httpx
45
from onepasswordconnectsdk.client import Client
56
from onepasswordconnectsdk.models import (
67
Item,
@@ -16,6 +17,44 @@
1617
)
1718

1819

20+
class ClientConfig:
21+
"""Configuration class for 1Password Connect client.
22+
Inherits from httpx.BaseClient to support all httpx client options.
23+
"""
24+
def __init__(self, cafile: Optional[str] = None, **kwargs):
25+
"""Initialize client configuration
26+
27+
Args:
28+
cafile (Optional[str]): Path to CA certificate file for SSL verification
29+
**kwargs: Additional httpx client options
30+
"""
31+
self.cafile = cafile
32+
self.httpx_options = kwargs
33+
34+
def get_client_args(self, base_url: str, headers: Dict[str, str], timeout: float) -> Dict:
35+
"""Get arguments for httpx client initialization
36+
37+
Args:
38+
base_url (str): Base URL for the client
39+
headers (Dict[str, str]): Headers to include in requests
40+
timeout (float): Request timeout in seconds
41+
42+
Returns:
43+
Dict: Arguments for httpx client initialization
44+
"""
45+
args = {
46+
'base_url': base_url,
47+
'headers': headers,
48+
'timeout': timeout,
49+
**self.httpx_options
50+
}
51+
52+
if self.cafile:
53+
args['verify'] = self.cafile
54+
55+
return args
56+
57+
1958
def load_dict(client: Client, config: dict):
2059
"""Load: Takes a dictionary with keys specifiying the user
2160
desired naming scheme of the values to return. Each key's

src/tests/test_client_config.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import pytest
2+
from onepasswordconnectsdk.config import ClientConfig
3+
import httpx
4+
5+
def test_client_config_with_cafile():
6+
config = ClientConfig(cafile="path/to/ca.pem")
7+
args = config.get_client_args("https://test.com", {"Authorization": "Bearer token"}, 30.0)
8+
9+
assert args["verify"] == "path/to/ca.pem"
10+
assert args["base_url"] == "https://test.com"
11+
assert args["headers"] == {"Authorization": "Bearer token"}
12+
assert args["timeout"] == 30.0
13+
14+
def test_client_config_with_kwargs():
15+
config = ClientConfig(
16+
cafile="path/to/ca.pem",
17+
follow_redirects=True,
18+
timeout=60.0
19+
)
20+
args = config.get_client_args("https://test.com", {"Authorization": "Bearer token"}, 30.0)
21+
22+
assert args["verify"] == "path/to/ca.pem"
23+
assert args["follow_redirects"] == True
24+
# kwargs should override default timeout
25+
assert args["timeout"] == 60.0
26+
27+
def test_client_config_verify_override():
28+
# When verify is explicitly set in kwargs, it should override cafile
29+
config = ClientConfig(
30+
cafile="path/to/ca.pem",
31+
verify=False
32+
)
33+
args = config.get_client_args("https://test.com", {"Authorization": "Bearer token"}, 30.0)
34+
35+
assert args["verify"] == False
36+
37+
def test_client_config_no_cafile():
38+
config = ClientConfig()
39+
args = config.get_client_args("https://test.com", {"Authorization": "Bearer token"}, 30.0)
40+
41+
assert "verify" not in args
42+
assert args["base_url"] == "https://test.com"
43+
assert args["headers"] == {"Authorization": "Bearer token"}
44+
assert args["timeout"] == 30.0

0 commit comments

Comments
 (0)