Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding ConfigClass for HTTPX #118

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,70 @@ connect_async_client: Client = new_client(
True)
```

## Client Configuration

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.

### SSL Certificate Verification

When connecting to a 1Password Connect server using HTTPS, you may need to configure SSL certificate verification:

```python
from onepasswordconnectsdk.config import ClientConfig

# Verify SSL using a custom CA certificate
config = ClientConfig(cafile="path/to/ca.pem")
client = new_client("https://connect.example.com", "your-token", config=config)

# Disable SSL verification (not recommended for production)
config = ClientConfig(verify=False)
client = new_client("https://connect.example.com", "your-token", config=config)
```

### Additional Configuration Options

The ClientConfig class accepts all httpx client options as keyword arguments. These options are passed directly to the underlying httpx client:

```python
# Configure timeouts and redirects
config = ClientConfig(
cafile="path/to/ca.pem",
timeout=30.0, # 30 second timeout
follow_redirects=True, # Follow HTTP redirects
max_redirects=5 # Maximum number of redirects to follow
)

# Configure proxy settings
config = ClientConfig(
proxies={
"http://": "http://proxy.example.com",
"https://": "https://proxy.example.com"
}
)

# Configure custom headers
config = ClientConfig(
headers={
"User-Agent": "CustomApp/1.0",
"X-Custom-Header": "value"
}
)
```

### Async Client Configuration

The same configuration options work for both synchronous and asynchronous clients:

```python
config = ClientConfig(
cafile="path/to/ca.pem",
timeout=30.0
)
async_client = new_client("https://connect.example.com", "your-token", is_async=True, config=config)
```

For a complete list of available configuration options, see the [httpx client documentation](https://www.python-httpx.org/api/#client).

## Environment Variables

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

asyncio.run(main())
```
```
85 changes: 85 additions & 0 deletions example/ca_file_example/list_secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Example script demonstrating how to connect to a 1Password Connect server
using CA certificate verification and list all secrets in a vault.

Shows both synchronous and asynchronous usage.
Update the configuration variables below with your values.
"""

import asyncio
from onepasswordconnectsdk.client import new_client
from onepasswordconnectsdk.config import ClientConfig

# Configuration
CONNECT_URL = "https://connect.example.com" # Your 1Password Connect server URL
TOKEN = "eyJhbGc..." # Your 1Password Connect token
VAULT_ID = "vaults_abc123" # ID of the vault to list secrets from
CA_FILE = "path/to/ca.pem" # Path to your CA certificate file

def list_vault_secrets():
"""
Connect to 1Password Connect server and list all secrets in the specified vault.
Uses CA certificate verification for secure connection.
"""
try:
# Configure client with CA certificate verification
config = ClientConfig(
cafile=CA_FILE,
timeout=30.0 # 30 second timeout
)

# Initialize client with configuration
client = new_client(CONNECT_URL, TOKEN, config=config)

# Get all items in the vault
items = client.get_items(VAULT_ID)

# Print items
print(f"\nSecrets in vault {VAULT_ID}:")
print("-" * 40)
for item in items:
print(f"- {item.title} ({item.category})")

except Exception as e:
print(f"Error: {str(e)}")


async def list_vault_secrets_async():
"""
Async version: Connect to 1Password Connect server and list all secrets in the specified vault.
Uses CA certificate verification for secure connection.
"""
try:
# Configure client with CA certificate verification
config = ClientConfig(
cafile=CA_FILE,
timeout=30.0 # 30 second timeout
)

# Initialize async client with configuration
client = new_client(CONNECT_URL, TOKEN, is_async=True, config=config)

# Get all items in the vault
items = await client.get_items(VAULT_ID)

# Print items
print(f"\nSecrets in vault {VAULT_ID} (async):")
print("-" * 40)
for item in items:
print(f"- {item.title} ({item.category})")

# Close the client gracefully
await client.session.aclose()

except Exception as e:
print(f"Error: {str(e)}")

if __name__ == "__main__":
# Run sync version
print("Running synchronous example...")
list_vault_secrets()

# Run async version
print("\nRunning asynchronous example...")
asyncio.run(list_vault_secrets_async())
23 changes: 19 additions & 4 deletions src/onepasswordconnectsdk/async_client.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Python AsyncClient for connecting to 1Password Connect"""
import httpx
from httpx import HTTPError
from typing import Dict, List, Union
from typing import Dict, List, Union, Optional
import os

from onepasswordconnectsdk.serializer import Serializer
from onepasswordconnectsdk.config import ClientConfig
from onepasswordconnectsdk.utils import build_headers, is_valid_uuid, PathBuilder, get_timeout
from onepasswordconnectsdk.errors import (
FailedToRetrieveItemException,
Expand All @@ -16,15 +17,29 @@
class AsyncClient:
"""Python Async Client Class"""

def __init__(self, url: str, token: str) -> None:
"""Initialize async client"""
def __init__(self, url: str, token: str, config: Optional[ClientConfig] = None) -> None:
"""Initialize async client

Args:
url (str): The url of the 1Password Connect API
token (str): The 1Password Service Account token
config (Optional[ClientConfig]): Optional configuration for httpx client
"""
self.url = url
self.token = token
self.config = config
self.session = self.create_session(url, token)
self.serializer = Serializer()

def create_session(self, url: str, token: str) -> httpx.AsyncClient:
return httpx.AsyncClient(base_url=url, headers=self.build_headers(token), timeout=get_timeout())
headers = self.build_headers(token)
timeout = get_timeout()

if self.config:
client_args = self.config.get_client_args(url, headers, timeout)
return httpx.AsyncClient(**client_args)

return httpx.AsyncClient(base_url=url, headers=headers, timeout=timeout)

def build_headers(self, token: str) -> Dict[str, str]:
return build_headers(token)
Expand Down
43 changes: 30 additions & 13 deletions src/onepasswordconnectsdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
import httpx
from httpx import HTTPError, USE_CLIENT_DEFAULT
import json
from typing import Dict, List, Union
from typing import Dict, List, Union, Optional
import os

from onepasswordconnectsdk.async_client import AsyncClient
from onepasswordconnectsdk.config import ClientConfig
from onepasswordconnectsdk.serializer import Serializer
from onepasswordconnectsdk.utils import build_headers, is_valid_uuid, PathBuilder, get_timeout
from onepasswordconnectsdk.errors import (
Expand All @@ -24,15 +25,29 @@
class Client:
"""Python Client Class"""

def __init__(self, url: str, token: str) -> None:
"""Initialize client"""
def __init__(self, url: str, token: str, config: Optional[ClientConfig] = None) -> None:
"""Initialize client

Args:
url (str): The url of the 1Password Connect API
token (str): The 1Password Service Account token
config (Optional[ClientConfig]): Optional configuration for httpx client
"""
self.url = url
self.token = token
self.config = config
self.session = self.create_session(url, token)
self.serializer = Serializer()

def create_session(self, url: str, token: str) -> httpx.Client:
return httpx.Client(base_url=url, headers=self.build_headers(token), timeout=get_timeout())
headers = self.build_headers(token)
timeout = get_timeout()

if self.config:
client_args = self.config.get_client_args(url, headers, timeout)
return httpx.Client(**client_args)

return httpx.Client(base_url=url, headers=headers, timeout=timeout)

def build_headers(self, token: str) -> Dict[str, str]:
return build_headers(token)
Expand Down Expand Up @@ -381,19 +396,21 @@ def sanitize_for_serialization(self, obj):
return self.serializer.sanitize_for_serialization(obj)


def new_client(url: str, token: str, is_async: bool = False) -> Union[AsyncClient, Client]:
def new_client(url: str, token: str, is_async: bool = False, config: Optional[ClientConfig] = None) -> Union[AsyncClient, Client]:
"""Builds a new client for interacting with 1Password Connect
Parameters:
url: The url of the 1Password Connect API
token: The 1Password Service Account token
is_async: Initialize async or sync client


Args:
url (str): The url of the 1Password Connect API
token (str): The 1Password Service Account token
is_async (bool): Initialize async or sync client
config (Optional[ClientConfig]): Optional configuration for httpx client

Returns:
Client: The 1Password Connect client
Union[AsyncClient, Client]: The 1Password Connect client
"""
if is_async:
return AsyncClient(url, token)
return Client(url, token)
return AsyncClient(url, token, config)
return Client(url, token, config)


def new_client_from_environment(url: str = None) -> Union[AsyncClient, Client]:
Expand Down
54 changes: 49 additions & 5 deletions src/onepasswordconnectsdk/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import os
import shlex
from typing import List, Dict
from onepasswordconnectsdk.client import Client
from typing import List, Dict, Optional, TYPE_CHECKING
import httpx

if TYPE_CHECKING:
from onepasswordconnectsdk.client import Client
from onepasswordconnectsdk.models import (
Item,
ParsedField,
Expand All @@ -16,7 +19,48 @@
)


def load_dict(client: Client, config: dict):
class ClientConfig:
"""Configuration class for 1Password Connect client.
Inherits from httpx.BaseClient to support all httpx client options.
"""
def __init__(self, cafile: Optional[str] = None, **kwargs):
"""Initialize client configuration

Args:
cafile (Optional[str]): Path to CA certificate file for SSL verification
**kwargs: Additional httpx client options
"""
self.cafile = cafile
self.httpx_options = kwargs

def get_client_args(self, base_url: str, headers: Dict[str, str], timeout: float) -> Dict:
"""Get arguments for httpx client initialization

Args:
base_url (str): Base URL for the client
headers (Dict[str, str]): Headers to include in requests
timeout (float): Request timeout in seconds

Returns:
Dict: Arguments for httpx client initialization
"""
args = {
'base_url': base_url,
'headers': headers,
'timeout': timeout,
}

# Set verify from cafile first
if self.cafile:
args['verify'] = self.cafile

# Allow httpx_options (including verify) to override
args.update(self.httpx_options)

return args


def load_dict(client: "Client", config: dict):
"""Load: Takes a dictionary with keys specifiying the user
desired naming scheme of the values to return. Each key's
value is a dictionary that includes information on where
Expand Down Expand Up @@ -83,7 +127,7 @@ def load_dict(client: Client, config: dict):
return config_values


def load(client: Client, config: object):
def load(client: "Client", config: object):
"""Load: Takes a an object with class attributes annotated with tags
describing where to find desired fields in 1Password. Manipulates given object
and fills attributes in with 1Password item field values.
Expand Down Expand Up @@ -162,7 +206,7 @@ def _vault_uuid_for_field(field: str, vault_tag: dict):


def _set_values_for_item(
client: Client,
client: "Client",
parsed_item: ParsedItem,
config_dict={},
config_object: object = None,
Expand Down
Loading