> built 2026-09-15 12:16 UTC from 789a9ca (master) · qh 0.0.17. Details: build_info.json

# index.html.md

<!-- generated by epythet -->

# qh

**Quick HTTP web-service construction** - From Python functions to production-ready HTTP services, with minimal boilerplate.

`qh` (pronounced “quick”) is a convention-over-configuration framework for exposing Python functions as HTTP services. Built on FastAPI, it provides a delightfully simple API while giving you escape hatches for advanced use cases.

```bash
pip install qh
```

<!-- epythet:agentic-readme:start -->

## For AI agents

`qh` publishes its documentation in forms made for coding agents. If you are one, start here.

**The documentation, machine-readable**: [`llms.txt`](https://i2mint.github.io/qh/llms.txt) indexes every page; [`qh.md`](https://i2mint.github.io/qh/qh.md) is the whole documentation in one file; every page has a `.md` twin; [`objects.inv`](https://i2mint.github.io/qh/objects.inv) maps symbols to URLs.

If you can’t let go of the old ways, the rest of this README is written for you, starting at [Quickstart: From Function to API in 3 Lines]().

<!-- epythet:agentic-readme:end -->

## Quickstart: From Function to API in 3 Lines

```python
from qh import mk_app

def add(x: int, y: int) -> int:
    return x + y

app = mk_app([add])
```

That’s it! You now have a FastAPI app with:

- ✅ Automatic request/response handling
- ✅ Type validation from your annotations
- ✅ OpenAPI documentation at `/docs`
- ✅ Multiple input formats (JSON body, query params, etc.)

Run it:

```bash
uvicorn your_module:app
```

Or test it:

```python
from qh.testing import test_app

with test_app(app) as client:
    response = client.post("/add", json={"x": 3, "y": 5})
    print(response.json())  # 8
```

## What You Can Do From Here

### 🚀 Async Task Processing (NEW in v0.5.0)

Handle long-running operations without blocking:

```python
import time

def expensive_computation(n: int) -> int:
    time.sleep(5)  # Simulate heavy processing
    return n * 2

# Enable async support
app = mk_app(
    [expensive_computation],
    async_funcs=['expensive_computation']
)
```

Now clients can choose sync or async execution:

```python
# Synchronous (blocks for 5 seconds)
POST /expensive_computation?n=10
→ 20

# Asynchronous (returns immediately)
POST /expensive_computation?n=10&async=true
→ {"task_id": "abc-123", "status": "submitted"}

# Check status
GET /tasks/abc-123/status
→ {"status": "running", "started_at": 1234567890}

# Get result (blocks until ready, or returns immediately if done)
GET /tasks/abc-123/result?wait=true&timeout=10
→ {"status": "completed", "result": 20}
```

**Advanced async configuration:**

```python
from qh import mk_app, TaskConfig, ProcessPoolTaskExecutor

app = mk_app(
    [cpu_bound_func, io_bound_func],
    async_funcs=['cpu_bound_func', 'io_bound_func'],
    async_config={
        'cpu_bound_func': TaskConfig(
            executor=ProcessPoolTaskExecutor(max_workers=4),  # Use processes for CPU-bound
            ttl=3600,  # Keep results for 1 hour
        ),
        'io_bound_func': TaskConfig(
            async_mode='always',  # Always async, no query param needed
        ),
    }
)
```

Task management endpoints are automatically created:

- `GET /tasks/` - List all tasks
- `GET /tasks/{id}` - Get complete task info
- `GET /tasks/{id}/status` - Get task status
- `GET /tasks/{id}/result` - Get result (with optional wait)
- `DELETE /tasks/{id}` - Cancel/delete task

### 📝 Convention-Based Routing

```python
def get_user(user_id: str):
    return {"id": user_id, "name": "Alice"}

def list_users():
    return [{"id": "1", "name": "Alice"}]

def create_user(name: str, email: str):
    return {"id": "123", "name": name, "email": email}

app = mk_app(
    [get_user, list_users, create_user],
    use_conventions=True
)
```

This automatically creates RESTful routes:

- `GET /users/{user_id}` → `get_user(user_id)`
- `GET /users` → `list_users()`
- `POST /users` → `create_user(name, email)`

### 🎯 Explicit Configuration

```python
from qh import mk_app, RouteConfig

def add(x: int, y: int) -> int:
    return x + y

app = mk_app({
    add: RouteConfig(
        path="/calculate/sum",
        methods=["GET", "POST"],
        tags=["math"],
        summary="Add two numbers"
    )
})
```

Or use dictionaries:

```python
app = mk_app({
    add: {
        "path": "/calculate/sum",
        "methods": ["GET", "POST"],
    }
})
```

### 🔄 Parameter Transformation

```python
import numpy as np
from qh import mk_app, RouteConfig, TransformSpec, HttpLocation

def add_arrays(a, b):
    return (a + b).tolist()

app = mk_app({
    add_arrays: RouteConfig(
        param_overrides={
            "a": TransformSpec(
                http_location=HttpLocation.JSON_BODY,
                ingress=np.array  # Convert JSON array to numpy
            ),
            "b": TransformSpec(
                http_location=HttpLocation.JSON_BODY,
                ingress=np.array
            )
        }
    )
})
```

Now you can send:

```bash
POST /add_arrays
{"a": [1,2,3], "b": [4,5,6]}
→ [5, 7, 9]
```

### 🌐 OpenAPI & Client Generation

```python
from qh import mk_app, export_openapi, mk_client_from_app

def greet(name: str) -> str:
    return f"Hello, {name}!"

app = mk_app([greet])

# Export OpenAPI spec
export_openapi(app, "api.json")

# Generate Python client
client = mk_client_from_app(app)
result = client.greet(name="World")  # "Hello, World!"

# Generate TypeScript client
from qh import export_ts_client
export_ts_client(app, "client.ts")
```

### 🎨 Custom Types

```python
from qh import register_type
from datetime import datetime

def custom_serializer(dt: datetime) -> str:
    return dt.isoformat()

def custom_deserializer(s: str) -> datetime:
    return datetime.fromisoformat(s)

register_type(
    datetime,
    serialize=custom_serializer,
    deserialize=custom_deserializer
)

def get_event_time(event_id: str) -> datetime:
    return datetime.now()

app = mk_app([get_event_time])
```

### ⚙️ Global Configuration

```python
from qh import mk_app, AppConfig

app = mk_app(
    funcs=[add, multiply, divide],
    config=AppConfig(
        path_prefix="/api/v1",
        default_methods=["POST"],
        title="Math API",
        version="1.0.0",
    )
)
```

### 🧪 Testing Utilities

```python
from qh import test_app, serve_app, quick_test

# Quick inline testing
with test_app(app) as client:
    response = client.post("/add", json={"x": 3, "y": 5})
    assert response.json() == 8

# Serve for external testing
with serve_app(app, port=8001) as url:
    import requests
    response = requests.post(f"{url}/add", json={"x": 3, "y": 5})

# Quick smoke test
quick_test(app)  # Tests all endpoints with example data
```

## Features

### Built-in

- ✅ **Minimal boilerplate** - Define functions, get HTTP service
- ✅ **Type-driven** - Uses Python type hints for validation
- ✅ **FastAPI-powered** - Full async support, high performance
- ✅ **Automatic OpenAPI** - Interactive docs at `/docs`
- ✅ **Client generation** - Python, TypeScript, JavaScript clients
- ✅ **Convention over configuration** - RESTful routing from function names
- ✅ **Flexible parameter handling** - JSON, query, path, headers, forms
- ✅ **Custom transformations** - Transform inputs/outputs as needed
- ✅ **Testing utilities** - Built-in test client and helpers

### Phase 4 (NEW): Async Task Processing

- ✅ **Background tasks** - Long-running operations without blocking
- ✅ **Task tracking** - Status monitoring and result retrieval
- ✅ **Flexible execution** - Thread pools, process pools, or custom executors
- ✅ **Client-controlled** - Let users choose sync vs async
- ✅ **Standard HTTP patterns** - Poll for status, wait for results
- ✅ **Task management** - List, query, cancel tasks via HTTP

## Examples

### Simple CRUD API

```python
from qh import mk_app

# In-memory database
users = {}

def create_user(name: str, email: str) -> dict:
    user_id = str(len(users) + 1)
    users[user_id] = {"id": user_id, "name": name, "email": email}
    return users[user_id]

def get_user(user_id: str) -> dict:
    return users.get(user_id, {})

def list_users() -> list:
    return list(users.values())

app = mk_app(
    [create_user, get_user, list_users],
    use_conventions=True
)
```

### File Processing with Async

```python
from qh import mk_app, TaskConfig
import time

def process_large_file(file_path: str) -> dict:
    time.sleep(10)  # Simulate heavy processing
    return {"status": "processed", "path": file_path}

app = mk_app(
    [process_large_file],
    async_funcs=['process_large_file'],
    async_config=TaskConfig(
        async_mode='always',  # Always async
        ttl=3600,  # Keep results for 1 hour
    )
)

# Client usage:
# POST /process_large_file -> Returns task_id immediately
# GET /tasks/{task_id}/result?wait=true -> Blocks until done
```

### Mixed Sync/Async API

```python
def quick_lookup(key: str) -> str:
    """Fast operation - always synchronous"""
    return cache.get(key)

def expensive_aggregation(days: int) -> dict:
    """Slow operation - supports async"""
    time.sleep(days * 2)
    return {"result": "..."}

app = mk_app(
    [quick_lookup, expensive_aggregation],
    async_funcs=['expensive_aggregation']  # Only expensive_aggregation supports async
)

# quick_lookup is always synchronous
# expensive_aggregation can be called with ?async=true
```

### Data Science API

```python
import numpy as np
import pandas as pd
from qh import mk_app, RouteConfig, TransformSpec

def analyze_data(data: pd.DataFrame) -> dict:
    return {
        "mean": data.mean().to_dict(),
        "std": data.std().to_dict()
    }

app = mk_app({
    analyze_data: RouteConfig(
        param_overrides={
            "data": TransformSpec(ingress=pd.DataFrame)
        }
    )
})

# POST /analyze_data
# {"data": {"col1": [1,2,3], "col2": [4,5,6]}}
```

## Philosophy

**Convention over configuration, but configuration when you need it.**

`qh` follows a layered approach:

1. **Simple case** - Just pass functions, get working HTTP service
2. **Common cases** - Use conventions (RESTful routing, type-driven validation)
3. **Advanced cases** - Explicit configuration for full control

You write Python functions. `qh` handles the HTTP layer.

## Comparison

| Feature                 | qh                  | FastAPI          | Flask        |
|-------------------------|---------------------|------------------|--------------|
| From functions to HTTP  | 1 line              | ~10 lines        | ~15 lines    |
| Type validation         | Automatic           | Automatic        | Manual       |
| OpenAPI docs            | Automatic           | Automatic        | Extensions   |
| Client generation       | ✅ Built-in          | ❌ External tools | ❌ Manual     |
| Convention routing      | ✅ Yes               | ❌ No             | ❌ No         |
| Async tasks             | ✅ Built-in          | ❌ Manual setup   | ❌ Extensions |
| Task tracking           | ✅ Automatic         | ❌ Manual         | ❌ Manual     |
| Learning curve          | Minutes             | Hours            | Hours        |
| Suitable for production | Yes (it’s FastAPI!) | Yes              | Yes          |

## Under the Hood

`qh` is built on:

- [FastAPI](https://fastapi.tiangolo.com/) - Modern, fast web framework
- [i2](https://github.com/i2mint/i2) - Function signature manipulation
- [Pydantic](https://pydantic-docs.helpmanual.io/) - Data validation

When you create an app with `qh`, you get a fully-featured FastAPI application. All FastAPI features are available.

## Advanced Topics

### Using au Package (External Async Backend)

The built-in async functionality is perfect for most use cases, but if you need distributed task processing, you can integrate with [au](https://github.com/i2mint/au):

```bash
pip install au
```

```python
from au import async_compute, RQBackend
from qh import mk_app, TaskConfig

# Configure au with Redis backend
@async_compute(backend=RQBackend('redis://localhost:6379'))
def heavy_computation(n: int) -> int:
    return n * 2

# Use with qh
app = mk_app([heavy_computation])
# Now heavy_computation can be distributed across multiple workers
```

### Custom Task Executors

```python
from qh import TaskExecutor, TaskConfig
from concurrent.futures import ThreadPoolExecutor

class MyCustomExecutor(TaskExecutor):
    def __init__(self):
        self.pool = ThreadPoolExecutor(max_workers=10)

    def submit_task(self, task_id, func, args, kwargs, callback):
        # Custom task submission logic
        def wrapper():
            try:
                result = func(*args, **kwargs)
                callback(task_id, result, None)
            except Exception as e:
                callback(task_id, None, e)
        self.pool.submit(wrapper)

    def shutdown(self, wait=True):
        self.pool.shutdown(wait=wait)

app = mk_app(
    [my_func],
    async_funcs=['my_func'],
    async_config=TaskConfig(executor=MyCustomExecutor())
)
```

### Middleware and Extensions

Since `qh` creates a FastAPI app, you can use all FastAPI features:

```python
from qh import mk_app
from fastapi.middleware.cors import CORSMiddleware

app = mk_app([my_func])

# Add CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# Add custom routes
@app.get("/health")
async def health():
    return {"status": "healthy"}
```

## Migration Guide

### From v0.4.0 to v0.5.0

The async task feature is fully backward compatible. Existing apps will work without changes.

To enable async:

```python
# Old (still works)
app = mk_app([my_func])

# New (with async support)
app = mk_app([my_func], async_funcs=['my_func'])
```

## Contributing

We welcome contributions! See [CONTRIBUTING.md]() for guidelines.

## License

Apache 2.0

## Links

- **Documentation**: https://github.com/i2mint/qh
- **Source Code**: https://github.com/i2mint/qh
- **Issue Tracker**: https://github.com/i2mint/qh/issues
- **Related Projects**:
  - [i2](https://github.com/i2mint/i2) - Function signature manipulation
  - [au](https://github.com/i2mint/au) - Async utilities for distributed computing
  - [FastAPI](https://fastapi.tiangolo.com/) - The underlying web framework

---

Made with ❤️ by the i2mint team

<p class="epythet-aggregates">This documentation as a single file: <a href="qh.md">qh.md</a> (Markdown, for agents).</p>


# _autosummary/qh.app.html.md

# qh.app

Build a FastAPI application from plain Python functions.

`mk_app` is the primary entry point of qh: it normalizes whatever you pass
(a callable, a list, or a dict of callables to route configs), resolves the
layered configuration from `qh.config`, applies the naming conventions from
`qh.conventions` when asked, wraps each function into a FastAPI endpoint via
`qh.endpoint`, and installs the type-hint-derived OpenAPI document from
`qh.openapi`. Functions named in `async_funcs` also get the task endpoints
of `qh.async_endpoints`.

Main entry points:

- `mk_app`: functions in, FastAPI app out
- `inspect_routes`: the registered routes as plain dicts
- `print_routes`: the same as a text table

```pycon
>>> from qh.app import mk_app, inspect_routes
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
>>> [r['path'] for r in inspect_routes(app) if r['name'] == 'add']
['/add']
```

### Functions

| [`create_app`](_autosummary/qh.app.html.md#qh.app.create_app)(funcs, \*[, app, config, ...])   | Create a FastAPI application whose routes call the given Python functions.   |
|----------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`inspect_routes`](_autosummary/qh.app.html.md#qh.app.inspect_routes)(app)                         | List the routes of a FastAPI app as plain dicts.                             |
| [`make_app`](_autosummary/qh.app.html.md#qh.app.make_app)(funcs, \*[, app, config, ...])     | Create a FastAPI application whose routes call the given Python functions.   |
| [`mk_app`](_autosummary/qh.app.html.md#qh.app.mk_app)(funcs, \*[, app, config, ...])       | Create a FastAPI application whose routes call the given Python functions.   |
| [`print_routes`](_autosummary/qh.app.html.md#qh.app.print_routes)(app)                           | Print a text table of a FastAPI app's routes (methods, path, endpoint name). |

### qh.app.create_app(funcs, , app=None, config=None, use_conventions=False, async_funcs=None, async_config=None, enhanced_openapi=True, \*\*kwargs)

Create a FastAPI application whose routes call the given Python functions.

This is the primary API for qh. It supports multiple input formats for maximum
flexibility while maintaining simplicity for common cases. Each function gets
one route; by default a `POST` at `/<function name>` taking its arguments
as a JSON object (see `RouteConfig` and `AppConfig` in `qh.config` for
what can be changed, and `qh.rules` for how parameters are mapped to HTTP).

* **Parameters:**
  * **funcs** (`Union`[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)], [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), `Union`[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)]]]) – 

    Functions to expose as HTTP endpoints. Can be:
    - A single callable
    - A list of callables
    - A dict mapping callables to their route configurations
  * **app** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[`FastAPI`]) – Optional existing FastAPI app to add routes to.
    If None, creates a new app.
  * **config** (`Union`[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`AppConfig`](_autosummary/qh.config.html.md#qh.config.AppConfig), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – 

    Optional app-level configuration. Can be:
    - AppConfig object
    - Dict that will be converted to AppConfig
    - None (uses defaults)
  * **use_conventions** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – 

    Whether to use convention-based routing.
    If True, infers paths and methods from function names:
    - get_user(user_id) → GET /users/{user_id}
    - list_users() → GET /users
    - create_user(user) → POST /users
  * **async_funcs** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`List`](https://docs.python.org/3/library/typing.html#typing.List)[`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)]]]) – List of functions (by name or reference) that should support
    async task execution. When enabled, clients can add ?async=true to
    get a task ID instead of blocking for the result.
  * **async_config** (`Union`[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – 

    Configuration for async task processing. Can be:
    - None (uses default TaskConfig for functions in async_funcs)
    - TaskConfig object (applies to all async_funcs)
    - Dict mapping function names to TaskConfig objects
  * **enhanced_openapi** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to serve an enhanced OpenAPI document at
    `/openapi.json` — one with `requestBody` / `responses` /
    `components.schemas` derived from each function’s Python type
    hints (see [`qh.openapi`](_autosummary/qh.openapi.html.md#module-qh.openapi)). Defaults to True; the enhancement is
    additive and falls back to FastAPI’s plain schema if it ever fails.
  * **\*\*kwargs** – Additional FastAPI() constructor kwargs (if creating new app)
* **Return type:**
  `FastAPI`
* **Returns:**
  The FastAPI application (the one passed as `app`, or a new one) with
  one route per function, in the order the functions were given.
* **Raises:**
  * [**TypeError**](https://docs.python.org/3/builtins/exceptions.html#TypeError) – If `config` is neither `None`, an `AppConfig`, nor a dict.
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If a per-function route config is invalid for that function
        (see `qh.endpoint.validate_route_config`), e.g. a `path` whose
        `{param}` placeholders don’t match the function’s parameters.

#### SEE ALSO
`qh.testing.test_app`: call the resulting app in-process without a server.
`qh.client.mk_client_from_app`: a Python client whose methods mirror the functions.
`qh.base.mk_fastapi_app`: the older, config-free variant kept for its tests.

### Examples

Simple case - just functions:

```pycon
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
```

With conventions:

```pycon
>>> def get_user(user_id: str): ...
>>> def list_users(): ...
>>> app = mk_app([get_user, list_users], use_conventions=True)
```

With configuration:

```pycon
>>> app = mk_app(
...     [add],
...     config={'path_prefix': '/api', 'default_methods': ['POST']}
... )
```

Per-function configuration:

```pycon
>>> app = mk_app({
...     add: {'methods': ['GET', 'POST'], 'path': '/calculate/add'},
... })
```

With async support:

```pycon
>>> def expensive_task(n: int) -> int:
...     import time
...     time.sleep(5)
...     return n * 2
>>> app = mk_app([expensive_task], async_funcs=['expensive_task'])
```

Now `POST /expensive_task?async=true` returns `{"task_id": ...}` and
`GET /tasks/{task_id}/result` returns the result when ready.

### qh.app.inspect_routes(app)

List the routes of a FastAPI app as plain dicts.

* **Parameters:**
  **app** (`FastAPI`) – FastAPI application
* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]
* **Returns:**
  One dict per route that has HTTP methods (FastAPI’s own `/docs`,
  `/redoc` and `/openapi.json` routes included), in registration
  order, with keys `path`, `methods`, `name` and `endpoint`.
  Routes made by `mk_app` also carry `function` (the original
  Python callable) and `param_specs` (the parameter-to-`TransformSpec`
  map used to build the OpenAPI document).

### Examples

```pycon
>>> from qh import mk_app, inspect_routes
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
>>> route = [r for r in inspect_routes(app) if r['name'] == 'add'][0]
>>> route['path'], route['methods'], route['function'] is add
('/add', ['POST'], True)
```

### qh.app.make_app(funcs, , app=None, config=None, use_conventions=False, async_funcs=None, async_config=None, enhanced_openapi=True, \*\*kwargs)

Create a FastAPI application whose routes call the given Python functions.

This is the primary API for qh. It supports multiple input formats for maximum
flexibility while maintaining simplicity for common cases. Each function gets
one route; by default a `POST` at `/<function name>` taking its arguments
as a JSON object (see `RouteConfig` and `AppConfig` in `qh.config` for
what can be changed, and `qh.rules` for how parameters are mapped to HTTP).

* **Parameters:**
  * **funcs** (`Union`[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)], [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), `Union`[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)]]]) – 

    Functions to expose as HTTP endpoints. Can be:
    - A single callable
    - A list of callables
    - A dict mapping callables to their route configurations
  * **app** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[`FastAPI`]) – Optional existing FastAPI app to add routes to.
    If None, creates a new app.
  * **config** (`Union`[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`AppConfig`](_autosummary/qh.config.html.md#qh.config.AppConfig), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – 

    Optional app-level configuration. Can be:
    - AppConfig object
    - Dict that will be converted to AppConfig
    - None (uses defaults)
  * **use_conventions** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – 

    Whether to use convention-based routing.
    If True, infers paths and methods from function names:
    - get_user(user_id) → GET /users/{user_id}
    - list_users() → GET /users
    - create_user(user) → POST /users
  * **async_funcs** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`List`](https://docs.python.org/3/library/typing.html#typing.List)[`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)]]]) – List of functions (by name or reference) that should support
    async task execution. When enabled, clients can add ?async=true to
    get a task ID instead of blocking for the result.
  * **async_config** (`Union`[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – 

    Configuration for async task processing. Can be:
    - None (uses default TaskConfig for functions in async_funcs)
    - TaskConfig object (applies to all async_funcs)
    - Dict mapping function names to TaskConfig objects
  * **enhanced_openapi** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to serve an enhanced OpenAPI document at
    `/openapi.json` — one with `requestBody` / `responses` /
    `components.schemas` derived from each function’s Python type
    hints (see [`qh.openapi`](_autosummary/qh.openapi.html.md#module-qh.openapi)). Defaults to True; the enhancement is
    additive and falls back to FastAPI’s plain schema if it ever fails.
  * **\*\*kwargs** – Additional FastAPI() constructor kwargs (if creating new app)
* **Return type:**
  `FastAPI`
* **Returns:**
  The FastAPI application (the one passed as `app`, or a new one) with
  one route per function, in the order the functions were given.
* **Raises:**
  * [**TypeError**](https://docs.python.org/3/builtins/exceptions.html#TypeError) – If `config` is neither `None`, an `AppConfig`, nor a dict.
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If a per-function route config is invalid for that function
        (see `qh.endpoint.validate_route_config`), e.g. a `path` whose
        `{param}` placeholders don’t match the function’s parameters.

#### SEE ALSO
`qh.testing.test_app`: call the resulting app in-process without a server.
`qh.client.mk_client_from_app`: a Python client whose methods mirror the functions.
`qh.base.mk_fastapi_app`: the older, config-free variant kept for its tests.

### Examples

Simple case - just functions:

```pycon
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
```

With conventions:

```pycon
>>> def get_user(user_id: str): ...
>>> def list_users(): ...
>>> app = mk_app([get_user, list_users], use_conventions=True)
```

With configuration:

```pycon
>>> app = mk_app(
...     [add],
...     config={'path_prefix': '/api', 'default_methods': ['POST']}
... )
```

Per-function configuration:

```pycon
>>> app = mk_app({
...     add: {'methods': ['GET', 'POST'], 'path': '/calculate/add'},
... })
```

With async support:

```pycon
>>> def expensive_task(n: int) -> int:
...     import time
...     time.sleep(5)
...     return n * 2
>>> app = mk_app([expensive_task], async_funcs=['expensive_task'])
```

Now `POST /expensive_task?async=true` returns `{"task_id": ...}` and
`GET /tasks/{task_id}/result` returns the result when ready.

### qh.app.mk_app(funcs, , app=None, config=None, use_conventions=False, async_funcs=None, async_config=None, enhanced_openapi=True, \*\*kwargs)

Create a FastAPI application whose routes call the given Python functions.

This is the primary API for qh. It supports multiple input formats for maximum
flexibility while maintaining simplicity for common cases. Each function gets
one route; by default a `POST` at `/<function name>` taking its arguments
as a JSON object (see `RouteConfig` and `AppConfig` in `qh.config` for
what can be changed, and `qh.rules` for how parameters are mapped to HTTP).

* **Parameters:**
  * **funcs** (`Union`[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)], [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), `Union`[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)]]]) – 

    Functions to expose as HTTP endpoints. Can be:
    - A single callable
    - A list of callables
    - A dict mapping callables to their route configurations
  * **app** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[`FastAPI`]) – Optional existing FastAPI app to add routes to.
    If None, creates a new app.
  * **config** (`Union`[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`AppConfig`](_autosummary/qh.config.html.md#qh.config.AppConfig), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – 

    Optional app-level configuration. Can be:
    - AppConfig object
    - Dict that will be converted to AppConfig
    - None (uses defaults)
  * **use_conventions** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – 

    Whether to use convention-based routing.
    If True, infers paths and methods from function names:
    - get_user(user_id) → GET /users/{user_id}
    - list_users() → GET /users
    - create_user(user) → POST /users
  * **async_funcs** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`List`](https://docs.python.org/3/library/typing.html#typing.List)[`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)]]]) – List of functions (by name or reference) that should support
    async task execution. When enabled, clients can add ?async=true to
    get a task ID instead of blocking for the result.
  * **async_config** (`Union`[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – 

    Configuration for async task processing. Can be:
    - None (uses default TaskConfig for functions in async_funcs)
    - TaskConfig object (applies to all async_funcs)
    - Dict mapping function names to TaskConfig objects
  * **enhanced_openapi** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to serve an enhanced OpenAPI document at
    `/openapi.json` — one with `requestBody` / `responses` /
    `components.schemas` derived from each function’s Python type
    hints (see [`qh.openapi`](_autosummary/qh.openapi.html.md#module-qh.openapi)). Defaults to True; the enhancement is
    additive and falls back to FastAPI’s plain schema if it ever fails.
  * **\*\*kwargs** – Additional FastAPI() constructor kwargs (if creating new app)
* **Return type:**
  `FastAPI`
* **Returns:**
  The FastAPI application (the one passed as `app`, or a new one) with
  one route per function, in the order the functions were given.
* **Raises:**
  * [**TypeError**](https://docs.python.org/3/builtins/exceptions.html#TypeError) – If `config` is neither `None`, an `AppConfig`, nor a dict.
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If a per-function route config is invalid for that function
        (see `qh.endpoint.validate_route_config`), e.g. a `path` whose
        `{param}` placeholders don’t match the function’s parameters.

#### SEE ALSO
`qh.testing.test_app`: call the resulting app in-process without a server.
`qh.client.mk_client_from_app`: a Python client whose methods mirror the functions.
`qh.base.mk_fastapi_app`: the older, config-free variant kept for its tests.

### Examples

Simple case - just functions:

```pycon
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
```

With conventions:

```pycon
>>> def get_user(user_id: str): ...
>>> def list_users(): ...
>>> app = mk_app([get_user, list_users], use_conventions=True)
```

With configuration:

```pycon
>>> app = mk_app(
...     [add],
...     config={'path_prefix': '/api', 'default_methods': ['POST']}
... )
```

Per-function configuration:

```pycon
>>> app = mk_app({
...     add: {'methods': ['GET', 'POST'], 'path': '/calculate/add'},
... })
```

With async support:

```pycon
>>> def expensive_task(n: int) -> int:
...     import time
...     time.sleep(5)
...     return n * 2
>>> app = mk_app([expensive_task], async_funcs=['expensive_task'])
```

Now `POST /expensive_task?async=true` returns `{"task_id": ...}` and
`GET /tasks/{task_id}/result` returns the result when ready.

### qh.app.print_routes(app)

Print a text table of a FastAPI app’s routes (methods, path, endpoint name).

* **Parameters:**
  **app** (`FastAPI`) – FastAPI application
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### Examples

```pycon
>>> from qh import mk_app, print_routes
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> print_routes(mk_app([add], config={'docs_url': None, 'redoc_url': None, 'openapi_url': None}))
METHODS  PATH  ENDPOINT
----------------------------------------------------------
POST  /add  add
```


# _autosummary/qh.async_endpoints.html.md

# qh.async_endpoints

Helper functions to create task management endpoints.

These endpoints provide standard HTTP interfaces for task status and results.

### Functions

| [`add_global_task_endpoints`](_autosummary/qh.async_endpoints.html.md#qh.async_endpoints.add_global_task_endpoints)(app[, path_prefix])     | Add global task management endpoints (cross all functions).   |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------|
| [`add_task_endpoints`](_autosummary/qh.async_endpoints.html.md#qh.async_endpoints.add_task_endpoints)(app, func_name[, path_prefix]) | Add task management endpoints for a specific function.        |

### qh.async_endpoints.add_global_task_endpoints(app, path_prefix='/tasks')

Add global task management endpoints (cross all functions).

Creates:

- GET {path_prefix}/ - List all recent tasks

* **Parameters:**
  * **app** (`FastAPI`) – FastAPI application
  * **path_prefix** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – URL path prefix for task endpoints
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### qh.async_endpoints.add_task_endpoints(app, func_name, path_prefix='/tasks')

Add task management endpoints for a specific function.

Creates the following endpoints:

- GET {path_prefix}/{task_id}/status - Get task status
- GET {path_prefix}/{task_id}/result - Get task result (waits if needed)
- GET {path_prefix}/{task_id} - Get complete task info
- DELETE {path_prefix}/{task_id} - Cancel/delete a task

* **Parameters:**
  * **app** (`FastAPI`) – FastAPI application
  * **func_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the function these tasks belong to
  * **path_prefix** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – URL path prefix for task endpoints
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/qh.async_tasks.html.md

# qh.async_tasks

Async task processing for qh.

Provides a minimal, boilerplate-free way to handle long-running operations
by returning task IDs immediately and allowing clients to poll for results.

Terminology (standard async task processing):

- Task: An asynchronous computation
- Task ID: Unique identifier for tracking a task
- Task Status: State of the task (pending, running, completed, failed)
- Task Result: The output of the completed task

Design Philosophy:

- Convention over configuration with escape hatches
- Pluggable backends (in-memory, file-based, au, Celery, etc.)
- HTTP-first patterns (query params, standard endpoints)

### Functions

| [`get_task_manager`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.get_task_manager)(func_name[, config])   | Get or create a task manager for a function.              |
|------------------------------------------------------------------------------------------|-----------------------------------------------------------|
| [`should_run_async`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.should_run_async)(request, config)       | Determine if a request should be executed asynchronously. |

### Classes

| [`InMemoryTaskStore`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.InMemoryTaskStore)([ttl])                     | Simple in-memory task storage (not persistent, single-process only).   |
|-----------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| [`ProcessPoolTaskExecutor`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.ProcessPoolTaskExecutor)([max_workers])       | Execute tasks using a process pool (good for CPU-bound tasks).         |
| [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig)([store, executor, ttl, ...])      | Configuration for async task processing.                               |
| [`TaskExecutor`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskExecutor)()                               | Abstract interface for task execution backends.                        |
| [`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)(task_id, status, created_at[, ...]) | Information about a task's state.                                      |
| [`TaskManager`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskManager)([config])                        | Manages async task execution and state.                                |
| [`TaskStatus`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskStatus)(\*values)                         | Standard task status values.                                           |
| [`TaskStore`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskStore)()                                  | Abstract interface for task storage backends.                          |
| [`ThreadPoolTaskExecutor`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.ThreadPoolTaskExecutor)([max_workers])        | Execute tasks using a thread pool (good for I/O-bound tasks).          |

### *class* qh.async_tasks.InMemoryTaskStore(ttl=None)

Bases: [`TaskStore`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskStore)

Simple in-memory task storage (not persistent, single-process only).

#### create_task(task_id, func_name)

`TaskStore.create_task`: record a new pending task in memory.

* **Return type:**
  [`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)

#### delete_task(task_id)

`TaskStore.delete_task`: remove a task, returning whether it existed.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### get_task(task_id)

`TaskStore.get_task`: look up a task, or `None` if unknown.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### list_tasks(limit=100)

`TaskStore.list_tasks`: the `limit` most recently created tasks, newest first.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### update_task(task_info)

`TaskStore.update_task`: overwrite the stored record for its task ID.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.async_tasks.ProcessPoolTaskExecutor(max_workers=None)

Bases: [`TaskExecutor`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskExecutor)

Execute tasks using a process pool (good for CPU-bound tasks).

#### shutdown(wait=True)

`TaskExecutor.shutdown`: shut down the underlying process pool.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### submit_task(task_id, func, args, kwargs, callback)

`TaskExecutor.submit_task`: run `func` in a worker process, calling
`callback` with its result or exception when the future resolves.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.async_tasks.TaskConfig(store=None, executor=None, ttl=3600, async_mode='query', async_param='async', async_header='X-Async', create_task_endpoints=True, default_executor='thread')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Configuration for async task processing.

This is the explicit configuration. The convention is to use sane defaults.
Pass an instance (or a dict) as `async_config` to `qh.mk_app`; `store`
and `executor` are built lazily by `get_store`/`get_executor` from
`default_executor` when left `None`.

```pycon
>>> tc = TaskConfig(ttl=60, default_executor='thread')
>>> tc.ttl, tc.async_mode
(60, 'query')
>>> type(tc.get_executor()).__name__
'ThreadPoolTaskExecutor'
```

#### get_executor()

Get or create the task executor.

* **Return type:**
  [`TaskExecutor`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskExecutor)

#### get_store()

Get or create the task store.

* **Return type:**
  [`TaskStore`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskStore)

### *class* qh.async_tasks.TaskExecutor

Bases: [`ABC`](https://docs.python.org/3/library/abc.html#abc.ABC)

Abstract interface for task execution backends.

#### *abstractmethod* shutdown(wait=True)

Shutdown the executor.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### *abstractmethod* submit_task(task_id, func, args, kwargs, callback)

Submit a task for execution.

* **Parameters:**
  * **task_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Unique task identifier
  * **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – Function to execute
  * **args** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)) – Positional arguments
  * **kwargs** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)) – Keyword arguments
  * **callback** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any), [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception)]], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – Called when task completes with (task_id, result, error)
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.async_tasks.TaskInfo(task_id, status, created_at, started_at=None, completed_at=None, result=None, error=None, traceback=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Information about a task’s state.

#### to_dict()

Convert to dictionary for JSON serialization.

* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *class* qh.async_tasks.TaskManager(config=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Manages async task execution and state.

This is the main coordinator between stores, executors, and HTTP handlers.

#### cancel_task(task_id)

Cancel a task (if possible).

* **Parameters:**
  **task_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Task identifier
* **Return type:**
  [*bool*](https://docs.python.org/3/builtins/functions.html#bool)

#### NOTE
Cancellation is best-effort and may not work for all executors.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)
* **Returns:**
  True if task was cancelled or deleted

#### create_task(func, args=(), kwargs=None)

Create and submit a new task.

* **Parameters:**
  * **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – Function to execute asynchronously
  * **args** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)) – Positional arguments
  * **kwargs** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]) – Keyword arguments
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  Task ID

#### get_result(task_id, wait=False, timeout=None)

Get task result.

* **Parameters:**
  * **task_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Task identifier
  * **wait** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to block until task completes
  * **timeout** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]) – Maximum time to wait in seconds (None = wait forever)
* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)
* **Returns:**
  Task result if completed
* **Raises:**
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If task not found or failed
  * [**TimeoutError**](https://docs.python.org/3/builtins/exceptions.html#TimeoutError) – If wait times out

#### get_status(task_id)

Get task status and metadata.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### list_tasks(limit=100)

List recent tasks.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### shutdown()

Shutdown the task manager and its executor.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.async_tasks.TaskStatus(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

Standard task status values.

### *class* qh.async_tasks.TaskStore

Bases: [`ABC`](https://docs.python.org/3/library/abc.html#abc.ABC)

Abstract interface for task storage backends.

#### *abstractmethod* create_task(task_id, func_name)

Create a new task record.

* **Return type:**
  [`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)

#### *abstractmethod* delete_task(task_id)

Delete a task. Returns True if deleted, False if not found.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### *abstractmethod* get_task(task_id)

Retrieve task information.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### *abstractmethod* list_tasks(limit=100)

List recent tasks.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### *abstractmethod* update_task(task_info)

Update task information.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.async_tasks.ThreadPoolTaskExecutor(max_workers=None)

Bases: [`TaskExecutor`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskExecutor)

Execute tasks using a thread pool (good for I/O-bound tasks).

#### shutdown(wait=True)

`TaskExecutor.shutdown`: shut down the underlying thread pool.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### submit_task(task_id, func, args, kwargs, callback)

`TaskExecutor.submit_task`: run `func` on the thread pool, calling
`callback` with its result or exception when it finishes.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### qh.async_tasks.get_task_manager(func_name, config=None)

Get or create a task manager for a function.

* **Parameters:**
  * **func_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the function
  * **config** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig)]) – Task configuration (only used when creating new manager)
* **Return type:**
  [`TaskManager`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskManager)
* **Returns:**
  TaskManager instance

### qh.async_tasks.should_run_async(request, config)

Determine if a request should be executed asynchronously.

* **Parameters:**
  * **request** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – FastAPI Request object
  * **config** ([`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig)) – Task configuration
* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)
* **Returns:**
  True if request should be async


# _autosummary/qh.au_integration.html.md

# qh.au_integration

Integration layer between qh and au.

This module provides adapters to use au’s powerful backend/storage system
with qh’s user-friendly HTTP interface.

Philosophy:

- qh provides the HTTP layer (each function gets its own endpoint)
- au provides the execution backend and result storage
- This module bridges them together

### Functions

| [`use_au_backend`](_autosummary/qh.au_integration.html.md#qh.au_integration.use_au_backend)([backend, store])            | Create a qh TaskConfig that uses au backend and storage.   |
|----------------------------------------------------------------------------------------------|------------------------------------------------------------|
| [`use_au_process_backend`](_autosummary/qh.au_integration.html.md#qh.au_integration.use_au_process_backend)([storage_path, ...]) | Use au's ProcessBackend for CPU-bound tasks.               |
| [`use_au_redis_backend`](_autosummary/qh.au_integration.html.md#qh.au_integration.use_au_redis_backend)([redis_url, ...])      | Use au's Redis/RQ backend for distributed tasks.           |
| [`use_au_thread_backend`](_autosummary/qh.au_integration.html.md#qh.au_integration.use_au_thread_backend)([storage_path, ...])  | Use au's ThreadBackend with filesystem storage.            |

### Classes

| [`AuTaskExecutor`](_autosummary/qh.au_integration.html.md#qh.au_integration.AuTaskExecutor)(au_backend, au_store)   | Adapter to use au's ComputationBackend as qh's TaskExecutor.   |
|-----------------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`AuTaskStore`](_autosummary/qh.au_integration.html.md#qh.au_integration.AuTaskStore)(au_store)                  | Adapter to use au's ComputationStore as qh's TaskStore.        |

### *class* qh.au_integration.AuTaskExecutor(au_backend, au_store)

Bases: [`TaskExecutor`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskExecutor)

Adapter to use au’s ComputationBackend as qh’s TaskExecutor.

Delegates task execution to au’s backend system.

#### shutdown(wait=True)

Shutdown the executor.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### submit_task(task_id, func, args, kwargs, callback)

Submit a task to au backend.

#### NOTE
au handles result storage internally, so we don’t use the callback.
The callback is for qh’s built-in backends, but au’s store handles this.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.au_integration.AuTaskStore(au_store)

Bases: [`TaskStore`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskStore)

Adapter to use au’s ComputationStore as qh’s TaskStore.

Maps between qh’s TaskInfo and au’s computation results.

#### create_task(task_id, func_name)

Create a new task record.

* **Return type:**
  [`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)

#### delete_task(task_id)

Delete `task_id` from the au store, returning whether it was present.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### get_task(task_id)

Retrieve task information from au store.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### list_tasks(limit=100)

List recent tasks.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### update_task(task_info)

Update task information.

#### NOTE
au manages its own state, so this is mostly a no-op.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### qh.au_integration.use_au_backend(backend=None, store=None, \*\*au_config_kwargs)

Create a qh TaskConfig that uses au backend and storage.

This is the main bridge function that lets qh use au.

* **Parameters:**
  * **backend** ([`None`](https://docs.python.org/3/builtins/constants.html#None)) – au ComputationBackend (ThreadBackend, ProcessBackend, RQBackend, etc.)
    If None, uses au’s default from config
  * **store** ([`None`](https://docs.python.org/3/builtins/constants.html#None)) – au ComputationStore (FileSystemStore, etc.)
    If None, uses au’s default from config
  * **\*\*au_config_kwargs** – Additional config passed to au
* **Return type:**
  [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig)
* **Returns:**
  TaskConfig configured to use au
* **Raises:**
  [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – If the `au` package is not installed.

### Example

```pycon
>>> from au import ThreadBackend, FileSystemStore
>>> from qh import mk_app
>>> from qh.au_integration import use_au_backend
>>> # Use au with thread backend and filesystem storage
>>> def slow_func(n: int) -> int:
...     import time
...     time.sleep(2)
...     return n * 2
>>> app = mk_app(
...     [slow_func],
...     async_funcs=['slow_func'],
...     async_config=use_au_backend(
...         backend=ThreadBackend(),
...         store=FileSystemStore('/tmp/qh_tasks')
...     )
... )
```

Example with au’s global config:

```pycon
>>> # Set AU environment variables:
>>> # AU_BACKEND=redis
>>> # AU_REDIS_URL=redis://localhost:6379
>>> # AU_STORAGE=filesystem
>>> # AU_STORAGE_PATH=/var/qh/tasks
>>> app = mk_app(
...     [slow_func],
...     async_funcs=['slow_func'],
...     async_config=use_au_backend()  # Uses au's config
... )
```

### qh.au_integration.use_au_process_backend(storage_path='/tmp/qh_au_tasks', ttl_seconds=3600)

Use au’s ProcessBackend for CPU-bound tasks.

* **Return type:**
  [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig)

### qh.au_integration.use_au_redis_backend(redis_url='redis://localhost:6379', storage_path='/tmp/qh_au_tasks', ttl_seconds=3600)

Use au’s Redis/RQ backend for distributed tasks.

* **Return type:**
  [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig)

### qh.au_integration.use_au_thread_backend(storage_path='/tmp/qh_au_tasks', ttl_seconds=3600)

Use au’s ThreadBackend with filesystem storage.

* **Return type:**
  [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig)


# _autosummary/qh.base.html.md

# qh.base

Config-free dispatch of Python callables as FastAPI routes, plus a store dispatcher.

A lighter predecessor of `qh.app.mk_app` with a dict-based route config
(`path`, `methods`, `input_trans`, `output_trans`, `defaults`,
`summary`, `tags`). Every endpoint reads its arguments from the JSON body
merged with path parameters, and answers `422` for a missing required
argument and `500` for an exception raised by the function. Importing this
module also patches `fastapi.testclient.TestClient.get` to accept a `json`
body. Not used by `qh.app`; kept for its tests and for `mk_store_dispatcher`.

Main entry points:

- `mk_fastapi_app`: callables (or a dict of callable to config) in, FastAPI app out
- `mk_store_dispatcher`: key/value routes over a `store_getter(store_id)` mapping
- `mk_json_ingress` / `mk_json_egress`: per-key and per-type transforms for the above

### Functions

| [`mk_fastapi_app`](_autosummary/qh.base.html.md#qh.base.mk_fastapi_app)(funcs, \*[, app, path_prefix, ...])   | Expose Python callables as FastAPI routes.                                               |
|-------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| [`mk_json_egress`](_autosummary/qh.base.html.md#qh.base.mk_json_egress)(transform_map)                        | Create an output transformer that applies functions based on the return type.            |
| [`mk_json_ingress`](_autosummary/qh.base.html.md#qh.base.mk_json_ingress)(transform_map)                       | Create an input transformer that applies functions to specific keys in the request JSON. |
| [`mk_store_dispatcher`](_autosummary/qh.base.html.md#qh.base.mk_store_dispatcher)(store_getter, \*[, ...])         | Create store dispatcher routes using mk_fastapi_app.                                     |
| [`name_based_ingress`](_autosummary/qh.base.html.md#qh.base.name_based_ingress)(\*\*kw)                           | Alias for mk_json_ingress with named transforms.                                         |

### qh.base.mk_fastapi_app(funcs, , app=None, path_prefix='', default_methods=None, path_template='/{func_name}')

Expose Python callables as FastAPI routes.

funcs can be:

> - dict mapping func -> RouteConfig dict
> - list of callables or dicts with ‘func’ key
> - single callable

RouteConfig keys: path, methods, input_trans, output_trans, defaults, summary, tags

* **Return type:**
  `FastAPI`

### qh.base.mk_json_egress(transform_map)

Create an output transformer that applies functions based on the return type.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### qh.base.mk_json_ingress(transform_map)

Create an input transformer that applies functions to specific keys in the request JSON.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### qh.base.mk_store_dispatcher(store_getter, , path_prefix='/stores', \*\*config)

Create store dispatcher routes using mk_fastapi_app.

* **Return type:**
  `FastAPI`

### qh.base.name_based_ingress(\*\*kw)

Alias for mk_json_ingress with named transforms.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]


# _autosummary/qh.client.html.md

# qh.client

Python client generation from OpenAPI specs.

Generates client-side Python functions that call HTTP endpoints,
preserving the original function signatures and behavior.

### Functions

| [`mk_client_from_app`](_autosummary/qh.client.html.md#qh.client.mk_client_from_app)(app[, base_url])              | Create an HTTP client from a FastAPI app (for testing).    |
|---------------------------------------------------------------------------------------------------|------------------------------------------------------------|
| [`mk_client_from_openapi`](_autosummary/qh.client.html.md#qh.client.mk_client_from_openapi)(openapi_spec[, ...])      | Create an HTTP client from an OpenAPI specification.       |
| [`mk_client_from_url`](_autosummary/qh.client.html.md#qh.client.mk_client_from_url)(openapi_url[, base_url, ...]) | Create an HTTP client by fetching OpenAPI spec from a URL. |

### Classes

| [`HttpClient`](_autosummary/qh.client.html.md#qh.client.HttpClient)(base_url[, session])   | Client for calling HTTP endpoints with Python function interface.   |
|------------------------------------------------------------------------------------|---------------------------------------------------------------------|

### *class* qh.client.HttpClient(base_url, session=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Client for calling HTTP endpoints with Python function interface.

Generated functions preserve original signatures and make HTTP requests
under the hood.

#### add_function(name, path, method, signature_info=None)

Add a function to the client.

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Function name
  * **path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – HTTP path (may contain {param} placeholders)
  * **method** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – HTTP method (GET, POST, etc.)
  * **signature_info** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – Optional x-python-signature metadata
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### qh.client.mk_client_from_app(app, base_url='http://testserver')

Create an HTTP client from a FastAPI app (for testing).

* **Parameters:**
  * **app** – FastAPI application
  * **base_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Base URL for API requests (default for TestClient)
* **Return type:**
  [`HttpClient`](_autosummary/qh.client.html.md#qh.client.HttpClient)
* **Returns:**
  HttpClient that uses FastAPI TestClient under the hood

### Example

```pycon
>>> from qh import mk_app
>>> from qh.client import mk_client_from_app
>>> app = mk_app([add, subtract])
>>> client = mk_client_from_app(app)
>>> result = client.add(x=3, y=5)
```

### qh.client.mk_client_from_openapi(openapi_spec, base_url='http://localhost:8000', session=None)

Create an HTTP client from an OpenAPI specification.

* **Parameters:**
  * **openapi_spec** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – The parsed OpenAPI document (as from `export_openapi`
    or `json.load` on a spec file), used to build one client function
    per operation.
  * **base_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Base URL for API requests
  * **session** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[`Session`]) – An existing session to reuse (e.g. for shared auth/headers);
    a new one is created if not given.
* **Return type:**
  [`HttpClient`](_autosummary/qh.client.html.md#qh.client.HttpClient)
* **Returns:**
  HttpClient with functions for each endpoint

### Example

```pycon
>>> from qh.client import mk_client_from_openapi
>>> spec = {'paths': {'/add': {...}}, ...}
>>> client = mk_client_from_openapi(spec, 'http://localhost:8000')
>>> result = client.add(x=3, y=5)
```

### qh.client.mk_client_from_url(openapi_url, base_url=None, session=None)

Create an HTTP client by fetching OpenAPI spec from a URL.

* **Parameters:**
  * **openapi_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – URL to OpenAPI JSON spec (e.g., “[http://localhost:8000/openapi.json](http://localhost:8000/openapi.json)”)
  * **base_url** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Base URL for API requests (defaults to same as openapi_url)
  * **session** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[`Session`]) – An existing session to reuse; a new one is created if not given.
* **Return type:**
  [`HttpClient`](_autosummary/qh.client.html.md#qh.client.HttpClient)
* **Returns:**
  HttpClient with functions for each endpoint

### Example

```pycon
>>> from qh.client import mk_client_from_url
>>> client = mk_client_from_url('http://localhost:8000/openapi.json')
>>> result = client.add(x=3, y=5)
```


# _autosummary/qh.config.html.md

# qh.config

Configuration system for qh with layered defaults.

Configuration flows from general to specific:

1. Global defaults
2. App-level config
3. Function-level config
4. Parameter-level config

### Functions

| [`from_dict`](_autosummary/qh.config.html.md#qh.config.from_dict)(config_dict)                        | Create AppConfig from dictionary.                               |
|------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
| [`normalize_funcs_input`](_autosummary/qh.config.html.md#qh.config.normalize_funcs_input)(funcs)                  | Normalize various input formats to Dict[Callable, RouteConfig]. |
| [`resolve_route_config`](_autosummary/qh.config.html.md#qh.config.resolve_route_config)(func, app_config[, ...]) | Resolve complete route configuration for a function.            |

### Classes

| [`AppConfig`](_autosummary/qh.config.html.md#qh.config.AppConfig)([default_methods, path_template, ...])   | Global configuration for the entire FastAPI app.               |
|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`ConfigBuilder`](_autosummary/qh.config.html.md#qh.config.ConfigBuilder)()                                    | Fluent interface for building configurations.                  |
| [`FunctionConfigBuilder`](_autosummary/qh.config.html.md#qh.config.FunctionConfigBuilder)(parent, func)                | Fluent interface for building function-specific configuration. |
| [`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)([path, methods, rule_chain, ...])      | Configuration for a single route (function endpoint).          |

### *class* qh.config.AppConfig(default_methods=<factory>, path_template='/{func_name}', path_prefix='', rule_chain=<factory>, title='qh API', version='0.1.0', docs_url='/docs', redoc_url='/redoc', openapi_url='/openapi.json', fastapi_kwargs=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Global configuration for the entire FastAPI app.

```pycon
>>> ac = AppConfig(path_prefix='/api')
>>> ac.default_methods, ac.path_prefix
(['POST'], '/api')
```

#### to_fastapi_kwargs()

Convert to FastAPI() constructor kwargs.

* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *class* qh.config.ConfigBuilder

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Fluent interface for building configurations.

#### build()

Build final configuration.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`AppConfig`](_autosummary/qh.config.html.md#qh.config.AppConfig), [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), [`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)]]

#### for_function(func)

Start configuring a specific function.

* **Return type:**
  [`FunctionConfigBuilder`](_autosummary/qh.config.html.md#qh.config.FunctionConfigBuilder)

#### with_default_methods(methods)

Set default HTTP methods.

* **Return type:**
  [`ConfigBuilder`](_autosummary/qh.config.html.md#qh.config.ConfigBuilder)

#### with_path_prefix(prefix)

Set path prefix for all routes.

* **Return type:**
  [`ConfigBuilder`](_autosummary/qh.config.html.md#qh.config.ConfigBuilder)

#### with_path_template(template)

Set path template for auto-generation.

* **Return type:**
  [`ConfigBuilder`](_autosummary/qh.config.html.md#qh.config.ConfigBuilder)

#### with_rule_chain(chain)

Set global rule chain.

* **Return type:**
  [`ConfigBuilder`](_autosummary/qh.config.html.md#qh.config.ConfigBuilder)

### *class* qh.config.FunctionConfigBuilder(parent, func)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Fluent interface for building function-specific configuration.

#### at_path(path)

Set custom path for this function.

* **Return type:**
  [`FunctionConfigBuilder`](_autosummary/qh.config.html.md#qh.config.FunctionConfigBuilder)

#### done()

Finish configuring this function.

* **Return type:**
  [`ConfigBuilder`](_autosummary/qh.config.html.md#qh.config.ConfigBuilder)

#### with_methods(methods)

Set HTTP methods for this function.

* **Return type:**
  [`FunctionConfigBuilder`](_autosummary/qh.config.html.md#qh.config.FunctionConfigBuilder)

#### with_summary(summary)

Set OpenAPI summary.

* **Return type:**
  [`FunctionConfigBuilder`](_autosummary/qh.config.html.md#qh.config.FunctionConfigBuilder)

#### with_tags(tags)

Set OpenAPI tags.

* **Return type:**
  [`FunctionConfigBuilder`](_autosummary/qh.config.html.md#qh.config.FunctionConfigBuilder)

### *class* qh.config.RouteConfig(path=None, methods=None, rule_chain=None, param_overrides=<factory>, async_config=None, summary=None, description=None, tags=None, response_model=None, include_in_schema=True, deprecated=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Configuration for a single route (function endpoint).

```pycon
>>> rc = RouteConfig(methods=['GET'])
>>> merged = rc.merge_with(RouteConfig(path='/x'))
>>> merged.methods, merged.path
(['GET'], '/x')
```

#### merge_with(other)

Merge with another config, other takes precedence.

* **Return type:**
  [`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)

### qh.config.from_dict(config_dict)

Create AppConfig from dictionary.

* **Return type:**
  [`AppConfig`](_autosummary/qh.config.html.md#qh.config.AppConfig)

### qh.config.normalize_funcs_input(funcs)

Normalize various input formats to Dict[Callable, RouteConfig].

Supports:

- Single callable
- List of callables
- Dict mapping callable to config dict
- Dict mapping callable to RouteConfig

* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), [`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)]

### qh.config.resolve_route_config(func, app_config, route_config=None)

Resolve complete route configuration for a function.

Precedence (highest to lowest):

1. route_config (function-specific)
2. app_config (app-level defaults)
3. DEFAULT_ROUTE_CONFIG (global defaults)

* **Return type:**
  [`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)


# _autosummary/qh.conventions.html.md

# qh.conventions

Convention-based routing for qh.

Automatically infer HTTP paths and methods from function names and signatures.

Supports patterns like:

- get_user(user_id: str) → GET /users/{user_id}
- list_users(limit: int = 100) → GET /users?limit=100
- create_user(user: User) → POST /users
- update_user(user_id: str, user: User) → PUT /users/{user_id}
- delete_user(user_id: str) → DELETE /users/{user_id}

### Functions

| [`apply_conventions_to_funcs`](_autosummary/qh.conventions.html.md#qh.conventions.apply_conventions_to_funcs)(funcs, \*[, ...])    | Apply conventions to a list of functions.                      |
|--------------------------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`get_id_params`](_autosummary/qh.conventions.html.md#qh.conventions.get_id_params)(func)                             | Extract parameters that look like IDs from function signature. |
| [`infer_http_method`](_autosummary/qh.conventions.html.md#qh.conventions.infer_http_method)(func_name[, parsed])          | Infer HTTP method from function name.                          |
| [`infer_path_from_function`](_autosummary/qh.conventions.html.md#qh.conventions.infer_path_from_function)(func, \*[, ...])       | Infer RESTful path from function name and signature.           |
| [`infer_route_config`](_autosummary/qh.conventions.html.md#qh.conventions.infer_route_config)(func, \*[, ...])             | Infer complete route configuration from function.              |
| [`merge_convention_config`](_autosummary/qh.conventions.html.md#qh.conventions.merge_convention_config)(convention_config, ...) | Merge convention-based config with explicit config.            |
| [`parse_function_name`](_autosummary/qh.conventions.html.md#qh.conventions.parse_function_name)(func_name)                  | Parse a function name to extract verb and resource.            |
| [`pluralize`](_autosummary/qh.conventions.html.md#qh.conventions.pluralize)(word)                                 | Simple pluralization.                                          |
| [`singularize`](_autosummary/qh.conventions.html.md#qh.conventions.singularize)(word)                               | Simple singularization (just removes trailing 's' for now).    |

### Classes

| [`ParsedFunctionName`](_autosummary/qh.conventions.html.md#qh.conventions.ParsedFunctionName)(verb, resource, ...)   | Result of parsing a function name.   |
|--------------------------------------------------------------------------------------------|--------------------------------------|

### *class* qh.conventions.ParsedFunctionName(verb, resource, is_plural, is_collection_operation)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of parsing a function name.

### qh.conventions.apply_conventions_to_funcs(funcs, , use_conventions=True, base_path='', use_plurals=True)

Apply conventions to a list of functions.

* **Parameters:**
  * **funcs** ([`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)]) – The functions to route.
  * **use_conventions** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to use conventions
  * **base_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Base path to prepend to all routes
  * **use_plurals** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to use plural resource names
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]
* **Returns:**
  Dict mapping functions to their inferred configurations

### qh.conventions.get_id_params(func)

Extract parameters that look like IDs from function signature.

ID parameters typically:

- End with ‘_id’
- Are named ‘id’
- Are the first parameter (for item operations)

* **Parameters:**
  **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – Function to analyze
* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  List of parameter names that are likely IDs

### qh.conventions.infer_http_method(func_name, parsed=None)

Infer HTTP method from function name.

* **Parameters:**
  * **func_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Function name
  * **parsed** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`ParsedFunctionName`](_autosummary/qh.conventions.html.md#qh.conventions.ParsedFunctionName)]) – Optional pre-parsed function name
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  HTTP method (‘GET’, ‘POST’, ‘PUT’, ‘PATCH’, ‘DELETE’)

### Examples

```pycon
>>> infer_http_method('get_user')
'GET'
>>> infer_http_method('create_user')
'POST'
>>> infer_http_method('update_user')
'PUT'
>>> infer_http_method('delete_user')
'DELETE'
```

### qh.conventions.infer_path_from_function(func, , use_plurals=True, base_path='')

Infer RESTful path from function name and signature.

* **Parameters:**
  * **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – Function to analyze
  * **use_plurals** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to use plural resource names for collections
  * **base_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Base path to prepend
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  Inferred path

### Examples

```pycon
>>> def get_user(user_id: str): pass
>>> infer_path_from_function(get_user)
'/users/{user_id}'
```

```pycon
>>> def list_users(limit: int = 100): pass
>>> infer_path_from_function(list_users)
'/users'
```

```pycon
>>> def create_user(name: str, email: str): pass
>>> infer_path_from_function(create_user)
'/users'
```

```pycon
>>> def update_user(user_id: str, name: str): pass
>>> infer_path_from_function(update_user)
'/users/{user_id}'
```

### qh.conventions.infer_route_config(func, , use_conventions=True, base_path='', use_plurals=True)

Infer complete route configuration from function.

* **Parameters:**
  * **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – Function to analyze
  * **use_conventions** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to use conventions (if False, returns empty dict)
  * **base_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Base path to prepend
  * **use_plurals** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to use plural resource names
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  Route configuration dict

### qh.conventions.merge_convention_config(convention_config, explicit_config)

Merge convention-based config with explicit config.

Explicit config takes precedence.

* **Parameters:**
  * **convention_config** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – Config inferred from conventions
  * **explicit_config** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – User-provided config
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  Merged config

### qh.conventions.parse_function_name(func_name)

Parse a function name to extract verb and resource.

* **Parameters:**
  **func_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The function’s name, e.g. `'get_user'`.
* **Return type:**
  [`ParsedFunctionName`](_autosummary/qh.conventions.html.md#qh.conventions.ParsedFunctionName)
* **Returns:**
  A `ParsedFunctionName`. When the name is `<verb>_<rest>` and
  `verb` is a known CRUD verb, `resource` is `rest`; otherwise
  the whole name is the resource and `verb` is `""`.

### Examples

```pycon
>>> parse_function_name('get_user')
ParsedFunctionName(verb='get', resource='user', is_plural=False, is_collection_operation=False)
```

```pycon
>>> parse_function_name('list_users')
ParsedFunctionName(verb='list', resource='users', is_plural=True, is_collection_operation=True)
```

```pycon
>>> parse_function_name('create_order_item')
ParsedFunctionName(verb='create', resource='order_item', is_plural=True, is_collection_operation=True)
```

### qh.conventions.pluralize(word)

Simple pluralization.

More sophisticated rules can be added later.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### qh.conventions.singularize(word)

Simple singularization (just removes trailing ‘s’ for now).

More sophisticated rules can be added later.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/qh.core.html.md

# qh.core

Minimal function-to-route dispatch built on `i2.wrapper.Wrap`.

An early, self-contained take on the qh idea: `mk_fastapi_app` turns a
collection of callables into `POST` routes that read a JSON body as keyword
arguments and return the JSON-encoded result. It is not used by `qh.app.mk_app`,
the maintained entry point, and it keeps a process-wide `default_configs`
dict that `mk_fastapi_app` mutates; prefer `qh.app` for new code.

Main entry points:

- `mk_fastapi_app`: callables in, FastAPI app out
- `default_configs`: the mutable process-wide defaults (path, method, mappers)

### Functions

| [`default_input_mapper`](_autosummary/qh.core.html.md#qh.core.default_input_mapper)(request)                   | Extract function arguments from request JSON body.       |
|--------------------------------------------------------------------------------------------------|----------------------------------------------------------|
| [`default_output_mapper`](_autosummary/qh.core.html.md#qh.core.default_output_mapper)(output)                   | Serialize function output to JSON response.              |
| [`get_config_for_func`](_autosummary/qh.core.html.md#qh.core.get_config_for_func)(func, default_configs, ...) | Merge default and per-function configurations.           |
| [`mk_fastapi_app`](_autosummary/qh.core.html.md#qh.core.mk_fastapi_app)(funcs[, configs, func_configs])  | Create a FastAPI app from a collection of functions.     |
| [`mk_wrapped_func`](_autosummary/qh.core.html.md#qh.core.mk_wrapped_func)(func, input_mapper, ...)        | Wrap a function with ingress and egress transformations. |

### *async* qh.core.default_input_mapper(request)

Extract function arguments from request JSON body.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### qh.core.default_output_mapper(output)

Serialize function output to JSON response.

* **Return type:**
  `Response`

### qh.core.get_config_for_func(func, default_configs, func_configs)

Merge default and per-function configurations.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### qh.core.mk_fastapi_app(funcs, configs=None, func_configs=None)

Create a FastAPI app from a collection of functions.

* **Return type:**
  `FastAPI`

### qh.core.mk_wrapped_func(func, input_mapper, output_mapper)

Wrap a function with ingress and egress transformations.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)


# _autosummary/qh.endpoint.html.md

# qh.endpoint

Endpoint creation using i2.Wrap to transform functions into FastAPI routes.

This module bridges Python functions and HTTP endpoints via transformation rules.

### Functions

| [`apply_egress_transform`](_autosummary/qh.endpoint.html.md#qh.endpoint.apply_egress_transform)(result, egress)        | Apply egress transformation to function result.                     |
|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------|
| [`apply_ingress_transforms`](_autosummary/qh.endpoint.html.md#qh.endpoint.apply_ingress_transforms)(params, param_specs) | Apply ingress transformations to extracted parameters.              |
| [`extract_http_params`](_autosummary/qh.endpoint.html.md#qh.endpoint.extract_http_params)(request, param_specs)     | Extract parameters from HTTP request based on transformation specs. |
| [`make_endpoint`](_autosummary/qh.endpoint.html.md#qh.endpoint.make_endpoint)(func, route_config)             | Create FastAPI endpoint from a function using i2.Wrap.              |
| [`validate_route_config`](_autosummary/qh.endpoint.html.md#qh.endpoint.validate_route_config)(func, config)           | Validate that route configuration is compatible with function.      |

### qh.endpoint.apply_egress_transform(result, egress)

Apply egress transformation to function result.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### qh.endpoint.apply_ingress_transforms(params, param_specs)

Apply ingress transformations to extracted parameters.

* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *async* qh.endpoint.extract_http_params(request, param_specs)

Extract parameters from HTTP request based on transformation specs.

* **Parameters:**
  * **request** (`Request`) – FastAPI Request object
  * **param_specs** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]) – Mapping of param name to its TransformSpec
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  Dict of parameter name to extracted value

### qh.endpoint.make_endpoint(func, route_config)

Create FastAPI endpoint from a function using i2.Wrap.

* **Parameters:**
  * **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – The Python function to wrap
  * **route_config** ([`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)) – Configuration for this route
* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)
* **Returns:**
  Async endpoint function compatible with FastAPI

### qh.endpoint.validate_route_config(func, config)

Validate that route configuration is compatible with function.

* **Parameters:**
  * **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – The function the route is for.
  * **config** ([`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)) – The route configuration to check against `func`’s signature.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If configuration is invalid
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/qh.html.md

# qh

Quick HTTP: expose Python functions as a FastAPI web service with one call.

Give `mk_app` a function, a list of functions, or a dict of functions to route
configs, and get back a FastAPI app whose routes call those functions. Type
hints drive request validation and the OpenAPI document; an optional
convention layer infers RESTful paths and methods from function names; a rule
chain and a type registry decide where each parameter lives in the HTTP
request and how it is (de)serialized. The same app can be tested in-process,
served, turned into a Python, JavaScript or TypeScript client, and given
background-task endpoints for long-running functions.

Main entry points:

- `mk_app`: functions in, FastAPI app out (`qh.app`)
- `RouteConfig` / `AppConfig`: per-route and app-wide configuration (`qh.config`)
- `test_app` / `quick_test` / `service_running`: in-process and live testing (`qh.testing`)
- `mk_client_from_app` / `export_openapi`: clients and the OpenAPI document (`qh.client`, `qh.openapi`)
- `TaskConfig`: background execution for functions named in `async_funcs` (`qh.async_tasks`)

```pycon
>>> from qh import mk_app, test_app
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
>>> with test_app(app) as client:
...     client.post('/add', json={'x': 3, 'y': 5}).json()
8
```

### Functions

| [`mk_app`](_autosummary/qh.html.md#qh.mk_app)(funcs, \*[, app, config, ...])              | Create a FastAPI application whose routes call the given Python functions.   |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`inspect_routes`](_autosummary/qh.html.md#qh.inspect_routes)(app)                                | List the routes of a FastAPI app as plain dicts.                             |
| [`print_routes`](_autosummary/qh.html.md#qh.print_routes)(app)                                  | Print a text table of a FastAPI app's routes (methods, path, endpoint name). |
| [`register_type`](_autosummary/qh.html.md#qh.register_type)(python_type, \*, to_json, from_json) | Register a type in the global registry.                                      |
| [`register_json_type`](_autosummary/qh.html.md#qh.register_json_type)([cls, to_json, from_json])      | Decorator to register a custom type.                                         |
| [`export_openapi`](_autosummary/qh.html.md#qh.export_openapi)(app, \*[, include_examples, ...])   | Export the enhanced OpenAPI schema, optionally writing it to a file.         |
| [`enhance_openapi_schema`](_autosummary/qh.html.md#qh.enhance_openapi_schema)(app, \*[, ...])             | Generate an enhanced OpenAPI schema for a `qh` app.                          |
| [`install_enhanced_openapi`](_autosummary/qh.html.md#qh.install_enhanced_openapi)(app, \*\*enhance_kwargs)  | Make `app` serve the enhanced OpenAPI schema at its `/openapi.json`.         |
| [`python_type_to_json_schema`](_autosummary/qh.html.md#qh.python_type_to_json_schema)(type_hint, schemas, \*) | Convert a Python type hint to a JSON Schema fragment.                        |
| [`mk_client_from_openapi`](_autosummary/qh.html.md#qh.mk_client_from_openapi)(openapi_spec[, ...])        | Create an HTTP client from an OpenAPI specification.                         |
| [`mk_client_from_url`](_autosummary/qh.html.md#qh.mk_client_from_url)(openapi_url[, base_url, ...])   | Create an HTTP client by fetching OpenAPI spec from a URL.                   |
| [`mk_client_from_app`](_autosummary/qh.html.md#qh.mk_client_from_app)(app[, base_url])                | Create an HTTP client from a FastAPI app (for testing).                      |
| [`export_js_client`](_autosummary/qh.html.md#qh.export_js_client)(openapi_spec, \*[, ...])          | Generate JavaScript client class from OpenAPI spec.                          |
| [`export_ts_client`](_autosummary/qh.html.md#qh.export_ts_client)(openapi_spec, \*[, ...])          | Generate TypeScript client class from OpenAPI spec.                          |
| [`run_app`](_autosummary/qh.html.md#qh.run_app)(app, \*[, use_server])                     | Context manager for running a FastAPI app.                                   |
| [`test_app`](_autosummary/qh.html.md#qh.test_app)(app)                                      | Call an app in-process through a `TestClient`, no server, no port.           |
| [`serve_app`](_autosummary/qh.html.md#qh.serve_app)(app[, port, host])                       | Context manager for running app with real server.                            |
| [`quick_test`](_autosummary/qh.html.md#qh.quick_test)(func, \*\*kwargs)                       | Call one function through HTTP and return the decoded JSON response.         |
| [`service_running`](_autosummary/qh.html.md#qh.service_running)(\*[, url, app, launcher, ...])     | Ensure an HTTP service is running for testing purposes.                      |
| [`use_au_backend`](_autosummary/qh.html.md#qh.use_au_backend)([backend, store])                   | Create a qh TaskConfig that uses au backend and storage.                     |
| [`use_au_thread_backend`](_autosummary/qh.html.md#qh.use_au_thread_backend)([storage_path, ...])         | Use au's ThreadBackend with filesystem storage.                              |
| [`use_au_process_backend`](_autosummary/qh.html.md#qh.use_au_process_backend)([storage_path, ...])        | Use au's ProcessBackend for CPU-bound tasks.                                 |
| [`use_au_redis_backend`](_autosummary/qh.html.md#qh.use_au_redis_backend)([redis_url, ...])             | Use au's Redis/RQ backend for distributed tasks.                             |

### Classes

| [`AppConfig`](_autosummary/qh.html.md#qh.AppConfig)([default_methods, path_template, ...])   | Global configuration for the entire FastAPI app.                              |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`RouteConfig`](_autosummary/qh.html.md#qh.RouteConfig)([path, methods, rule_chain, ...])      | Configuration for a single route (function endpoint).                         |
| [`ConfigBuilder`](_autosummary/qh.html.md#qh.ConfigBuilder)()                                    | Fluent interface for building configurations.                                 |
| [`RuleChain`](_autosummary/qh.html.md#qh.RuleChain)([rules])                                 | Chain of rules evaluated in order with first-match semantics.                 |
| [`TransformSpec`](_autosummary/qh.html.md#qh.TransformSpec)([http_location, ingress, ...])       | Specification for how to transform a parameter.                               |
| [`HttpLocation`](_autosummary/qh.html.md#qh.HttpLocation)(\*values)                             | Where in HTTP request/response to map a parameter.                            |
| [`TypeRule`](_autosummary/qh.html.md#qh.TypeRule)(type_map)                                 | Rule that matches based on parameter type.                                    |
| [`NameRule`](_autosummary/qh.html.md#qh.NameRule)(name_map)                                 | Rule that matches based on parameter name.                                    |
| [`FuncRule`](_autosummary/qh.html.md#qh.FuncRule)(func_map)                                 | Rule that matches based on function.                                          |
| [`FuncNameRule`](_autosummary/qh.html.md#qh.FuncNameRule)(pattern_map)                          | Rule that matches based on function name pattern.                             |
| [`TypeRegistry`](_autosummary/qh.html.md#qh.TypeRegistry)()                                     | Registry for type handlers.                                                   |
| [`HttpClient`](_autosummary/qh.html.md#qh.HttpClient)(base_url[, session])                    | Client for calling HTTP endpoints with Python function interface.             |
| [`TaskConfig`](_autosummary/qh.html.md#qh.TaskConfig)([store, executor, ttl, ...])            | Configuration for async task processing.                                      |
| [`TaskStatus`](_autosummary/qh.html.md#qh.TaskStatus)(\*values)                               | Standard task status values.                                                  |
| [`TaskInfo`](_autosummary/qh.html.md#qh.TaskInfo)(task_id, status, created_at[, ...])       | Information about a task's state.                                             |
| [`TaskStore`](_autosummary/qh.html.md#qh.TaskStore)()                                        | Abstract interface for task storage backends.                                 |
| [`InMemoryTaskStore`](_autosummary/qh.html.md#qh.InMemoryTaskStore)([ttl])                           | Simple in-memory task storage (not persistent, single-process only).          |
| [`TaskExecutor`](_autosummary/qh.html.md#qh.TaskExecutor)()                                     | Abstract interface for task execution backends.                               |
| [`ThreadPoolTaskExecutor`](_autosummary/qh.html.md#qh.ThreadPoolTaskExecutor)([max_workers])              | Execute tasks using a thread pool (good for I/O-bound tasks).                 |
| [`ProcessPoolTaskExecutor`](_autosummary/qh.html.md#qh.ProcessPoolTaskExecutor)([max_workers])             | Execute tasks using a process pool (good for CPU-bound tasks).                |
| [`TaskManager`](_autosummary/qh.html.md#qh.TaskManager)([config])                              | Manages async task execution and state.                                       |
| [`AppRunner`](_autosummary/qh.html.md#qh.AppRunner)(app, \*[, use_server, host, port, ...])  | Context manager for running a FastAPI app in test mode or with a real server. |
| [`ServiceInfo`](_autosummary/qh.html.md#qh.ServiceInfo)(url, was_already_running[, ...])       | Information about a running service.                                          |
| [`AuTaskStore`](_autosummary/qh.html.md#qh.AuTaskStore)(au_store)                              | Adapter to use au's ComputationStore as qh's TaskStore.                       |
| [`AuTaskExecutor`](_autosummary/qh.html.md#qh.AuTaskExecutor)(au_backend, au_store)               | Adapter to use au's ComputationBackend as qh's TaskExecutor.                  |

### *class* qh.AppConfig(default_methods=<factory>, path_template='/{func_name}', path_prefix='', rule_chain=<factory>, title='qh API', version='0.1.0', docs_url='/docs', redoc_url='/redoc', openapi_url='/openapi.json', fastapi_kwargs=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Global configuration for the entire FastAPI app.

```pycon
>>> ac = AppConfig(path_prefix='/api')
>>> ac.default_methods, ac.path_prefix
(['POST'], '/api')
```

#### to_fastapi_kwargs()

Convert to FastAPI() constructor kwargs.

* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *class* qh.AppRunner(app, , use_server=False, host='127.0.0.1', port=8000, server_timeout=2.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Context manager for running a FastAPI app in test mode or with a real server.

Supports both synchronous testing (using TestClient) and integration testing
(using a real uvicorn server). With `use_server=False` (the default) the
`with` block receives a `TestClient`; with `use_server=True` it
receives the base URL of a uvicorn server started in a daemon thread. On
exit the TestClient reference is dropped; a real server is not stopped, its
daemon thread ends with the process. `run_app` is the function form.

### Examples

Basic usage with TestClient:

```pycon
>>> from qh import mk_app
>>> from qh.testing import AppRunner
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
>>> with AppRunner(app) as client:
...     response = client.post('/add', json={'x': 3, 'y': 5})
...     assert response.json() == 8
```

With real server (integration testing):

```pycon
>>> with AppRunner(app, use_server=True, port=8001) as base_url:
...     response = requests.post(f'{base_url}/add', json={'x': 3, 'y': 5})
...     assert response.json() == 8
```

An exception inside the block propagates; `__exit__` still runs:

```pycon
>>> with AppRunner(app) as client:
...     raise ValueError("Test error")
```

### *class* qh.AuTaskExecutor(au_backend, au_store)

Bases: [`TaskExecutor`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskExecutor)

Adapter to use au’s ComputationBackend as qh’s TaskExecutor.

Delegates task execution to au’s backend system.

#### shutdown(wait=True)

Shutdown the executor.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### submit_task(task_id, func, args, kwargs, callback)

Submit a task to au backend.

#### NOTE
au handles result storage internally, so we don’t use the callback.
The callback is for qh’s built-in backends, but au’s store handles this.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.AuTaskStore(au_store)

Bases: [`TaskStore`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskStore)

Adapter to use au’s ComputationStore as qh’s TaskStore.

Maps between qh’s TaskInfo and au’s computation results.

#### create_task(task_id, func_name)

Create a new task record.

* **Return type:**
  [`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)

#### delete_task(task_id)

Delete `task_id` from the au store, returning whether it was present.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### get_task(task_id)

Retrieve task information from au store.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### list_tasks(limit=100)

List recent tasks.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### update_task(task_info)

Update task information.

#### NOTE
au manages its own state, so this is mostly a no-op.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.ConfigBuilder

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Fluent interface for building configurations.

#### build()

Build final configuration.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`AppConfig`](_autosummary/qh.config.html.md#qh.config.AppConfig), [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), [`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)]]

#### for_function(func)

Start configuring a specific function.

* **Return type:**
  [`FunctionConfigBuilder`](_autosummary/qh.config.html.md#qh.config.FunctionConfigBuilder)

#### with_default_methods(methods)

Set default HTTP methods.

* **Return type:**
  [`ConfigBuilder`](_autosummary/qh.config.html.md#qh.config.ConfigBuilder)

#### with_path_prefix(prefix)

Set path prefix for all routes.

* **Return type:**
  [`ConfigBuilder`](_autosummary/qh.config.html.md#qh.config.ConfigBuilder)

#### with_path_template(template)

Set path template for auto-generation.

* **Return type:**
  [`ConfigBuilder`](_autosummary/qh.config.html.md#qh.config.ConfigBuilder)

#### with_rule_chain(chain)

Set global rule chain.

* **Return type:**
  [`ConfigBuilder`](_autosummary/qh.config.html.md#qh.config.ConfigBuilder)

### *class* qh.FuncNameRule(pattern_map)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Rule that matches based on function name pattern.

#### match(, param_name, param_type, param_default, func, func_name)

Match by function name pattern.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

### *class* qh.FuncRule(func_map)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Rule that matches based on function.

#### match(, param_name, param_type, param_default, func, func_name)

Match by function object and parameter name.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

### *class* qh.HttpClient(base_url, session=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Client for calling HTTP endpoints with Python function interface.

Generated functions preserve original signatures and make HTTP requests
under the hood.

#### add_function(name, path, method, signature_info=None)

Add a function to the client.

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Function name
  * **path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – HTTP path (may contain {param} placeholders)
  * **method** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – HTTP method (GET, POST, etc.)
  * **signature_info** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – Optional x-python-signature metadata
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.HttpLocation(\*values)

Bases: [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

Where in HTTP request/response to map a parameter.

### *class* qh.InMemoryTaskStore(ttl=None)

Bases: [`TaskStore`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskStore)

Simple in-memory task storage (not persistent, single-process only).

#### create_task(task_id, func_name)

`TaskStore.create_task`: record a new pending task in memory.

* **Return type:**
  [`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)

#### delete_task(task_id)

`TaskStore.delete_task`: remove a task, returning whether it existed.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### get_task(task_id)

`TaskStore.get_task`: look up a task, or `None` if unknown.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### list_tasks(limit=100)

`TaskStore.list_tasks`: the `limit` most recently created tasks, newest first.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### update_task(task_info)

`TaskStore.update_task`: overwrite the stored record for its task ID.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.NameRule(name_map)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Rule that matches based on parameter name.

#### match(, param_name, param_type, param_default, func, func_name)

Match by parameter name.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

### *class* qh.ProcessPoolTaskExecutor(max_workers=None)

Bases: [`TaskExecutor`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskExecutor)

Execute tasks using a process pool (good for CPU-bound tasks).

#### shutdown(wait=True)

`TaskExecutor.shutdown`: shut down the underlying process pool.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### submit_task(task_id, func, args, kwargs, callback)

`TaskExecutor.submit_task`: run `func` in a worker process, calling
`callback` with its result or exception when the future resolves.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.RouteConfig(path=None, methods=None, rule_chain=None, param_overrides=<factory>, async_config=None, summary=None, description=None, tags=None, response_model=None, include_in_schema=True, deprecated=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Configuration for a single route (function endpoint).

```pycon
>>> rc = RouteConfig(methods=['GET'])
>>> merged = rc.merge_with(RouteConfig(path='/x'))
>>> merged.methods, merged.path
(['GET'], '/x')
```

#### merge_with(other)

Merge with another config, other takes precedence.

* **Return type:**
  [`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)

### *class* qh.RuleChain(rules=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Chain of rules evaluated in order with first-match semantics.

Rules are tried from most specific to most general.

```pycon
>>> chain = RuleChain()
>>> chain.add_rule(TypeRule({int: TransformSpec(http_location=HttpLocation.QUERY)}))
>>> chain.match(param_name='x', param_type=int).http_location
<HttpLocation.QUERY: 'query'>
>>> chain.match(param_name='y', param_type=str) is None
True
```

#### add_rule(rule, priority=0)

Add a rule with optional priority (higher = evaluated earlier).

#### match(\*, param_name, param_type=<class 'NoneType'>, param_default, func=None, func_name='')

Find first matching rule.

* **Parameters:**
  * **param_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The parameter’s name.
  * **param_type** ([`type`](https://docs.python.org/3/builtins/functions.html#type)) – The parameter’s type annotation.
  * **param_default** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – The parameter’s default, or `inspect.Parameter.empty`.
  * **func** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)]) – The function the parameter belongs to, if known.
  * **func_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `func`’s name.
* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]
* **Returns:**
  TransformSpec from first matching rule, or None if no match

### *class* qh.ServiceInfo(url, was_already_running, thread=None, app=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Information about a running service.

#### url

Base URL of the service (e.g., ‘[http://localhost:8000](http://localhost:8000)’)

#### was_already_running

True if service was already running, False if launched

#### thread

Thread object if service was launched in thread, None otherwise

#### app

The FastAPI app if one was provided, None otherwise

### *class* qh.TaskConfig(store=None, executor=None, ttl=3600, async_mode='query', async_param='async', async_header='X-Async', create_task_endpoints=True, default_executor='thread')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Configuration for async task processing.

This is the explicit configuration. The convention is to use sane defaults.
Pass an instance (or a dict) as `async_config` to `qh.mk_app`; `store`
and `executor` are built lazily by `get_store`/`get_executor` from
`default_executor` when left `None`.

```pycon
>>> tc = TaskConfig(ttl=60, default_executor='thread')
>>> tc.ttl, tc.async_mode
(60, 'query')
>>> type(tc.get_executor()).__name__
'ThreadPoolTaskExecutor'
```

#### get_executor()

Get or create the task executor.

* **Return type:**
  [`TaskExecutor`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskExecutor)

#### get_store()

Get or create the task store.

* **Return type:**
  [`TaskStore`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskStore)

### *class* qh.TaskExecutor

Bases: [`ABC`](https://docs.python.org/3/library/abc.html#abc.ABC)

Abstract interface for task execution backends.

#### *abstractmethod* shutdown(wait=True)

Shutdown the executor.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### *abstractmethod* submit_task(task_id, func, args, kwargs, callback)

Submit a task for execution.

* **Parameters:**
  * **task_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Unique task identifier
  * **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – Function to execute
  * **args** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)) – Positional arguments
  * **kwargs** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)) – Keyword arguments
  * **callback** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any), [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception)]], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – Called when task completes with (task_id, result, error)
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.TaskInfo(task_id, status, created_at, started_at=None, completed_at=None, result=None, error=None, traceback=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Information about a task’s state.

#### to_dict()

Convert to dictionary for JSON serialization.

* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *class* qh.TaskManager(config=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Manages async task execution and state.

This is the main coordinator between stores, executors, and HTTP handlers.

#### cancel_task(task_id)

Cancel a task (if possible).

* **Parameters:**
  **task_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Task identifier
* **Return type:**
  [*bool*](https://docs.python.org/3/builtins/functions.html#bool)

#### NOTE
Cancellation is best-effort and may not work for all executors.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)
* **Returns:**
  True if task was cancelled or deleted

#### create_task(func, args=(), kwargs=None)

Create and submit a new task.

* **Parameters:**
  * **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – Function to execute asynchronously
  * **args** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)) – Positional arguments
  * **kwargs** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]) – Keyword arguments
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  Task ID

#### get_result(task_id, wait=False, timeout=None)

Get task result.

* **Parameters:**
  * **task_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Task identifier
  * **wait** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to block until task completes
  * **timeout** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]) – Maximum time to wait in seconds (None = wait forever)
* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)
* **Returns:**
  Task result if completed
* **Raises:**
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If task not found or failed
  * [**TimeoutError**](https://docs.python.org/3/builtins/exceptions.html#TimeoutError) – If wait times out

#### get_status(task_id)

Get task status and metadata.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### list_tasks(limit=100)

List recent tasks.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### shutdown()

Shutdown the task manager and its executor.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.TaskStatus(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

Standard task status values.

### *class* qh.TaskStore

Bases: [`ABC`](https://docs.python.org/3/library/abc.html#abc.ABC)

Abstract interface for task storage backends.

#### *abstractmethod* create_task(task_id, func_name)

Create a new task record.

* **Return type:**
  [`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)

#### *abstractmethod* delete_task(task_id)

Delete a task. Returns True if deleted, False if not found.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### *abstractmethod* get_task(task_id)

Retrieve task information.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### *abstractmethod* list_tasks(limit=100)

List recent tasks.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`TaskInfo`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskInfo)]

#### *abstractmethod* update_task(task_info)

Update task information.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.ThreadPoolTaskExecutor(max_workers=None)

Bases: [`TaskExecutor`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskExecutor)

Execute tasks using a thread pool (good for I/O-bound tasks).

#### shutdown(wait=True)

`TaskExecutor.shutdown`: shut down the underlying thread pool.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### submit_task(task_id, func, args, kwargs, callback)

`TaskExecutor.submit_task`: run `func` on the thread pool, calling
`callback` with its result or exception when it finishes.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.TransformSpec(http_location=HttpLocation.JSON_BODY, ingress=None, egress=None, http_name=None, metadata=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Specification for how to transform a parameter.

### *class* qh.TypeRegistry

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Registry for type handlers.

Manages conversion between Python types and HTTP representations.
Comes pre-populated with pass-through handlers for the JSON-native
builtins (`str`, `int`, `float`, `bool`, `list`, `dict`,
`NoneType`); its `register` method adds more. The module-level
`register_type` (and the `register_json_type` decorator built on it)
register into the separate, global registry used by the rest of `qh`,
not into a particular `TypeRegistry` instance.

```pycon
>>> reg = TypeRegistry()
>>> reg.get_handler(int).to_json(3)
3
>>> reg.get_handler(str) is not None
True
>>> class Unregistered: pass
>>> reg.get_handler(Unregistered) is None
True
```

#### get_handler(python_type)

Get handler for a type.

* **Parameters:**
  **python_type** ([`Type`](https://docs.python.org/3/library/typing.html#typing.Type)) – The type to look up
* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TypeHandler`](_autosummary/qh.types.html.md#qh.types.TypeHandler)]
* **Returns:**
  TypeHandler if registered, None otherwise

#### get_transform_spec(python_type)

Get TransformSpec for a type.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

#### register(python_type, , to_json, from_json, http_location=HttpLocation.JSON_BODY, content_type=None)

Register a type handler.

* **Parameters:**
  * **python_type** ([`Type`](https://docs.python.org/3/library/typing.html#typing.Type)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]) – The Python type
  * **to_json** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – Function to serialize to JSON-compatible format
  * **from_json** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]) – Function to deserialize from JSON
  * **http_location** ([`HttpLocation`](_autosummary/qh.rules.html.md#qh.rules.HttpLocation)) – Where this appears in HTTP
  * **content_type** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Optional content type for binary data
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### unregister(python_type)

Unregister a type handler.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* qh.TypeRule(type_map)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Rule that matches based on parameter type.

#### match(, param_name, param_type, param_default, func, func_name)

Match by type, including type hierarchy.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

### qh.enhance_openapi_schema(app, , include_examples=True, include_python_metadata=True, include_schemas=True, include_transformers=False)

Generate an enhanced OpenAPI schema for a `qh` app.

On top of FastAPI’s base document this fills in what `qh`’s
`Request`-based endpoints hide from FastAPI:

- `requestBody` / `parameters` / `responses` JSON Schema derived from
  each wrapped function’s Python type hints (`include_schemas`),
- `components.schemas` for every dataclass / `TypedDict` / Pydantic
  model / `NamedTuple` / `Enum` referenced,
- `x-python-signature` metadata (`include_python_metadata`),
- request examples (`include_examples`).

* **Parameters:**
  * **app** (`FastAPI`) – the FastAPI application.
  * **include_examples** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – add example requests.
  * **include_python_metadata** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – add `x-python-*` extensions.
  * **include_schemas** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – derive `requestBody` / `responses` /
    `components.schemas` from the Python type hints.
  * **include_transformers** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – add (placeholder) transformation metadata.
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  the enhanced OpenAPI schema dictionary.

### qh.export_js_client(openapi_spec, , class_name='ApiClient', use_axios=False, base_url='http://localhost:8000')

Generate JavaScript client class from OpenAPI spec.

* **Parameters:**
  * **openapi_spec** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – OpenAPI specification dictionary
  * **class_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name for the generated class
  * **use_axios** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Use axios instead of fetch
  * **base_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Default base URL
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  JavaScript code as string

### Example

```pycon
>>> from qh import mk_app, export_openapi
>>> from qh.jsclient import export_js_client
>>> app = mk_app([add, subtract])
>>> spec = export_openapi(app)
>>> js_code = export_js_client(spec, use_axios=True)
```

### qh.export_openapi(app, , include_examples=True, include_python_metadata=True, include_schemas=True, include_transformers=False, output_file=None)

Export the enhanced OpenAPI schema, optionally writing it to a file.

* **Parameters:**
  * **app** (`FastAPI`) – the FastAPI application.
  * **include_examples** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – include example requests.
  * **include_python_metadata** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – include `x-python-*` extensions.
  * **include_schemas** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – derive `requestBody` / `responses` /
    `components.schemas` from the Python type hints.
  * **include_transformers** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – include transformation metadata.
  * **output_file** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – optional path to write the JSON document to.
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  the enhanced OpenAPI schema dictionary.

### Example

```pycon
>>> from qh import mk_app
>>> from qh.openapi import export_openapi
>>> app = mk_app([my_func])
>>> spec = export_openapi(app, include_examples=True)
```

### qh.export_ts_client(openapi_spec, , class_name='ApiClient', use_axios=False, base_url='http://localhost:8000')

Generate TypeScript client class from OpenAPI spec.

* **Parameters:**
  * **openapi_spec** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – OpenAPI specification dictionary
  * **class_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name for the generated class
  * **use_axios** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Use axios instead of fetch
  * **base_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Default base URL
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  TypeScript code as string

### Example

```pycon
>>> from qh import mk_app, export_openapi
>>> from qh.jsclient import export_ts_client
>>> app = mk_app([add, subtract])
>>> spec = export_openapi(app, include_python_metadata=True)
>>> ts_code = export_ts_client(spec, use_axios=True)
```

### qh.inspect_routes(app)

List the routes of a FastAPI app as plain dicts.

* **Parameters:**
  **app** (`FastAPI`) – FastAPI application
* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]
* **Returns:**
  One dict per route that has HTTP methods (FastAPI’s own `/docs`,
  `/redoc` and `/openapi.json` routes included), in registration
  order, with keys `path`, `methods`, `name` and `endpoint`.
  Routes made by `mk_app` also carry `function` (the original
  Python callable) and `param_specs` (the parameter-to-`TransformSpec`
  map used to build the OpenAPI document).

### Examples

```pycon
>>> from qh import mk_app, inspect_routes
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
>>> route = [r for r in inspect_routes(app) if r['name'] == 'add'][0]
>>> route['path'], route['methods'], route['function'] is add
('/add', ['POST'], True)
```

### qh.install_enhanced_openapi(app, \*\*enhance_kwargs)

Make `app` serve the enhanced OpenAPI schema at its `/openapi.json`.

Overrides `app.openapi` so the document returned by FastAPI — and rendered
by `/docs` and `/redoc` — carries the request/response JSON Schema and
`components.schemas` derived from the wrapped functions’ Python type hints.

The override is **defensive**: if enhancement raises for any reason, it
falls back to FastAPI’s plain schema, so a converter bug can never turn
`/openapi.json` into a 500.

* **Parameters:**
  * **app** (`FastAPI`) – the FastAPI application (typically the result of [`qh.mk_app()`](_autosummary/qh.html.md#qh.mk_app)).
  * **\*\*enhance_kwargs** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – forwarded to [`enhance_openapi_schema()`](_autosummary/qh.html.md#qh.enhance_openapi_schema).
* **Return type:**
  `FastAPI`
* **Returns:**
  the same `app`, for chaining.

### qh.mk_app(funcs, , app=None, config=None, use_conventions=False, async_funcs=None, async_config=None, enhanced_openapi=True, \*\*kwargs)

Create a FastAPI application whose routes call the given Python functions.

This is the primary API for qh. It supports multiple input formats for maximum
flexibility while maintaining simplicity for common cases. Each function gets
one route; by default a `POST` at `/<function name>` taking its arguments
as a JSON object (see `RouteConfig` and `AppConfig` in `qh.config` for
what can be changed, and `qh.rules` for how parameters are mapped to HTTP).

* **Parameters:**
  * **funcs** (`Union`[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)], [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), `Union`[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`RouteConfig`](_autosummary/qh.config.html.md#qh.config.RouteConfig)]]]) – 

    Functions to expose as HTTP endpoints. Can be:
    - A single callable
    - A list of callables
    - A dict mapping callables to their route configurations
  * **app** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[`FastAPI`]) – Optional existing FastAPI app to add routes to.
    If None, creates a new app.
  * **config** (`Union`[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`AppConfig`](_autosummary/qh.config.html.md#qh.config.AppConfig), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – 

    Optional app-level configuration. Can be:
    - AppConfig object
    - Dict that will be converted to AppConfig
    - None (uses defaults)
  * **use_conventions** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – 

    Whether to use convention-based routing.
    If True, infers paths and methods from function names:
    - get_user(user_id) → GET /users/{user_id}
    - list_users() → GET /users
    - create_user(user) → POST /users
  * **async_funcs** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`List`](https://docs.python.org/3/library/typing.html#typing.List)[`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)]]]) – List of functions (by name or reference) that should support
    async task execution. When enabled, clients can add ?async=true to
    get a task ID instead of blocking for the result.
  * **async_config** (`Union`[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – 

    Configuration for async task processing. Can be:
    - None (uses default TaskConfig for functions in async_funcs)
    - TaskConfig object (applies to all async_funcs)
    - Dict mapping function names to TaskConfig objects
  * **enhanced_openapi** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to serve an enhanced OpenAPI document at
    `/openapi.json` — one with `requestBody` / `responses` /
    `components.schemas` derived from each function’s Python type
    hints (see [`qh.openapi`](_autosummary/qh.openapi.html.md#module-qh.openapi)). Defaults to True; the enhancement is
    additive and falls back to FastAPI’s plain schema if it ever fails.
  * **\*\*kwargs** – Additional FastAPI() constructor kwargs (if creating new app)
* **Return type:**
  `FastAPI`
* **Returns:**
  The FastAPI application (the one passed as `app`, or a new one) with
  one route per function, in the order the functions were given.
* **Raises:**
  * [**TypeError**](https://docs.python.org/3/builtins/exceptions.html#TypeError) – If `config` is neither `None`, an `AppConfig`, nor a dict.
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If a per-function route config is invalid for that function
        (see `qh.endpoint.validate_route_config`), e.g. a `path` whose
        `{param}` placeholders don’t match the function’s parameters.

#### SEE ALSO
`qh.testing.test_app`: call the resulting app in-process without a server.
`qh.client.mk_client_from_app`: a Python client whose methods mirror the functions.
`qh.base.mk_fastapi_app`: the older, config-free variant kept for its tests.

### Examples

Simple case - just functions:

```pycon
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
```

With conventions:

```pycon
>>> def get_user(user_id: str): ...
>>> def list_users(): ...
>>> app = mk_app([get_user, list_users], use_conventions=True)
```

With configuration:

```pycon
>>> app = mk_app(
...     [add],
...     config={'path_prefix': '/api', 'default_methods': ['POST']}
... )
```

Per-function configuration:

```pycon
>>> app = mk_app({
...     add: {'methods': ['GET', 'POST'], 'path': '/calculate/add'},
... })
```

With async support:

```pycon
>>> def expensive_task(n: int) -> int:
...     import time
...     time.sleep(5)
...     return n * 2
>>> app = mk_app([expensive_task], async_funcs=['expensive_task'])
```

Now `POST /expensive_task?async=true` returns `{"task_id": ...}` and
`GET /tasks/{task_id}/result` returns the result when ready.

### qh.mk_client_from_app(app, base_url='http://testserver')

Create an HTTP client from a FastAPI app (for testing).

* **Parameters:**
  * **app** – FastAPI application
  * **base_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Base URL for API requests (default for TestClient)
* **Return type:**
  [`HttpClient`](_autosummary/qh.client.html.md#qh.client.HttpClient)
* **Returns:**
  HttpClient that uses FastAPI TestClient under the hood

### Example

```pycon
>>> from qh import mk_app
>>> from qh.client import mk_client_from_app
>>> app = mk_app([add, subtract])
>>> client = mk_client_from_app(app)
>>> result = client.add(x=3, y=5)
```

### qh.mk_client_from_openapi(openapi_spec, base_url='http://localhost:8000', session=None)

Create an HTTP client from an OpenAPI specification.

* **Parameters:**
  * **openapi_spec** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – The parsed OpenAPI document (as from `export_openapi`
    or `json.load` on a spec file), used to build one client function
    per operation.
  * **base_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Base URL for API requests
  * **session** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[`Session`]) – An existing session to reuse (e.g. for shared auth/headers);
    a new one is created if not given.
* **Return type:**
  [`HttpClient`](_autosummary/qh.client.html.md#qh.client.HttpClient)
* **Returns:**
  HttpClient with functions for each endpoint

### Example

```pycon
>>> from qh.client import mk_client_from_openapi
>>> spec = {'paths': {'/add': {...}}, ...}
>>> client = mk_client_from_openapi(spec, 'http://localhost:8000')
>>> result = client.add(x=3, y=5)
```

### qh.mk_client_from_url(openapi_url, base_url=None, session=None)

Create an HTTP client by fetching OpenAPI spec from a URL.

* **Parameters:**
  * **openapi_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – URL to OpenAPI JSON spec (e.g., “[http://localhost:8000/openapi.json](http://localhost:8000/openapi.json)”)
  * **base_url** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Base URL for API requests (defaults to same as openapi_url)
  * **session** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[`Session`]) – An existing session to reuse; a new one is created if not given.
* **Return type:**
  [`HttpClient`](_autosummary/qh.client.html.md#qh.client.HttpClient)
* **Returns:**
  HttpClient with functions for each endpoint

### Example

```pycon
>>> from qh.client import mk_client_from_url
>>> client = mk_client_from_url('http://localhost:8000/openapi.json')
>>> result = client.add(x=3, y=5)
```

### qh.print_routes(app)

Print a text table of a FastAPI app’s routes (methods, path, endpoint name).

* **Parameters:**
  **app** (`FastAPI`) – FastAPI application
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### Examples

```pycon
>>> from qh import mk_app, print_routes
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> print_routes(mk_app([add], config={'docs_url': None, 'redoc_url': None, 'openapi_url': None}))
METHODS  PATH  ENDPOINT
----------------------------------------------------------
POST  /add  add
```

### qh.python_type_to_json_schema(type_hint, schemas, , \_stack=None)

Convert a Python type hint to a JSON Schema fragment.

Primitives, containers and unions are inlined. Named composite types —
dataclasses, `TypedDict`s, Pydantic models, `NamedTuple`s and
`Enum`s — are registered in `schemas` (the OpenAPI
`components.schemas` table) and returned as a `$ref`, so the same type
used in several places is described once.

* **Parameters:**
  * **type_hint** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – the Python type / annotation to convert. `Any` and
    `inspect.Parameter.empty` map to the empty schema `{}`
    (matches anything); `None` / `NoneType` map to `{"type": "null"}`.
  * **schemas** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – the mutable `components.schemas` accumulator — composite
    types encountered are added here, keyed by their class name.
  * **\_stack** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`frozenset`](https://docs.python.org/3/builtins/stdtypes.html#frozenset)]) – internal — the set of composite type names currently being
    built, used to break recursion on self-referential types.
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  a JSON Schema dict — an inline fragment, or `{"$ref": ...}` for a
  named composite type.

### Examples

```pycon
>>> python_type_to_json_schema(int, {})
{'type': 'integer'}
>>> python_type_to_json_schema(list[str], {})
{'type': 'array', 'items': {'type': 'string'}}
>>> python_type_to_json_schema(Optional[int], {})
{'anyOf': [{'type': 'integer'}, {'type': 'null'}]}
```

### qh.quick_test(func, \*\*kwargs)

Call one function through HTTP and return the decoded JSON response.

Builds `mk_app([func])`, POSTs `kwargs` as the JSON body to
`/<func name>`, and returns `response.json()`. Meant for a one-line
smoke check of what a function looks like over HTTP.

* **Parameters:**
  * **func** – Function to test
  * **\*\*kwargs** – Arguments to pass to the function (sent as the JSON body)
* **Returns:**
  The JSON-decoded response body, i.e. the function’s return value after
  JSON round-tripping.
* **Raises:**
  **httpx.HTTPStatusError** – If the response status is 4xx or 5xx (for
      example a missing required argument, or an exception in
      `func`); `fastapi.testclient.TestClient` is httpx-based, not
      requests-based.

### Examples

```pycon
>>> from qh.testing import quick_test
>>>
>>> def add(x: int, y: int) -> int:
...     return x + y
>>>
>>> result = quick_test(add, x=3, y=5)
>>> assert result == 8
>>>
>>> def greet(name: str) -> str:
...     return f"Hello, {name}!"
>>>
>>> result = quick_test(greet, name="World")
>>> assert result == "Hello, World!"
```

### qh.register_json_type(cls=None, , to_json=None, from_json=None)

Decorator to register a custom type.

Can be used as:

1. Class decorator (auto-detect to_dict/from_dict methods)
2. With explicit serializers

* **Parameters:**
  * **cls** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Type`](https://docs.python.org/3/library/typing.html#typing.Type)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]]) – The class being decorated, when used as `@register_json_type`
    with no arguments; `None` when called as
    `@register_json_type(to_json=..., from_json=...)`.
  * **to_json** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – Serializer; when omitted, falls back to `cls.to_dict()`,
    then `obj.__dict__`.
  * **from_json** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]]) – Deserializer; when omitted, falls back to
    `cls.from_dict`, then `cls(**data)`.

### Examples

```pycon
>>> @register_json_type
... class Point:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def to_dict(self):
...         return {'x': self.x, 'y': self.y}
...     @classmethod
...     def from_dict(cls, data):
...         return cls(data['x'], data['y'])
```

```pycon
>>> @register_json_type(
...     to_json=lambda p: [p.x, p.y],
...     from_json=lambda data: Point(data[0], data[1])
... )
... class Point:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
```

### qh.register_type(python_type, , to_json, from_json, http_location=HttpLocation.JSON_BODY, content_type=None)

Register a type in the global registry.

* **Parameters:**
  * **python_type** ([`Type`](https://docs.python.org/3/library/typing.html#typing.Type)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]) – The Python type
  * **to_json** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – Function to serialize to JSON-compatible format
  * **from_json** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]) – Function to deserialize from JSON
  * **http_location** ([`HttpLocation`](_autosummary/qh.rules.html.md#qh.rules.HttpLocation)) – Where this appears in HTTP
  * **content_type** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Optional content type for binary data
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### Example

```pycon
>>> import numpy as np
>>> register_type(
...     np.ndarray,
...     to_json=lambda arr: arr.tolist(),
...     from_json=lambda lst: np.array(lst)
... )
```

### qh.run_app(app, , use_server=False, \*\*kwargs)

Context manager for running a FastAPI app.

A convenience wrapper around AppRunner.

* **Parameters:**
  * **app** (`FastAPI`) – FastAPI application
  * **use_server** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, runs real server; if False, uses TestClient
  * **\*\*kwargs** – Additional arguments passed to AppRunner
* **Yields:**
  TestClient or base URL string
* **Return type:**
  [*Generator*](https://docs.python.org/3/library/typing.html#typing.Generator)[*TestClient* | [*str*](https://docs.python.org/3/builtins/stdtypes.html#str), *None*, *None*]

### Examples

```pycon
>>> from qh import mk_app
>>> from qh.testing import run_app
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
>>> # Quick testing with TestClient
>>> with run_app(app) as client:
...     result = client.post('/add', json={'x': 3, 'y': 5})
...     assert result.json() == 8
>>> # Integration testing with real server
>>> with run_app(app, use_server=True, port=8001) as url:
...     result = requests.post(f'{url}/add', json={'x': 3, 'y': 5})
...     assert result.json() == 8
```

### qh.serve_app(app, port=8000, host='127.0.0.1')

Context manager for running app with real server.

Convenience wrapper for integration testing with a real uvicorn server.

* **Parameters:**
  * **app** (`FastAPI`) – FastAPI application
  * **port** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Port to bind to
  * **host** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Host to bind to
* **Yields:**
  Base URL string
* **Return type:**
  [*Generator*](https://docs.python.org/3/library/typing.html#typing.Generator)[[*str*](https://docs.python.org/3/builtins/stdtypes.html#str), *None*, *None*]

### Examples

```pycon
>>> from qh import mk_app
>>> from qh.testing import serve_app
>>> import requests
>>> def multiply(x: int, y: int) -> int:
...     return x * y
>>> app = mk_app([multiply])
>>> with serve_app(app, port=8001) as url:
...     response = requests.post(f'{url}/multiply', json={'x': 4, 'y': 5})
...     assert response.json() == 20
```

### qh.service_running(, url=None, app=None, launcher=None, port=8000, host='127.0.0.1', startup_wait=2.0, readiness_check_interval=0.2, readiness_timeout=10.0, log_level='error')

Ensure an HTTP service is running for testing purposes.

This context manager checks if a service is already running at the specified URL.
If not running, it launches the service using one of the provided methods (app,
launcher). Either way, it leaves the service running on exit – see the Note below.

Exactly one of `url`, `app`, or `launcher` must be provided.

#### NOTE
Services are launched in daemon threads (not processes) to avoid
serialization issues with FastAPI apps on macOS. A service this
context manager launched is not stopped on exit: the thread ends
with the process.

* **Parameters:**
  * **url** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – URL of an existing service to check (e.g., ‘[http://localhost:8000](http://localhost:8000)’).
    If provided alone, will fail if service is not running.
  * **app** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[`FastAPI`]) – FastAPI/ASGI app to serve using uvicorn
  * **launcher** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[], [`None`](https://docs.python.org/3/builtins/constants.html#None)]]) – Custom callable to launch the service (will run in background thread)
  * **port** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Port to bind service to (used with app or launcher)
  * **host** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Host to bind service to (used with app or launcher)
  * **startup_wait** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Initial wait time after launching (seconds)
  * **readiness_check_interval** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Polling interval for readiness checks (seconds)
  * **readiness_timeout** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Maximum time to wait for service to be ready (seconds)
  * **log_level** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Uvicorn log level when serving an app
* **Yields:**
  *ServiceInfo* – Information about the running service including URL and status
* **Raises:**
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If none, or more than one, of `url`, `app` and `launcher` is given.
  * [**RuntimeError**](https://docs.python.org/3/builtins/exceptions.html#RuntimeError) – If `url` alone was given and nothing answers there, or if a
        launched service does not answer within `readiness_timeout` seconds.
* **Return type:**
  [*Generator*](https://docs.python.org/3/library/typing.html#typing.Generator)[[*ServiceInfo*](_autosummary/qh.testing.html.md#qh.testing.ServiceInfo), *None*, *None*]

### Examples

Test a qh app (launches a server in a daemon thread):

```pycon
>>> from qh import mk_app
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
>>> with service_running(app=app, port=8001) as info:
...     response = requests.post(f'{info.url}/add', json={'x': 3, 'y': 5})
...     assert response.json() == 8
...     assert not info.was_already_running
```

Test an already-running service (won’t tear down):

```pycon
>>> with service_running(url='https://api.github.com') as info:
...     response = requests.get(f'{info.url}/users/octocat')
...     assert info.was_already_running
```

Use custom launcher:

```pycon
>>> def my_launcher():
...     # Custom service startup code
...     pass
>>> with service_running(launcher=my_launcher, port=8002) as info:
...     # Test your service
...     pass
```

### qh.test_app(app)

Call an app in-process through a `TestClient`, no server, no port.

The most common case, and the fastest: requests are dispatched straight
to the ASGI app. Use `serve_app` when a real socket matters (another
process, a browser, a generated client pointed at a URL).

* **Parameters:**
  **app** (`FastAPI`) – FastAPI application
* **Yields:**
  TestClient instance
* **Return type:**
  [*Generator*](https://docs.python.org/3/library/typing.html#typing.Generator)[*TestClient*, *None*, *None*]

### Examples

```pycon
>>> from qh import mk_app
>>> from qh.testing import test_app
>>> def hello(name: str = "World") -> str:
...     return f"Hello, {name}!"
>>> app = mk_app([hello])
>>> with test_app(app) as client:
...     client.post('/hello', json={'name': 'Alice'}).json()
'Hello, Alice!'
```

A missing required argument is a `422`, as in FastAPI:

```pycon
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> with test_app(mk_app([add])) as client:
...     client.post('/add', json={'x': 3}).status_code
422
```

### qh.use_au_backend(backend=None, store=None, \*\*au_config_kwargs)

Create a qh TaskConfig that uses au backend and storage.

This is the main bridge function that lets qh use au.

* **Parameters:**
  * **backend** ([`None`](https://docs.python.org/3/builtins/constants.html#None)) – au ComputationBackend (ThreadBackend, ProcessBackend, RQBackend, etc.)
    If None, uses au’s default from config
  * **store** ([`None`](https://docs.python.org/3/builtins/constants.html#None)) – au ComputationStore (FileSystemStore, etc.)
    If None, uses au’s default from config
  * **\*\*au_config_kwargs** – Additional config passed to au
* **Return type:**
  [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig)
* **Returns:**
  TaskConfig configured to use au
* **Raises:**
  [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – If the `au` package is not installed.

### Example

```pycon
>>> from au import ThreadBackend, FileSystemStore
>>> from qh import mk_app
>>> from qh.au_integration import use_au_backend
>>> # Use au with thread backend and filesystem storage
>>> def slow_func(n: int) -> int:
...     import time
...     time.sleep(2)
...     return n * 2
>>> app = mk_app(
...     [slow_func],
...     async_funcs=['slow_func'],
...     async_config=use_au_backend(
...         backend=ThreadBackend(),
...         store=FileSystemStore('/tmp/qh_tasks')
...     )
... )
```

Example with au’s global config:

```pycon
>>> # Set AU environment variables:
>>> # AU_BACKEND=redis
>>> # AU_REDIS_URL=redis://localhost:6379
>>> # AU_STORAGE=filesystem
>>> # AU_STORAGE_PATH=/var/qh/tasks
>>> app = mk_app(
...     [slow_func],
...     async_funcs=['slow_func'],
...     async_config=use_au_backend()  # Uses au's config
... )
```

### qh.use_au_process_backend(storage_path='/tmp/qh_au_tasks', ttl_seconds=3600)

Use au’s ProcessBackend for CPU-bound tasks.

* **Return type:**
  [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig)

### qh.use_au_redis_backend(redis_url='redis://localhost:6379', storage_path='/tmp/qh_au_tasks', ttl_seconds=3600)

Use au’s Redis/RQ backend for distributed tasks.

* **Return type:**
  [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig)

### qh.use_au_thread_backend(storage_path='/tmp/qh_au_tasks', ttl_seconds=3600)

Use au’s ThreadBackend with filesystem storage.

* **Return type:**
  [`TaskConfig`](_autosummary/qh.async_tasks.html.md#qh.async_tasks.TaskConfig)

### Modules

| [`app`](_autosummary/qh.app.html.md#module-qh.app)                         | Build a FastAPI application from plain Python functions.                             |
|--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`async_endpoints`](_autosummary/qh.async_endpoints.html.md#module-qh.async_endpoints) | Helper functions to create task management endpoints.                                |
| [`async_tasks`](_autosummary/qh.async_tasks.html.md#module-qh.async_tasks)         | Async task processing for qh.                                                        |
| [`au_integration`](_autosummary/qh.au_integration.html.md#module-qh.au_integration)   | Integration layer between qh and au.                                                 |
| [`base`](_autosummary/qh.base.html.md#module-qh.base)                       | Config-free dispatch of Python callables as FastAPI routes, plus a store dispatcher. |
| [`client`](_autosummary/qh.client.html.md#module-qh.client)                   | Python client generation from OpenAPI specs.                                         |
| [`config`](_autosummary/qh.config.html.md#module-qh.config)                   | Configuration system for qh with layered defaults.                                   |
| [`conventions`](_autosummary/qh.conventions.html.md#module-qh.conventions)         | Convention-based routing for qh.                                                     |
| [`core`](_autosummary/qh.core.html.md#module-qh.core)                       | Minimal function-to-route dispatch built on `i2.wrapper.Wrap`.                       |
| [`endpoint`](_autosummary/qh.endpoint.html.md#module-qh.endpoint)               | Endpoint creation using i2.Wrap to transform functions into FastAPI routes.          |
| [`jsclient`](_autosummary/qh.jsclient.html.md#module-qh.jsclient)               | JavaScript and TypeScript client generation from OpenAPI specs.                      |
| [`openapi`](_autosummary/qh.openapi.html.md#module-qh.openapi)                 | OpenAPI generation for qh — including JSON Schema derived from Python type hints.    |
| [`rules`](_autosummary/qh.rules.html.md#module-qh.rules)                     | Transformation rule system for qh.                                                   |
| [`stores_qh`](_autosummary/qh.stores_qh.html.md#module-qh.stores_qh)             | FastAPI service for operating on stores objects.                                     |
| [`testing`](_autosummary/qh.testing.html.md#module-qh.testing)                 | Run a qh (or any FastAPI) app for a test: in-process, or on a real port.             |
| [`types`](_autosummary/qh.types.html.md#module-qh.types)                     | Type registry for qh - automatic serialization/deserialization for custom types.     |


# _autosummary/qh.jsclient.html.md

# qh.jsclient

JavaScript and TypeScript client generation from OpenAPI specs.

Generates client code for calling qh HTTP services from JavaScript/TypeScript applications.

### Functions

| [`export_js_client`](_autosummary/qh.jsclient.html.md#qh.jsclient.export_js_client)(openapi_spec, \*[, ...])       | Generate JavaScript client class from OpenAPI spec.    |
|--------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| [`export_ts_client`](_autosummary/qh.jsclient.html.md#qh.jsclient.export_ts_client)(openapi_spec, \*[, ...])       | Generate TypeScript client class from OpenAPI spec.    |
| [`generate_js_function`](_autosummary/qh.jsclient.html.md#qh.jsclient.generate_js_function)(name, path, method[, ...]) | Generate JavaScript function for calling an endpoint.  |
| [`generate_ts_function`](_autosummary/qh.jsclient.html.md#qh.jsclient.generate_ts_function)(name, path, method[, ...]) | Generate TypeScript function for calling an endpoint.  |
| [`generate_ts_interface`](_autosummary/qh.jsclient.html.md#qh.jsclient.generate_ts_interface)(name, signature_info)     | Generate TypeScript interface for function parameters. |
| [`python_type_to_ts_type`](_autosummary/qh.jsclient.html.md#qh.jsclient.python_type_to_ts_type)(python_type)             | Convert Python type annotation to TypeScript type.     |

### qh.jsclient.export_js_client(openapi_spec, , class_name='ApiClient', use_axios=False, base_url='http://localhost:8000')

Generate JavaScript client class from OpenAPI spec.

* **Parameters:**
  * **openapi_spec** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – OpenAPI specification dictionary
  * **class_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name for the generated class
  * **use_axios** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Use axios instead of fetch
  * **base_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Default base URL
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  JavaScript code as string

### Example

```pycon
>>> from qh import mk_app, export_openapi
>>> from qh.jsclient import export_js_client
>>> app = mk_app([add, subtract])
>>> spec = export_openapi(app)
>>> js_code = export_js_client(spec, use_axios=True)
```

### qh.jsclient.export_ts_client(openapi_spec, , class_name='ApiClient', use_axios=False, base_url='http://localhost:8000')

Generate TypeScript client class from OpenAPI spec.

* **Parameters:**
  * **openapi_spec** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – OpenAPI specification dictionary
  * **class_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name for the generated class
  * **use_axios** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Use axios instead of fetch
  * **base_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Default base URL
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  TypeScript code as string

### Example

```pycon
>>> from qh import mk_app, export_openapi
>>> from qh.jsclient import export_ts_client
>>> app = mk_app([add, subtract])
>>> spec = export_openapi(app, include_python_metadata=True)
>>> ts_code = export_ts_client(spec, use_axios=True)
```

### qh.jsclient.generate_js_function(name, path, method, signature_info=None, use_axios=False)

Generate JavaScript function for calling an endpoint.

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Function name
  * **path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – HTTP path
  * **method** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – HTTP method
  * **signature_info** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – Optional x-python-signature metadata
  * **use_axios** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Use axios instead of fetch
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  JavaScript function code

### qh.jsclient.generate_ts_function(name, path, method, signature_info=None, use_axios=False)

Generate TypeScript function for calling an endpoint.

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Function name
  * **path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – HTTP path
  * **method** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – HTTP method
  * **signature_info** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – Optional x-python-signature metadata
  * **use_axios** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Use axios instead of fetch
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  TypeScript function code with type annotations

### qh.jsclient.generate_ts_interface(name, signature_info)

Generate TypeScript interface for function parameters.

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Function name
  * **signature_info** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – x-python-signature metadata
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  TypeScript interface definition

### qh.jsclient.python_type_to_ts_type(python_type)

Convert Python type annotation to TypeScript type.

* **Parameters:**
  **python_type** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Python type string (e.g., “int”, “str”, “list[int]”)
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  TypeScript type string


# _autosummary/qh.openapi.html.md

# qh.openapi

OpenAPI generation for qh — including JSON Schema derived from Python type hints.

`qh` builds every route with a generic `endpoint(request: Request)` signature
(see [`qh.endpoint`](_autosummary/qh.endpoint.html.md#module-qh.endpoint)): the wrapped function’s real parameters and return type
are parsed *manually* from the request body, which makes them invisible to
FastAPI’s own OpenAPI machinery. Out of the box, therefore, `/openapi.json`
lists routes and docstrings but emits empty `{}` request/response schemas and
no `components.schemas`.

This module closes that gap. It introspects each wrapped function’s Python type
hints and derives a complete OpenAPI document:

- `requestBody` — a JSON Schema object built from the JSON-body parameters,
- `parameters` — path/query parameters,
- `responses` — a JSON Schema for the return type,
- `components.schemas` — one named schema per dataclass / `TypedDict` /
  Pydantic model / `NamedTuple` / `Enum` encountered, referenced via `$ref`,
- `x-python-*` extensions — Python signature metadata for bidirectional
  Python ↔ HTTP transformation.

The whole thing is **additive**: the request-handling path is untouched, and
[`install_enhanced_openapi()`](_autosummary/qh.openapi.html.md#qh.openapi.install_enhanced_openapi) falls back to FastAPI’s plain schema if
enhancement ever raises — so a converter bug can never break `/openapi.json`.

Public surface:

- [`python_type_to_json_schema()`](_autosummary/qh.openapi.html.md#qh.openapi.python_type_to_json_schema) — the Python-type-hint → JSON Schema converter.
- [`enhance_openapi_schema()`](_autosummary/qh.openapi.html.md#qh.openapi.enhance_openapi_schema) — full enhanced OpenAPI document for an app.
- [`export_openapi()`](_autosummary/qh.openapi.html.md#qh.openapi.export_openapi) — [`enhance_openapi_schema()`](_autosummary/qh.openapi.html.md#qh.openapi.enhance_openapi_schema) plus optional file output.
- [`install_enhanced_openapi()`](_autosummary/qh.openapi.html.md#qh.openapi.install_enhanced_openapi) — make an app serve the enhanced doc at
  `/openapi.json` (and render it in `/docs`).

### Functions

| [`build_parameters`](_autosummary/qh.openapi.html.md#qh.openapi.build_parameters)(func, param_specs, schemas)       | Build OpenAPI `parameters` entries for a function's path/query arguments.   |
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`build_request_body_schema`](_autosummary/qh.openapi.html.md#qh.openapi.build_request_body_schema)(func, param_specs, ...)  | Build the JSON Schema object for a function's JSON-body parameters.         |
| [`build_response_schema`](_autosummary/qh.openapi.html.md#qh.openapi.build_response_schema)(func, schemas)               | Build the JSON Schema for a function's return type.                         |
| [`enhance_openapi_schema`](_autosummary/qh.openapi.html.md#qh.openapi.enhance_openapi_schema)(app, \*[, ...])             | Generate an enhanced OpenAPI schema for a `qh` app.                         |
| [`export_openapi`](_autosummary/qh.openapi.html.md#qh.openapi.export_openapi)(app, \*[, include_examples, ...])   | Export the enhanced OpenAPI schema, optionally writing it to a file.        |
| [`extract_function_signature`](_autosummary/qh.openapi.html.md#qh.openapi.extract_function_signature)(func)                   | Extract detailed signature information from a function.                     |
| [`generate_examples_for_function`](_autosummary/qh.openapi.html.md#qh.openapi.generate_examples_for_function)(func)               | Generate example requests/responses for a function.                         |
| [`get_python_type_name`](_autosummary/qh.openapi.html.md#qh.openapi.get_python_type_name)(type_hint)                    | Get a string representation of a Python type.                               |
| [`install_enhanced_openapi`](_autosummary/qh.openapi.html.md#qh.openapi.install_enhanced_openapi)(app, \*\*enhance_kwargs)  | Make `app` serve the enhanced OpenAPI schema at its `/openapi.json`.        |
| [`python_type_to_json_schema`](_autosummary/qh.openapi.html.md#qh.openapi.python_type_to_json_schema)(type_hint, schemas, \*) | Convert a Python type hint to a JSON Schema fragment.                       |

### qh.openapi.build_parameters(func, param_specs, schemas)

Build OpenAPI `parameters` entries for a function’s path/query arguments.

Path parameters are always required; query parameters are required only
when the Python parameter has no default.

* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### qh.openapi.build_request_body_schema(func, param_specs, schemas)

Build the JSON Schema object for a function’s JSON-body parameters.

Only parameters whose resolved HTTP location is the JSON body are included;
path/query/header parameters are emitted separately by
[`build_parameters()`](_autosummary/qh.openapi.html.md#qh.openapi.build_parameters). A parameter with no default is `required`.

* **Parameters:**
  * **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – The function whose parameters are being described.
  * **param_specs** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – Parameter name to `TransformSpec`, deciding each
    parameter’s HTTP location (see `_param_location`).
  * **schemas** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – The mutable `components.schemas` accumulator, passed
    through to `python_type_to_json_schema` for composite types.
* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]
* **Returns:**
  an object JSON Schema, or `None` when the function has no body
  parameters (e.g. a GET route whose arguments are all query parameters).

### qh.openapi.build_response_schema(func, schemas)

Build the JSON Schema for a function’s return type.

A missing return annotation yields the permissive empty schema; a `None`
return yields `{"type": "null"}` (`qh` still replies with a JSON body).

* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### qh.openapi.enhance_openapi_schema(app, , include_examples=True, include_python_metadata=True, include_schemas=True, include_transformers=False)

Generate an enhanced OpenAPI schema for a `qh` app.

On top of FastAPI’s base document this fills in what `qh`’s
`Request`-based endpoints hide from FastAPI:

- `requestBody` / `parameters` / `responses` JSON Schema derived from
  each wrapped function’s Python type hints (`include_schemas`),
- `components.schemas` for every dataclass / `TypedDict` / Pydantic
  model / `NamedTuple` / `Enum` referenced,
- `x-python-signature` metadata (`include_python_metadata`),
- request examples (`include_examples`).

* **Parameters:**
  * **app** (`FastAPI`) – the FastAPI application.
  * **include_examples** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – add example requests.
  * **include_python_metadata** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – add `x-python-*` extensions.
  * **include_schemas** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – derive `requestBody` / `responses` /
    `components.schemas` from the Python type hints.
  * **include_transformers** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – add (placeholder) transformation metadata.
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  the enhanced OpenAPI schema dictionary.

### qh.openapi.export_openapi(app, , include_examples=True, include_python_metadata=True, include_schemas=True, include_transformers=False, output_file=None)

Export the enhanced OpenAPI schema, optionally writing it to a file.

* **Parameters:**
  * **app** (`FastAPI`) – the FastAPI application.
  * **include_examples** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – include example requests.
  * **include_python_metadata** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – include `x-python-*` extensions.
  * **include_schemas** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – derive `requestBody` / `responses` /
    `components.schemas` from the Python type hints.
  * **include_transformers** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – include transformation metadata.
  * **output_file** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – optional path to write the JSON document to.
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  the enhanced OpenAPI schema dictionary.

### Example

```pycon
>>> from qh import mk_app
>>> from qh.openapi import export_openapi
>>> app = mk_app([my_func])
>>> spec = export_openapi(app, include_examples=True)
```

### qh.openapi.extract_function_signature(func)

Extract detailed signature information from a function.

* **Parameters:**
  **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – The function to inspect.
* **Returns:**
  - name: function name
  - module: module path
  - parameters: list of parameter info
  - return_type: return type annotation
  - docstring: function docstring
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### qh.openapi.generate_examples_for_function(func)

Generate example requests/responses for a function.

Uses type hints to generate sensible example values.

* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### qh.openapi.get_python_type_name(type_hint)

Get a string representation of a Python type.

`inspect.Parameter.empty` and `None` map to `"Any"`. Anything else
with a `__name__` uses that name alone, with no type arguments — on
Python 3.10+ this includes builtin generic aliases (`list[int]`) and
`typing` generics (`Optional[str]`, `Dict[str, int]`), since they
all carry a `__name__` now. The bracketed-argument form only appears
for the rare origin type that lacks `__name__`.

* **Parameters:**
  **type_hint** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – A type or type annotation, or `inspect.Parameter.empty`.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  The type’s bare name, e.g. `"int"` or `"list"`.

### Examples

```pycon
>>> get_python_type_name(int)
'int'
>>> get_python_type_name(str)
'str'
>>> get_python_type_name(list[int])
'list'
>>> from typing import Optional
>>> get_python_type_name(Optional[str])
'Optional'
```

### qh.openapi.install_enhanced_openapi(app, \*\*enhance_kwargs)

Make `app` serve the enhanced OpenAPI schema at its `/openapi.json`.

Overrides `app.openapi` so the document returned by FastAPI — and rendered
by `/docs` and `/redoc` — carries the request/response JSON Schema and
`components.schemas` derived from the wrapped functions’ Python type hints.

The override is **defensive**: if enhancement raises for any reason, it
falls back to FastAPI’s plain schema, so a converter bug can never turn
`/openapi.json` into a 500.

* **Parameters:**
  * **app** (`FastAPI`) – the FastAPI application (typically the result of [`qh.mk_app()`](_autosummary/qh.html.md#qh.mk_app)).
  * **\*\*enhance_kwargs** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – forwarded to [`enhance_openapi_schema()`](_autosummary/qh.openapi.html.md#qh.openapi.enhance_openapi_schema).
* **Return type:**
  `FastAPI`
* **Returns:**
  the same `app`, for chaining.

### qh.openapi.python_type_to_json_schema(type_hint, schemas, , \_stack=None)

Convert a Python type hint to a JSON Schema fragment.

Primitives, containers and unions are inlined. Named composite types —
dataclasses, `TypedDict`s, Pydantic models, `NamedTuple`s and
`Enum`s — are registered in `schemas` (the OpenAPI
`components.schemas` table) and returned as a `$ref`, so the same type
used in several places is described once.

* **Parameters:**
  * **type_hint** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – the Python type / annotation to convert. `Any` and
    `inspect.Parameter.empty` map to the empty schema `{}`
    (matches anything); `None` / `NoneType` map to `{"type": "null"}`.
  * **schemas** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – the mutable `components.schemas` accumulator — composite
    types encountered are added here, keyed by their class name.
  * **\_stack** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`frozenset`](https://docs.python.org/3/builtins/stdtypes.html#frozenset)]) – internal — the set of composite type names currently being
    built, used to break recursion on self-referential types.
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  a JSON Schema dict — an inline fragment, or `{"$ref": ...}` for a
  named composite type.

### Examples

```pycon
>>> python_type_to_json_schema(int, {})
{'type': 'integer'}
>>> python_type_to_json_schema(list[str], {})
{'type': 'array', 'items': {'type': 'string'}}
>>> python_type_to_json_schema(Optional[int], {})
{'anyOf': [{'type': 'integer'}, {'type': 'null'}]}
```


# _autosummary/qh.rules.html.md

# qh.rules

Transformation rule system for qh.

Supports multi-dimensional matching:

- Type-based
- Argument name-based
- Function name-based
- Function object-based
- Default value-based
- Any combination thereof

Rules are layered with first-match semantics, from specific to general.

### Functions

| [`extract_param_context`](_autosummary/qh.rules.html.md#qh.rules.extract_param_context)(func, param_name)           | Extract context information for a parameter.          |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------|
| [`resolve_transform`](_autosummary/qh.rules.html.md#qh.rules.resolve_transform)(func, param_name[, rule_chain]) | Resolve transformation specification for a parameter. |

### Classes

| [`CompositeRule`](_autosummary/qh.rules.html.md#qh.rules.CompositeRule)(rules[, combine_mode, spec])   | Rule that combines multiple conditions.                       |
|-----------------------------------------------------------------------------------------------|---------------------------------------------------------------|
| [`DefaultValueRule`](_autosummary/qh.rules.html.md#qh.rules.DefaultValueRule)(predicate, spec)            | Rule that matches based on default values.                    |
| [`FuncNameRule`](_autosummary/qh.rules.html.md#qh.rules.FuncNameRule)(pattern_map)                    | Rule that matches based on function name pattern.             |
| [`FuncRule`](_autosummary/qh.rules.html.md#qh.rules.FuncRule)(func_map)                           | Rule that matches based on function.                          |
| [`HttpLocation`](_autosummary/qh.rules.html.md#qh.rules.HttpLocation)(\*values)                       | Where in HTTP request/response to map a parameter.            |
| [`NameRule`](_autosummary/qh.rules.html.md#qh.rules.NameRule)(name_map)                           | Rule that matches based on parameter name.                    |
| [`Rule`](_autosummary/qh.rules.html.md#qh.rules.Rule)(\*args, \*\*kwargs)                     | Protocol for transformation rules.                            |
| [`RuleChain`](_autosummary/qh.rules.html.md#qh.rules.RuleChain)([rules])                           | Chain of rules evaluated in order with first-match semantics. |
| [`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)([http_location, ingress, ...]) | Specification for how to transform a parameter.               |
| [`TypeRule`](_autosummary/qh.rules.html.md#qh.rules.TypeRule)(type_map)                           | Rule that matches based on parameter type.                    |

### *class* qh.rules.CompositeRule(rules, combine_mode='all', spec=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Rule that combines multiple conditions.

#### match(, param_name, param_type, param_default, func, func_name)

Match based on combination of sub-rules.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

### *class* qh.rules.DefaultValueRule(predicate, spec)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Rule that matches based on default values.

#### match(, param_name, param_type, param_default, func, func_name)

Match if predicate returns True for default value.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

### *class* qh.rules.FuncNameRule(pattern_map)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Rule that matches based on function name pattern.

#### match(, param_name, param_type, param_default, func, func_name)

Match by function name pattern.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

### *class* qh.rules.FuncRule(func_map)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Rule that matches based on function.

#### match(, param_name, param_type, param_default, func, func_name)

Match by function object and parameter name.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

### *class* qh.rules.HttpLocation(\*values)

Bases: [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

Where in HTTP request/response to map a parameter.

### *class* qh.rules.NameRule(name_map)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Rule that matches based on parameter name.

#### match(, param_name, param_type, param_default, func, func_name)

Match by parameter name.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

### *class* qh.rules.Rule(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Protocol for transformation rules.

#### match(, param_name, param_type, param_default, func, func_name)

Check if this rule matches the given parameter context.

* **Parameters:**
  * **param_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The parameter’s name.
  * **param_type** ([`type`](https://docs.python.org/3/builtins/functions.html#type)) – The parameter’s type annotation.
  * **param_default** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – The parameter’s default, or `inspect.Parameter.empty`.
  * **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – The function the parameter belongs to.
  * **func_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `func`’s name.
* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]
* **Returns:**
  TransformSpec if matched, None otherwise

### *class* qh.rules.RuleChain(rules=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Chain of rules evaluated in order with first-match semantics.

Rules are tried from most specific to most general.

```pycon
>>> chain = RuleChain()
>>> chain.add_rule(TypeRule({int: TransformSpec(http_location=HttpLocation.QUERY)}))
>>> chain.match(param_name='x', param_type=int).http_location
<HttpLocation.QUERY: 'query'>
>>> chain.match(param_name='y', param_type=str) is None
True
```

#### add_rule(rule, priority=0)

Add a rule with optional priority (higher = evaluated earlier).

#### match(\*, param_name, param_type=<class 'NoneType'>, param_default, func=None, func_name='')

Find first matching rule.

* **Parameters:**
  * **param_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The parameter’s name.
  * **param_type** ([`type`](https://docs.python.org/3/builtins/functions.html#type)) – The parameter’s type annotation.
  * **param_default** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – The parameter’s default, or `inspect.Parameter.empty`.
  * **func** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)]) – The function the parameter belongs to, if known.
  * **func_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `func`’s name.
* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]
* **Returns:**
  TransformSpec from first matching rule, or None if no match

### *class* qh.rules.TransformSpec(http_location=HttpLocation.JSON_BODY, ingress=None, egress=None, http_name=None, metadata=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Specification for how to transform a parameter.

### *class* qh.rules.TypeRule(type_map)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Rule that matches based on parameter type.

#### match(, param_name, param_type, param_default, func, func_name)

Match by type, including type hierarchy.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

### qh.rules.extract_param_context(func, param_name)

Extract context information for a parameter.

* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### qh.rules.resolve_transform(func, param_name, rule_chain=None)

Resolve transformation specification for a parameter.

Resolution order:

1. Rule chain (explicit rules)
2. Type registry (registered types)
3. Default fallback (JSON body, no transformation)

* **Parameters:**
  * **func** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – The function containing the parameter
  * **param_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the parameter
  * **rule_chain** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`RuleChain`](_autosummary/qh.rules.html.md#qh.rules.RuleChain)]) – Custom rule chain (uses DEFAULT_RULE_CHAIN if None)
* **Return type:**
  [`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)
* **Returns:**
  TransformSpec with transformation details


# _autosummary/qh.stores_qh.html.md

# qh.stores_qh

FastAPI service for operating on stores objects.

This module provides a RESTful API for interacting with mall objects,
which are Mappings of MutableMappings (dict of dicts).

### Functions

| [`add_mall_access`](_autosummary/qh.stores_qh.html.md#qh.stores_qh.add_mall_access)(get_mall[, app, write, delete])   | Add mall/store access endpoints to a FastAPI application.   |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------|
| [`add_store_access`](_autosummary/qh.stores_qh.html.md#qh.stores_qh.add_store_access)(get_obj[, app, methods, ...])    | Add store access endpoints to a FastAPI application.        |
| [`create_method_endpoint`](_autosummary/qh.stores_qh.html.md#qh.stores_qh.create_method_endpoint)(method_name, config, ...)  | Create an endpoint function for a specific mapping method.  |

### Classes

| [`StoreValue`](_autosummary/qh.stores_qh.html.md#qh.stores_qh.StoreValue)(\*\*data)   | Request body for setting store values.   |
|-------------------------------------------------------------------------|------------------------------------------|

### *class* qh.stores_qh.StoreValue(\*\*data)

Bases: `BaseModel`

Request body for setting store values.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### qh.stores_qh.add_mall_access(get_mall, app=None, , write=False, delete=False)

Add mall/store access endpoints to a FastAPI application.

* **Return type:**
  `FastAPI`

### qh.stores_qh.add_store_access(get_obj, app=None, , methods=None, get_obj_dispatch=None, base_path='/users/{user_id}/mall/{store_key}')

Add store access endpoints to a FastAPI application.

* **Parameters:**
  * **get_obj** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)]) – Function that takes an identifier and returns a mapping object
  * **app** – 

    Can be:
    - None: creates a new FastAPI app with default settings
    - FastAPI instance: uses this existing app
    - str: creates a new FastAPI app with this title
    - dict: creates a new FastAPI app with these kwargs
  * **methods** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)]]]) – 

    Dictionary mapping method names to dispatch configuration
    - Key is the mapping method name (e.g., ‘_\_iter_\_’, ‘_\_getitem_\_’)
    - Value is None to use defaults or a dict with configuration
  * **get_obj_dispatch** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)]) – Configuration for how to dispatch the get_obj function
  * **base_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Base path for all endpoints
* **Return type:**
  `FastAPI`
* **Returns:**
  FastAPI application instance with store endpoints added

### qh.stores_qh.create_method_endpoint(method_name, config, get_obj_fn, path_params=None)

Create an endpoint function for a specific mapping method.

* **Parameters:**
  * **method_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The mapping method to dispatch (e.g., ‘_\_iter_\_’, ‘_\_getitem_\_’)
  * **config** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)) – Configuration for the endpoint
  * **get_obj_fn** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)) – Function to retrieve the object to operate on
  * **path_params** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – List of path parameter names (e.g., [‘user_id’, ‘store_key’])
* **Returns:**
  An async endpoint function compatible with FastAPI
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If `path_params` has a length this method’s branch does
      not implement (currently 1 or 2 are supported).


# _autosummary/qh.testing.html.md

# qh.testing

Run a qh (or any FastAPI) app for a test: in-process, or on a real port.

Two ways to exercise an app. `test_app` (and `run_app`, `AppRunner`) wrap
FastAPI’s `TestClient` so requests go straight to the app with no socket.
`serve_app` and `service_running` start uvicorn in a daemon thread and
give you a base URL to hit with `requests`; `service_running` can also
notice a service that is already up and leave it alone. `quick_test` is the
one-liner: build an app around one function, POST to it, return the JSON.

Similar tools elsewhere: `meshed.tools.launch_webservice`,
`strand.taskrunning.utils.run_process`, and the service helpers in
`py2http`.

Main entry points:

- `test_app`: `with test_app(app) as client:` for in-process requests
- `quick_test`: call one function through HTTP and get its JSON back
- `serve_app` / `service_running`: a live server on a port, for integration tests

```pycon
>>> from qh import mk_app
>>> from qh.testing import quick_test
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> quick_test(add, x=3, y=5)
8
```

### Functions

| [`service_running`](_autosummary/qh.testing.html.md#qh.testing.service_running)(\*[, url, app, launcher, ...])   | Ensure an HTTP service is running for testing purposes.              |
|---------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| [`run_app`](_autosummary/qh.testing.html.md#qh.testing.run_app)(app, \*[, use_server])                   | Context manager for running a FastAPI app.                           |
| [`test_app`](_autosummary/qh.testing.html.md#qh.testing.test_app)(app)                                    | Call an app in-process through a `TestClient`, no server, no port.   |
| [`serve_app`](_autosummary/qh.testing.html.md#qh.testing.serve_app)(app[, port, host])                     | Context manager for running app with real server.                    |
| [`quick_test`](_autosummary/qh.testing.html.md#qh.testing.quick_test)(func, \*\*kwargs)                     | Call one function through HTTP and return the decoded JSON response. |
| [`app_runner`](_autosummary/qh.testing.html.md#qh.testing.app_runner)(app, \*[, use_server])                | Context manager for running a FastAPI app.                           |
| [`test_client`](_autosummary/qh.testing.html.md#qh.testing.test_client)(app)                                 | Call an app in-process through a `TestClient`, no server, no port.   |

### Classes

| [`ServiceInfo`](_autosummary/qh.testing.html.md#qh.testing.ServiceInfo)(url, was_already_running[, ...])      | Information about a running service.                                          |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`AppRunner`](_autosummary/qh.testing.html.md#qh.testing.AppRunner)(app, \*[, use_server, host, port, ...]) | Context manager for running a FastAPI app in test mode or with a real server. |

### *class* qh.testing.AppRunner(app, , use_server=False, host='127.0.0.1', port=8000, server_timeout=2.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Context manager for running a FastAPI app in test mode or with a real server.

Supports both synchronous testing (using TestClient) and integration testing
(using a real uvicorn server). With `use_server=False` (the default) the
`with` block receives a `TestClient`; with `use_server=True` it
receives the base URL of a uvicorn server started in a daemon thread. On
exit the TestClient reference is dropped; a real server is not stopped, its
daemon thread ends with the process. `run_app` is the function form.

### Examples

Basic usage with TestClient:

```pycon
>>> from qh import mk_app
>>> from qh.testing import AppRunner
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
>>> with AppRunner(app) as client:
...     response = client.post('/add', json={'x': 3, 'y': 5})
...     assert response.json() == 8
```

With real server (integration testing):

```pycon
>>> with AppRunner(app, use_server=True, port=8001) as base_url:
...     response = requests.post(f'{base_url}/add', json={'x': 3, 'y': 5})
...     assert response.json() == 8
```

An exception inside the block propagates; `__exit__` still runs:

```pycon
>>> with AppRunner(app) as client:
...     raise ValueError("Test error")
```

### *class* qh.testing.ServiceInfo(url, was_already_running, thread=None, app=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Information about a running service.

#### url

Base URL of the service (e.g., ‘[http://localhost:8000](http://localhost:8000)’)

#### was_already_running

True if service was already running, False if launched

#### thread

Thread object if service was launched in thread, None otherwise

#### app

The FastAPI app if one was provided, None otherwise

### qh.testing.app_runner(app, , use_server=False, \*\*kwargs)

Context manager for running a FastAPI app.

A convenience wrapper around AppRunner.

* **Parameters:**
  * **app** (`FastAPI`) – FastAPI application
  * **use_server** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, runs real server; if False, uses TestClient
  * **\*\*kwargs** – Additional arguments passed to AppRunner
* **Yields:**
  TestClient or base URL string
* **Return type:**
  [*Generator*](https://docs.python.org/3/library/typing.html#typing.Generator)[*TestClient* | [*str*](https://docs.python.org/3/builtins/stdtypes.html#str), *None*, *None*]

### Examples

```pycon
>>> from qh import mk_app
>>> from qh.testing import run_app
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
>>> # Quick testing with TestClient
>>> with run_app(app) as client:
...     result = client.post('/add', json={'x': 3, 'y': 5})
...     assert result.json() == 8
>>> # Integration testing with real server
>>> with run_app(app, use_server=True, port=8001) as url:
...     result = requests.post(f'{url}/add', json={'x': 3, 'y': 5})
...     assert result.json() == 8
```

### qh.testing.quick_test(func, \*\*kwargs)

Call one function through HTTP and return the decoded JSON response.

Builds `mk_app([func])`, POSTs `kwargs` as the JSON body to
`/<func name>`, and returns `response.json()`. Meant for a one-line
smoke check of what a function looks like over HTTP.

* **Parameters:**
  * **func** – Function to test
  * **\*\*kwargs** – Arguments to pass to the function (sent as the JSON body)
* **Returns:**
  The JSON-decoded response body, i.e. the function’s return value after
  JSON round-tripping.
* **Raises:**
  **httpx.HTTPStatusError** – If the response status is 4xx or 5xx (for
      example a missing required argument, or an exception in
      `func`); `fastapi.testclient.TestClient` is httpx-based, not
      requests-based.

### Examples

```pycon
>>> from qh.testing import quick_test
>>>
>>> def add(x: int, y: int) -> int:
...     return x + y
>>>
>>> result = quick_test(add, x=3, y=5)
>>> assert result == 8
>>>
>>> def greet(name: str) -> str:
...     return f"Hello, {name}!"
>>>
>>> result = quick_test(greet, name="World")
>>> assert result == "Hello, World!"
```

### qh.testing.run_app(app, , use_server=False, \*\*kwargs)

Context manager for running a FastAPI app.

A convenience wrapper around AppRunner.

* **Parameters:**
  * **app** (`FastAPI`) – FastAPI application
  * **use_server** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, runs real server; if False, uses TestClient
  * **\*\*kwargs** – Additional arguments passed to AppRunner
* **Yields:**
  TestClient or base URL string
* **Return type:**
  [*Generator*](https://docs.python.org/3/library/typing.html#typing.Generator)[*TestClient* | [*str*](https://docs.python.org/3/builtins/stdtypes.html#str), *None*, *None*]

### Examples

```pycon
>>> from qh import mk_app
>>> from qh.testing import run_app
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
>>> # Quick testing with TestClient
>>> with run_app(app) as client:
...     result = client.post('/add', json={'x': 3, 'y': 5})
...     assert result.json() == 8
>>> # Integration testing with real server
>>> with run_app(app, use_server=True, port=8001) as url:
...     result = requests.post(f'{url}/add', json={'x': 3, 'y': 5})
...     assert result.json() == 8
```

### qh.testing.serve_app(app, port=8000, host='127.0.0.1')

Context manager for running app with real server.

Convenience wrapper for integration testing with a real uvicorn server.

* **Parameters:**
  * **app** (`FastAPI`) – FastAPI application
  * **port** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Port to bind to
  * **host** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Host to bind to
* **Yields:**
  Base URL string
* **Return type:**
  [*Generator*](https://docs.python.org/3/library/typing.html#typing.Generator)[[*str*](https://docs.python.org/3/builtins/stdtypes.html#str), *None*, *None*]

### Examples

```pycon
>>> from qh import mk_app
>>> from qh.testing import serve_app
>>> import requests
>>> def multiply(x: int, y: int) -> int:
...     return x * y
>>> app = mk_app([multiply])
>>> with serve_app(app, port=8001) as url:
...     response = requests.post(f'{url}/multiply', json={'x': 4, 'y': 5})
...     assert response.json() == 20
```

### qh.testing.service_running(, url=None, app=None, launcher=None, port=8000, host='127.0.0.1', startup_wait=2.0, readiness_check_interval=0.2, readiness_timeout=10.0, log_level='error')

Ensure an HTTP service is running for testing purposes.

This context manager checks if a service is already running at the specified URL.
If not running, it launches the service using one of the provided methods (app,
launcher). Either way, it leaves the service running on exit – see the Note below.

Exactly one of `url`, `app`, or `launcher` must be provided.

#### NOTE
Services are launched in daemon threads (not processes) to avoid
serialization issues with FastAPI apps on macOS. A service this
context manager launched is not stopped on exit: the thread ends
with the process.

* **Parameters:**
  * **url** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – URL of an existing service to check (e.g., ‘[http://localhost:8000](http://localhost:8000)’).
    If provided alone, will fail if service is not running.
  * **app** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[`FastAPI`]) – FastAPI/ASGI app to serve using uvicorn
  * **launcher** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[], [`None`](https://docs.python.org/3/builtins/constants.html#None)]]) – Custom callable to launch the service (will run in background thread)
  * **port** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Port to bind service to (used with app or launcher)
  * **host** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Host to bind service to (used with app or launcher)
  * **startup_wait** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Initial wait time after launching (seconds)
  * **readiness_check_interval** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Polling interval for readiness checks (seconds)
  * **readiness_timeout** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Maximum time to wait for service to be ready (seconds)
  * **log_level** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Uvicorn log level when serving an app
* **Yields:**
  *ServiceInfo* – Information about the running service including URL and status
* **Raises:**
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If none, or more than one, of `url`, `app` and `launcher` is given.
  * [**RuntimeError**](https://docs.python.org/3/builtins/exceptions.html#RuntimeError) – If `url` alone was given and nothing answers there, or if a
        launched service does not answer within `readiness_timeout` seconds.
* **Return type:**
  [*Generator*](https://docs.python.org/3/library/typing.html#typing.Generator)[[*ServiceInfo*](_autosummary/qh.testing.html.md#qh.testing.ServiceInfo), *None*, *None*]

### Examples

Test a qh app (launches a server in a daemon thread):

```pycon
>>> from qh import mk_app
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> app = mk_app([add])
>>> with service_running(app=app, port=8001) as info:
...     response = requests.post(f'{info.url}/add', json={'x': 3, 'y': 5})
...     assert response.json() == 8
...     assert not info.was_already_running
```

Test an already-running service (won’t tear down):

```pycon
>>> with service_running(url='https://api.github.com') as info:
...     response = requests.get(f'{info.url}/users/octocat')
...     assert info.was_already_running
```

Use custom launcher:

```pycon
>>> def my_launcher():
...     # Custom service startup code
...     pass
>>> with service_running(launcher=my_launcher, port=8002) as info:
...     # Test your service
...     pass
```

### qh.testing.test_app(app)

Call an app in-process through a `TestClient`, no server, no port.

The most common case, and the fastest: requests are dispatched straight
to the ASGI app. Use `serve_app` when a real socket matters (another
process, a browser, a generated client pointed at a URL).

* **Parameters:**
  **app** (`FastAPI`) – FastAPI application
* **Yields:**
  TestClient instance
* **Return type:**
  [*Generator*](https://docs.python.org/3/library/typing.html#typing.Generator)[*TestClient*, *None*, *None*]

### Examples

```pycon
>>> from qh import mk_app
>>> from qh.testing import test_app
>>> def hello(name: str = "World") -> str:
...     return f"Hello, {name}!"
>>> app = mk_app([hello])
>>> with test_app(app) as client:
...     client.post('/hello', json={'name': 'Alice'}).json()
'Hello, Alice!'
```

A missing required argument is a `422`, as in FastAPI:

```pycon
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> with test_app(mk_app([add])) as client:
...     client.post('/add', json={'x': 3}).status_code
422
```

### qh.testing.test_client(app)

Call an app in-process through a `TestClient`, no server, no port.

The most common case, and the fastest: requests are dispatched straight
to the ASGI app. Use `serve_app` when a real socket matters (another
process, a browser, a generated client pointed at a URL).

* **Parameters:**
  **app** (`FastAPI`) – FastAPI application
* **Yields:**
  TestClient instance
* **Return type:**
  [*Generator*](https://docs.python.org/3/library/typing.html#typing.Generator)[*TestClient*, *None*, *None*]

### Examples

```pycon
>>> from qh import mk_app
>>> from qh.testing import test_app
>>> def hello(name: str = "World") -> str:
...     return f"Hello, {name}!"
>>> app = mk_app([hello])
>>> with test_app(app) as client:
...     client.post('/hello', json={'name': 'Alice'}).json()
'Hello, Alice!'
```

A missing required argument is a `422`, as in FastAPI:

```pycon
>>> def add(x: int, y: int) -> int:
...     return x + y
>>> with test_app(mk_app([add])) as client:
...     client.post('/add', json={'x': 3}).status_code
422
```


# _autosummary/qh.types.html.md

# qh.types

Type registry for qh - automatic serialization/deserialization for custom types.

Supports:

- NumPy arrays and dtypes
- Pandas DataFrames and Series
- Custom user types
- Pydantic models

The type registry maps Python types to HTTP representations and provides
automatic conversion functions (ingress/egress transformations).

### Functions

| [`get_transform_spec_for_type`](_autosummary/qh.types.html.md#qh.types.get_transform_spec_for_type)(python_type)           | Get TransformSpec for a type from global registry.   |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------|
| [`get_type_handler`](_autosummary/qh.types.html.md#qh.types.get_type_handler)(python_type)                      | Get handler for a type from global registry.         |
| [`register_json_type`](_autosummary/qh.types.html.md#qh.types.register_json_type)([cls, to_json, from_json])      | Decorator to register a custom type.                 |
| [`register_type`](_autosummary/qh.types.html.md#qh.types.register_type)(python_type, \*, to_json, from_json) | Register a type in the global registry.              |

### Classes

| [`TypeHandler`](_autosummary/qh.types.html.md#qh.types.TypeHandler)(python_type, to_json, from_json)   | Handler for serializing/deserializing a specific type.   |
|-------------------------------------------------------------------------------------------------|----------------------------------------------------------|
| [`TypeRegistry`](_autosummary/qh.types.html.md#qh.types.TypeRegistry)()                                 | Registry for type handlers.                              |

### *class* qh.types.TypeHandler(python_type, to_json, from_json, http_location=HttpLocation.JSON_BODY, content_type=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Handler for serializing/deserializing a specific type.

#### python_type

The Python type this handler manages

#### to_json

Function to serialize Python object to JSON-compatible format

#### from_json

Function to deserialize JSON to Python object

#### http_location

Where in HTTP request/response this appears

#### content_type

Optional HTTP content type for binary data

#### to_transform_spec()

Convert this handler to a TransformSpec.

* **Return type:**
  [`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)

### *class* qh.types.TypeRegistry

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Registry for type handlers.

Manages conversion between Python types and HTTP representations.
Comes pre-populated with pass-through handlers for the JSON-native
builtins (`str`, `int`, `float`, `bool`, `list`, `dict`,
`NoneType`); its `register` method adds more. The module-level
`register_type` (and the `register_json_type` decorator built on it)
register into the separate, global registry used by the rest of `qh`,
not into a particular `TypeRegistry` instance.

```pycon
>>> reg = TypeRegistry()
>>> reg.get_handler(int).to_json(3)
3
>>> reg.get_handler(str) is not None
True
>>> class Unregistered: pass
>>> reg.get_handler(Unregistered) is None
True
```

#### get_handler(python_type)

Get handler for a type.

* **Parameters:**
  **python_type** ([`Type`](https://docs.python.org/3/library/typing.html#typing.Type)) – The type to look up
* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TypeHandler`](_autosummary/qh.types.html.md#qh.types.TypeHandler)]
* **Returns:**
  TypeHandler if registered, None otherwise

#### get_transform_spec(python_type)

Get TransformSpec for a type.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

#### register(python_type, , to_json, from_json, http_location=HttpLocation.JSON_BODY, content_type=None)

Register a type handler.

* **Parameters:**
  * **python_type** ([`Type`](https://docs.python.org/3/library/typing.html#typing.Type)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]) – The Python type
  * **to_json** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – Function to serialize to JSON-compatible format
  * **from_json** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]) – Function to deserialize from JSON
  * **http_location** ([`HttpLocation`](_autosummary/qh.rules.html.md#qh.rules.HttpLocation)) – Where this appears in HTTP
  * **content_type** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Optional content type for binary data
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### unregister(python_type)

Unregister a type handler.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### qh.types.get_transform_spec_for_type(python_type)

Get TransformSpec for a type from global registry.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TransformSpec`](_autosummary/qh.rules.html.md#qh.rules.TransformSpec)]

### qh.types.get_type_handler(python_type)

Get handler for a type from global registry.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TypeHandler`](_autosummary/qh.types.html.md#qh.types.TypeHandler)]

### qh.types.register_json_type(cls=None, , to_json=None, from_json=None)

Decorator to register a custom type.

Can be used as:

1. Class decorator (auto-detect to_dict/from_dict methods)
2. With explicit serializers

* **Parameters:**
  * **cls** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Type`](https://docs.python.org/3/library/typing.html#typing.Type)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]]) – The class being decorated, when used as `@register_json_type`
    with no arguments; `None` when called as
    `@register_json_type(to_json=..., from_json=...)`.
  * **to_json** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – Serializer; when omitted, falls back to `cls.to_dict()`,
    then `obj.__dict__`.
  * **from_json** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]]) – Deserializer; when omitted, falls back to
    `cls.from_dict`, then `cls(**data)`.

### Examples

```pycon
>>> @register_json_type
... class Point:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def to_dict(self):
...         return {'x': self.x, 'y': self.y}
...     @classmethod
...     def from_dict(cls, data):
...         return cls(data['x'], data['y'])
```

```pycon
>>> @register_json_type(
...     to_json=lambda p: [p.x, p.y],
...     from_json=lambda data: Point(data[0], data[1])
... )
... class Point:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
```

### qh.types.register_type(python_type, , to_json, from_json, http_location=HttpLocation.JSON_BODY, content_type=None)

Register a type in the global registry.

* **Parameters:**
  * **python_type** ([`Type`](https://docs.python.org/3/library/typing.html#typing.Type)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]) – The Python type
  * **to_json** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – Function to serialize to JSON-compatible format
  * **from_json** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]) – Function to deserialize from JSON
  * **http_location** ([`HttpLocation`](_autosummary/qh.rules.html.md#qh.rules.HttpLocation)) – Where this appears in HTTP
  * **content_type** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Optional content type for binary data
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### Example

```pycon
>>> import numpy as np
>>> register_type(
...     np.ndarray,
...     to_json=lambda arr: arr.tolist(),
...     from_json=lambda lst: np.array(lst)
... )
```


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-09-15 12:16 UTC** from commit <a href="https://github.com/i2mint/qh/commit/789a9cae460b66140a0b74a80fdb23427d4f8b24"><code>789a9ca</code></a> on branch <code>master</code>, for **qh 0.0.17** (from <code>pyproject.toml</code>).

#### WARNING
The documentation and the package may be misaligned:

- The documented version (0.0.17) is behind the latest release on PyPI (0.0.18): `pip install qh` gives newer code than these docs describe.

## Source

|                     |                                                                                                                                                  |
|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/i2mint/qh/commit/789a9cae460b66140a0b74a80fdb23427d4f8b24"><code>789a9cae460b66140a0b74a80fdb23427d4f8b24</code></a> |
| Branch              | <code>master</code>                                                                                                                              |
| Tags at this commit | none                                                                                                                                             |
| Working tree        | clean                                                                                                                                            |
| Remote              | <code>https://github.com/i2mint/qh</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>i2mint/qh</code>                                                                     |
| Run          | <a href="https://github.com/i2mint/qh/actions/runs/34967663191">34967663191</a>            |
| Ref          | <code>refs/heads/master</code>                                                             |
| Event commit | <code>789a9cae460b66140a0b74a80fdb23427d4f8b24</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.11  |
| Sphinx   | 9.1.0   |
| docutils | 0.22.4  |
| Python   | 3.12.14 |

## Configuration as resolved

|               |                                                                   |
|---------------|-------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>sphinxawesome_theme</code>) |
| accent        | <code>#3b6600</code>                                              |
| api_generator | <code>autosummary</code>                                          |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code>  |
| agent_outputs | <code>true</code>                                                 |
| aggregates    | <code>md</code>                                                   |
| ai_artifacts  | <code>true</code>                                                 |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/qh/0.0.18/">0.0.18</a>, newer than the documented version (0.0.17).

## Reproduce

```bash
git clone https://github.com/i2mint/qh && cd qh
git checkout 789a9cae460b66140a0b74a80fdb23427d4f8b24
pip install "epythet==0.2.11"
epythet quickstart . --ignore tests/ scrap/ examples/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# api.html.md

# API reference

| [`qh`](_autosummary/qh.html.md#module-qh)   | Quick HTTP: expose Python functions as a FastAPI web service with one call.   |
|-----------------------------------------------------------------|-------------------------------------------------------------------------------|


