---
name: python-microservice-add-owsclient
description: Guide for onboarding Python microservices to use OwsClient / AsyncOwsClient
---

# Onboarding a Python Microservice to Use OwsClient / AsyncOwsClient

## Overview

This skill document explains how to integrate `OwsClient` or `AsyncOwsClient` as a datasource in a Python microservice. Both clients are centralized HTTP clients for communicating with other OWS (Orchard Web Service) microservices. This guide uses the `python-owsclient` library (version 0.12.0+) as reference.

## What is OwsClient / AsyncOwsClient?

The `owsclient` package provides two HTTP client classes that share a common base (`BaseOwsClient`):

| | `OwsClient` | `AsyncOwsClient` |
|---|---|---|
| **I/O model** | Synchronous | Asynchronous (`async/await`) |
| **Underlying transport** | `httpx.Client` (created per request) | Persistent `httpx.AsyncClient` (reused across requests) |
| **Cleanup** | None required | Must call `await client.close()` on shutdown |
| **Best for** | Synchronous frameworks (Flask, Django) or sync contexts within async apps | Async frameworks (FastAPI with async handlers, Starlette) |

Both provide:
- Authentication and request headers for OWS service-to-service communication
- Correlation ID and request context management for distributed tracing
- A consistent interface for calling other OWS microservices (`get`, `post`, `put`, `patch`, `delete`, `head`, `graphql_query`)
- Environment awareness (dev/qa/prod) via service URL discovery

### Choosing Between OwsClient and AsyncOwsClient

- Use **`AsyncOwsClient`** when your framework supports `async/await` natively (e.g., FastAPI with async route handlers) and you want non-blocking I/O for downstream service calls.
- Use **`OwsClient`** when working in a synchronous framework (e.g., Flask, Django) or when your handlers are synchronous.
- Both can coexist in the same application if needed, but prefer one for consistency.

## Prerequisites

- Access to the `owsclient` package (version 0.12.0 or later)
- Configuration management setup (environment-based)
- Context utilities (framework-dependent):
  - **FastAPI**: `owscontext` (version 0.3.0 or later)
  - **Flask**: `owsrequest` (version 2.10.1 or later) — provides `correlation_id_getter` and `request_context_getter` wrappers that integrate with Flask's `g.request_context` and `next_correlation_id()`
- For FastAPI: a lifespan context manager
- For Flask: `flask_request.setup()` called with `add_request_context=True`

> **Note on package managers**: The examples below try to show representative package manager syntax, but always check the project's existing dependency files (`requirements.txt` / `requirements-dev.txt` for pip, `pyproject.toml` with `[tool.poetry.*]` for Poetry, `pyproject.toml` with `[project.dependencies]` for uv) and use the same package manager and format already in use.

---

# Onboarding: FastAPI Microservices

## Implementation Steps

### Step 1: Update Dependencies

Add or update the `owsclient` package using your project's package manager. Inspect the project to determine which is in use (pip, Poetry, or uv) and add to the appropriate dependency file. Ensure you use the latest compatible version of `owsclient` (0.12.0+ at minimum).


*Poetry (`pyproject.toml`):*
```toml
[tool.poetry.dependencies]
owsclient = "^0.12.0"
owscontext = "^0.3.0"
```

*pip (`requirements.txt`):*
```text
owsclient>=0.12.0
owscontext>=0.3.0
```

*uv (`pyproject.toml`):*
```toml
[project]
dependencies = [
    "owsclient>=0.12.0",
    "owscontext>=0.3.0",
]
```

Then install with the appropriate command:
```bash
# Poetry
poetry update owsclient && poetry lock

# pip
pip install -r requirements.txt

# uv
uv sync
```

### Step 2: Import Required Dependencies

In your `datasources.py` file (or equivalent), add the following imports:

**For AsyncOwsClient:**
```python
from owscontext import get_correlation_id, get_request_context
from owsclient import AsyncOwsClient
```

**For OwsClient:**
```python
from owscontext import get_correlation_id, get_request_context
from owsclient import OwsClient
```

These imports provide:
- `get_correlation_id`: Retrieves the current request's correlation ID for distributed tracing
- `get_request_context`: Retrieves the current request context
- `AsyncOwsClient` / `OwsClient`: The HTTP client class

### Step 3: Define the Datasource Key

Define a module-level constant for the datasource key:

```python
OWS_CLIENT_KEY = "OWS_CLIENT"
```

This key is used to store and retrieve the client instance from the datasources dictionary.

### Step 4: Initialize the Client in the Lifespan Context Manager

In your `datasources_lifespan` function (or your FastAPI app's lifespan context manager), initialize the client.

**AsyncOwsClient:**
```python
from contextlib import asynccontextmanager

@asynccontextmanager
async def datasources_lifespan(app: FastAPI):
    """Context manager for FastAPI application lifespan."""
    
    # ... other datasource initialization code ...
    
    ows_client = AsyncOwsClient(
        environment=config.ENVIRONMENT,
        service_name=config.SERVICE_NAME,
        correlation_id_getter=get_correlation_id,
        request_context_getter=get_request_context,
    )
    logger.info("[lifespan] Initialized async ows client")
    DATA_SOURCES[OWS_CLIENT_KEY] = ows_client
    
    try:
        yield DATA_SOURCES
    finally:
        # AsyncOwsClient maintains a persistent httpx.AsyncClient connection
        # that MUST be closed on shutdown to release resources.
        await ows_client.close()
        logger.info("[lifespan] Closed async ows client")
```

> **⚠️ IMPORTANT**: `AsyncOwsClient` uses a persistent `httpx.AsyncClient` under the hood. You **must** call `await ows_client.close()` in the `finally` block to properly release connections. Failing to do so can cause resource leaks.

**OwsClient:**
```python
from contextlib import asynccontextmanager

@asynccontextmanager
async def datasources_lifespan(app: FastAPI):
    """Context manager for FastAPI application lifespan."""
    
    # ... other datasource initialization code ...
    
    ows_client = OwsClient(
        environment=config.ENVIRONMENT,
        service_name=config.SERVICE_NAME,
        correlation_id_getter=get_correlation_id,
        request_context_getter=get_request_context,
    )
    logger.info("[lifespan] Initialized ows client")
    DATA_SOURCES[OWS_CLIENT_KEY] = ows_client
    
    try:
        yield DATA_SOURCES
    finally:
        pass
```

#### Key Parameters (both clients):

- **`environment`**: The deployment environment (dev, qa, prod). Should come from your config module.
- **`service_name`**: The name of your microservice. Should come from your config module.
- **`correlation_id_getter`**: A callable that retrieves the current request's correlation ID. Essential for distributed tracing.
- **`request_context_getter`**: A callable that retrieves the current request context.
- **`timeout`** *(optional)*: An `httpx.Timeout` instance. Defaults to `httpx.Timeout(5.0)`.
- **`retries`** *(optional)*: Number of transport-level retries. Defaults to `5`.
- **`m2m_token_manager`** *(optional)*: An `AsyncM2MTokenManager` (for `AsyncOwsClient`) or `M2MTokenManager` (for `OwsClient`) for machine-to-machine authentication.

### Step 5: Create a Dependency Injection Function

Create a getter function that can be used as a FastAPI dependency:

```python
def get_ows_client():
    """Dependency injector method for the ows client."""
    return DATA_SOURCES[OWS_CLIENT_KEY]
```

This function works the same way regardless of whether the stored instance is `OwsClient` or `AsyncOwsClient`.

### Step 6: Connect to FastAPI App Lifespan

Ensure your FastAPI app uses the datasources lifespan:

```python
from your_service.api.datasources import datasources_lifespan

app = FastAPI(
    # ... other parameters ...
    lifespan=datasources_lifespan,
)
```

### Step 7: Configure Required Middleware

> **⚠️ CRITICAL**: Both `OwsClient` and `AsyncOwsClient` depend on request context being available. You **must** ensure that `CorrelationIdMiddleware` and `RequestContextMiddleware` are properly configured in your FastAPI app.

Add the required middleware to your FastAPI app initialization:

**For Python:**
```python
from fastapi import FastAPI
from fastapi.middleware import Middleware
from owscontext.context.asgi.middleware import (
    CorrelationIdMiddleware,
    RequestContextMiddleware,
)
from your_service.api.datasources import datasources_lifespan

app = FastAPI(
    title="Your Service Name",
    lifespan=datasources_lifespan,
    middleware=[
        Middleware(CorrelationIdMiddleware),
        Middleware(RequestContextMiddleware),
        # ... other middleware ...
    ],
)
```

**Key Points:**
- `CorrelationIdMiddleware` **must** be declared before other middleware
- `RequestContextMiddleware` **must** be declared immediately after `CorrelationIdMiddleware`
- Without these middleware, the `correlation_id_getter` and `request_context_getter` functions will fail, causing the client to malfunction

## Usage in Route Handlers

### AsyncOwsClient

All methods on `AsyncOwsClient` are coroutines and must be `await`ed:

```python
from fastapi import Depends, APIRouter
from your_service.api.datasources import get_ows_client
from owsclient import AsyncOwsClient

router = APIRouter()

@router.get("/example")
async def example_endpoint(ows_client: AsyncOwsClient = Depends(get_ows_client)):
    """Example endpoint that uses AsyncOwsClient."""
    response = await ows_client.get("some-service", path="/endpoint")
    return response.json()
```

### OwsClient

`OwsClient` methods are synchronous — do **not** use `await`. In a FastAPI async handler you can still call them (they will block the event loop), or use them from sync handlers:

```python
from fastapi import Depends, APIRouter
from your_service.api.datasources import get_ows_client
from owsclient import OwsClient

router = APIRouter()

@router.get("/example")
def example_endpoint(ows_client: OwsClient = Depends(get_ows_client)):
    """Example endpoint that uses OwsClient (sync). Do NOT use await."""
    response = ows_client.get("some-service", path="/endpoint")
    return response.json()
```

## Usage in Logic Layer

### AsyncOwsClient

```python
from owsclient import AsyncOwsClient

async def some_business_logic(ows_client: AsyncOwsClient):
    """Business logic that calls OWS services."""
    response = await ows_client.post(
        "permissions-service",
        path="/check",
        json={"resource": "document", "action": "read"},
    )
    return response.json()
```

```python
@router.post("/business-operation")
async def business_operation(
    ows_client: AsyncOwsClient = Depends(get_ows_client),
):
    """Route handler that uses business logic."""
    result = await some_business_logic(ows_client)
    return result
```

### OwsClient

```python
from owsclient import OwsClient

def some_business_logic(ows_client: OwsClient):
    """Business logic that calls OWS services."""
    response = ows_client.post(
        "permissions-service",
        path="/check",
        json={"resource": "document", "action": "read"},
    )
    return response.json()
```

```python
@router.post("/business-operation")
def business_operation(
    ows_client: OwsClient = Depends(get_ows_client),
):
    """Route handler that uses business logic."""
    result = some_business_logic(ows_client)
    return result
```

## Testing

See the [Testing](#testing-1) section in Common Reference for full details on the `ows_client_mock` fixture, assertion markers, and code examples for sync and async testing.

## Configuration Requirements (FastAPI)

Ensure your config module has the following properties:

```python
# In your_service/config.py
ENVIRONMENT = os.getenv("Environment", "dev")  # dev, qa, prod
SERVICE_NAME = "your-service-name"  # e.g., "ows-product-staging"
```

## FastAPI Summary

To onboard `OwsClient` or `AsyncOwsClient` as a datasource in a FastAPI microservice:

1. ✅ Choose `AsyncOwsClient` (async handlers) or `OwsClient` (sync handlers)
2. ✅ Add `owsclient` to dependencies (and appropriate context library for your Python version)
3. ✅ Import the chosen client class and context utilities
4. ✅ Define `OWS_CLIENT_KEY` constant
5. ✅ Initialize the client in lifespan context manager (call `await client.close()` on shutdown for `AsyncOwsClient`)
6. ✅ Create `get_ows_client()` dependency injector
7. ✅ Connect datasources lifespan to FastAPI app
8. ✅ Configure `CorrelationIdMiddleware` and `RequestContextMiddleware` in FastAPI app
9. ✅ Use `Depends(get_ows_client)` in route handlers and logic (`await` calls for `AsyncOwsClient`)
10. ✅ Add tests (use `ows_client_mock` fixture from `owsclient.test`)
11. ✅ Verify correlation ID propagation in middleware

---

# Onboarding: Flask Microservices

Flask microservices use `OwsClient` (synchronous) and rely on the `owsrequest` package for correlation ID and request context integration.

> **⚠️ IMPORTANT**: `OwsClient` methods are **synchronous** — do **not** use `await` when calling them. Always call them directly: `ows_client.get(...)`, `ows_client.post(...)`, etc.

## Flask Implementation Steps

### Step 1: Update Dependencies

Add or update `owsclient` and `owsrequest` using your project's package manager. Inspect the project to determine which is in use (pip, Poetry, or uv) and add to the appropriate dependency file.

*pip (`requirements.txt`):*
```text
owsclient>=0.12.0
owsrequest>=2.10.1
```

*Poetry (`pyproject.toml`):*
```toml
[tool.poetry.dependencies]
owsclient = "^0.12.0"
owsrequest = "^2.10.1"
```

*uv (`pyproject.toml`):*
```toml
[project]
dependencies = [
    "owsclient>=0.12.0",
    "owsrequest>=2.10.1",
]
```

> **Note**: Flask microservices do **not** need `owscontext`. The `owsrequest` package provides framework-compatible context getters.

### Step 2: Ensure `flask_request.setup()` Is Configured

The `owsrequest` context integration requires `flask_request.setup()` to be called with `add_request_context=True` during app initialization. This populates `flask.g.request_context` on each request, which the `request_context_getter` reads from.

Example (from a typical Flask `api.py`):

```python
from owsrequest import flask_request

flask_request.setup(
    app,
    config.ENVIRONMENT,
    add_request_context=True,
    ...  # other options, irrelevant to this SKILL
)
```

The critical parameter is **`add_request_context=True`** — this registers a `before_request` hook that adds request context (authorization, identity ID, profile ID, profile type) to `flask.g`.

### Step 3: Import Context Getters and OwsClient

In your `api.py` (or module where you initialize the client), import:

```python
from owsclient import OwsClient
from owsrequest.ows_client import correlation_id_getter, request_context_getter
```

These getters are provided by `owsrequest.ows_client` and are specifically built for Flask:

- **`correlation_id_getter()`**: Wraps `owsrequest.flask_request.next_correlation_id()` to retrieve or generate a correlation ID from the Flask request context.
- **`request_context_getter()`**: Reads `flask.g.request_context` and returns a `RequestContext` dataclass compatible with the `owsclient` `RequestContext` protocol (maps `authorization`, `identity_id`, `profile_id`, `profile_type`).

### Step 4: Instantiate OwsClient as a Module-Level Global

Flask microservices typically create the `OwsClient` as a module-level singleton (no lifespan manager or dependency injection needed):

```python
def setup_ows_client() -> OwsClient:
    """Setup ows-client."""
    return OwsClient(
        environment=config.ENVIRONMENT,
        service_name='your-service-name',
        correlation_id_getter=correlation_id_getter,
        request_context_getter=request_context_getter,
    )

# Global ows-client instance
ows_client = setup_ows_client()
```

The `correlation_id_getter` and `request_context_getter` are called lazily during each request, so the global instance is safe to share across requests.

### Step 5 (Optional): Wire the Client Into Other Components

If other modules depend on the `OwsClient` (e.g., authorization backends), pass the global instance:

```python
from python_pdp_sdk import (
    AuthorizationBackend,
    OwsPdpClient,
    PdpAuthorizationBackend,
)

def setup_authorization_backend(ows_client: OwsClient) -> AuthorizationBackend:
    """Setup Authorization Backend."""
    ows_pdp_client = OwsPdpClient(ows_client=ows_client)
    return PdpAuthorizationBackend(ows_pdp_client)

authorization_backend = setup_authorization_backend(ows_client)
```

## Flask Usage in Models / Logic Layer

In Flask, import the global `ows_client` directly into modules that need it:

```python
from your_service.api import ows_client
from your_service.constants import service

def get_some_resource(resource_id: int):
    """Call another OWS service."""
    result = ows_client.get(
        service.SOME_SERVICE_NAME,
        path='/resource',
        params={'id': resource_id},
    )
    if result.status_code != 200:
        raise Exception(f"Service returned {result.status_code}: {result.text}")
    return result.json()
```

The `ows_client` automatically attaches the correct authorization header and correlation ID from the current Flask request context.

## Flask Usage in Route Handlers

```python
from flask import jsonify
from your_service.api import app, ows_client
from your_service.constants import service

@app.route('/example', methods=['GET'])
def example_endpoint():
    """Example handler that calls another OWS service."""
    response = ows_client.get(service.SOME_SERVICE_NAME, path='/endpoint')
    return jsonify(response.json()), response.status_code
```

Or delegate to a logic/model module (preferred pattern — keep handlers thin):

```python
from flask import jsonify
from your_service.api import app
from your_service.models import some_model

@app.route('/example', methods=['GET'])
def example_endpoint():
    """Example handler that delegates to model layer."""
    data = some_model.get_some_resource(resource_id=123)
    return jsonify(data)
```

## Flask Testing

The recommended approach is to use the `ows_client_mock` fixture from `owsclient.test` — see the [Testing](#testing-1) section in Common Reference for full details. The fixture works for Flask as well, automatically patching global `OwsClient` instances.

## Configuration Requirements (Flask)

Ensure your config module has:

```python
# In your_service/config.py
ENVIRONMENT = os.getenv('Environment', 'dev')  # dev, qa, prod
SERVICE_NAME = 'your-service-name'  # e.g., 'ows-account'
```

## Flask Summary

To onboard `OwsClient` in a Flask microservice:

1. ✅ Add `owsclient` and `owsrequest` to dependencies
2. ✅ Ensure `flask_request.setup()` is called with `add_request_context=True`
3. ✅ Import `correlation_id_getter` and `request_context_getter` from `owsrequest.ows_client`
4. ✅ Create `setup_ows_client()` function and instantiate as a module-level global
5. ✅ Wire the global `ows_client` into any dependent components (e.g., authorization backend)
6. ✅ Import `ows_client` in models/handlers and call its methods synchronously
7. ✅ Add tests (use `ows_client_mock` fixture from `owsclient.test`)

---

# Common Reference

## Method Signatures

Both clients expose the same set of HTTP methods. The first positional argument is the **service name** (used for URL discovery), and `path` is the second positional argument:

```python
# AsyncOwsClient (all return Awaitable[httpx.Response])
await ows_client.get(service_name, path, *, headers=None, correlation_id=None, **kwargs)
await ows_client.post(service_name, path, *, headers=None, correlation_id=None, **kwargs)
await ows_client.put(service_name, path, *, headers=None, correlation_id=None, **kwargs)
await ows_client.patch(service_name, path, *, headers=None, correlation_id=None, **kwargs)
await ows_client.delete(service_name, path, *, headers=None, correlation_id=None, **kwargs)
await ows_client.head(service_name, path, *, headers=None, correlation_id=None, **kwargs)
await ows_client.graphql_query(service_name, *, query, operation_name=None, variables=None, headers=None, identity_id=None, profile_id=None, profile_type=None, correlation_id=None, **kwargs)

# OwsClient (all return httpx.Response — do NOT use await)
ows_client.get(service_name, path, *, headers=None, correlation_id=None, **kwargs)
ows_client.post(service_name, path, *, headers=None, correlation_id=None, **kwargs)
ows_client.put(service_name, path, *, headers=None, correlation_id=None, **kwargs)
ows_client.patch(service_name, path, *, headers=None, correlation_id=None, **kwargs)
ows_client.delete(service_name, path, *, headers=None, correlation_id=None, **kwargs)
ows_client.head(service_name, path, *, headers=None, correlation_id=None, **kwargs)
ows_client.graphql_query(service_name, *, query, operation_name=None, variables=None, headers=None, identity_id=None, profile_id=None, profile_type=None, correlation_id=None, **kwargs)
```

Any extra `**kwargs` are forwarded to `httpx` (e.g., `json=`, `params=`, `content=`).

## Testing

This skill is distributed as a plugin, alongside another skill called `python-owsclient-unit-testing`. Please use that skill to guide you to write unit tests around the usage of `OwsClient` and `AsyncOwsClient`. This includes full details on the `ows_client_mock` fixture, assertion markers, and code examples for sync and async testing.

## Correlation ID and Distributed Tracing

Both `OwsClient` and `AsyncOwsClient` automatically integrate with your application's correlation ID mechanism. Key benefits:

- **Automatic Header Injection**: The `correlation_id_getter` ensures the current request's correlation ID is included in all downstream service calls.
- **Distributed Tracing**: Enables tracing requests across multiple microservices.
- **Debug Support**: Helps identify issues across service boundaries.

The correlation ID source depends on the framework:
- **FastAPI**: Set by `CorrelationIdMiddleware` from `owscontext.context.asgi.middleware`.
- **Flask**: Set by `owsrequest.flask_request.next_correlation_id()`, which extracts or generates a correlation ID per request.
