Metadata-Version: 2.1
Name: HttpMessageProcessor
Version: 2.5.0
Author-email: Eduardo Almeida <eduardo.almeida@e-deploy.com.br>
Maintainer-email: Eduardo Almeida <eduardo.almeida@e-deploy.com.br>, Max Guenes <max.santos@e-deploy.com.br>
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: MessageProcessor~=4.1
Requires-Dist: SimpleHttpRouter~=1.0
Requires-Dist: iso8601==2.1.0; python_version >= "3.8" and python_version < "4.0"
Requires-Dist: nh3<0.4.0,>=0.3.3

# HttpMessageProcessor

HTTP routing and request-parsing library built on top of the `MessageProcessor` and `SimpleHttpRouter` packages. It provides two packages: a core routing layer (`httpmessageprocessor`) and a higher-level automation layer (`httpmessageprocessorutil`).

## Installation

```bash
pip install HttpMessageProcessor --extra-index-url https://pip.e-deploy.com.br
```

## Packages

### `httpmessageprocessor` — Core routing

Routes `HttpRequestMessage` events to per-route `MessageProcessor` instances.

```python
from httpmessageprocessor import (
    HttpMessageProcessor,
    HttpMessageProcessorMessageHandler,
    ApiRequest,
    RouterProcessorBuilder,
)
```

**`HttpMessageProcessor`** accepts either a `Dict[Route, MessageProcessor]` mapping or a custom `RouterProcessorBuilder`.

**`HttpMessageProcessorMessageHandler`** is a convenience wrapper that binds `HttpMessageProcessor` to `DefaultToken.TK_HTTP_MSG` on the message bus.

### `httpmessageprocessorutil` — Automation layer

#### `AutoMappedMessageProcessor`

Automates the full HTTP request lifecycle: parse → business call → serialize response.

```python
from httpmessageprocessorutil.automapped import AutoMappedMessageProcessor

processor = AutoMappedMessageProcessor(
    request_type=MyRequestDto,   # DTO class (or None for no body)
    interactor=my_interactor,    # must have execute(dto) or execute()
    exception_map={              # optional: maps exception types to HTTP status codes
        NotFoundException: 404,
        ConflictException: (409, b"conflict"),
        MyException: lambda e: HttpResponseMessage(422, ...),
    },
    success_status=200,          # optional, default 200
    extra_mapping=None,          # optional, see below
    response_content_type=None,  # optional, forces Content-Type header
    sanitizer=...,               # optional, see Sanitization section
)
```

The interactor must expose exactly one `execute` method accepting zero or one parameter. When `request_type` is `None`, `execute()` is called with no arguments.

#### DTO parsing (`ApiRequestParser`)

The parser deserializes `ApiRequest` into a typed DTO using Python type annotations. It searches **path parameters → query parameters → JSON body** in that priority order and automatically resolves field names across `snake_case`, `camelCase`, `PascalCase`, and `kebab-case` variants.

**Supported field types:** `int`, `float`, `bool`, `str`, `Decimal`, `date`, `datetime`, `UUID`, `Enum`, `List[T]`, `Dict[K, V]`, `Optional[T]`, `Union[A, B]`, nested DTOs, `ForwardRef`.

**GET requests:** the body is never parsed; all values come from path/query parameters.

#### `extra_mapping`

Pass as the `extra_mapping` keyword argument to `AutoMappedMessageProcessor` or `ApiRequestParser`:

```python
extra_mapping = {
    # Remove attributes absent from the payload (useful for PATCH)
    "removeNotSent": True,

    # Per-field overrides
    "user_id": {
        "location": "path",   # "path" | "query" | "header" | "body"
        "name": "userId",     # key name to look up
        "type": UUID,         # override parsed type
    },
}
```

#### `@auto_map` decorator

Override per-field parsing metadata directly on the DTO class:

```python
from httpmessageprocessorutil.automapped import auto_map

@auto_map({
    "host": {"location": "header", "name": "X-Host", "type": str},
})
class MyRequestDto:
    def __init__(self, host: str, name: str):
        self.host = host
        self.name = name
```

#### `@exec_map` decorator

Attach HTTP metadata to custom exception classes:

```python
from httpmessageprocessorutil.automapped import exec_map

@exec_map({"status_code": 422, "body": {"error": "unprocessable"}})
class MyException(Exception):
    pass
```

#### Exception formatting

`PropertyError` subclasses are always returned as **400** with a structured body (unless explicitly mapped):

```json
{"property": "field_name", "reason": "cannot be null"}
```

For list errors, an `innerError` key is included with the nested cause.

The `exception_map` values can be:
- `int` — status code; body is auto-serialised from the exception if it has a custom `__init__`
- `(int, body)` — explicit status code and body (`bytes` or `dict`)
- `callable` — receives the exception, returns `HttpResponseMessage` or a JSON-serialisable object

#### Global exception map

Use `GlobalExceptionMap` to register application-wide exception mappings once, instead of repeating them in every processor's `exception_map`:

```python
from httpmessageprocessorutil.automapped import GlobalExceptionMap

# At application startup
GlobalExceptionMap.set({
    ForbiddenException: 403,
    UnauthorizedException: 401,
    NotFoundException: 404,
})
```

`ExceptionFormatter` checks the per-instance `exception_map` first, then falls back to the global map. Per-instance entries always take priority. Changes to the global map take effect immediately on all processors.

### Sanitization

By default, all `str` values parsed from requests **and** responses are sanitized using `nh3` (strips all HTML tags). This prevents XSS from reaching business logic or clients.

Plain-text characters and entities round-trip unchanged: bare `<`, `>`, `&`, sequences like `->` or `AT&T`, and pre-existing HTML entities (`&gt;`, `&amp;`, `&#60;`, `&#x3C;`) are preserved verbatim — only actual tags and attributes are stripped.

```python
from httpmessageprocessorutil.sanitizer import Sanitizer, Nh3Sanitizer, SanitizeMode

# Disable sanitization entirely
processor = AutoMappedMessageProcessor(..., sanitizer=None)

# Sanitize only requests (preserve HTML in responses)
processor = AutoMappedMessageProcessor(..., sanitize_mode=SanitizeMode.REQUEST_ONLY)

# Sanitize only responses
processor = AutoMappedMessageProcessor(..., sanitize_mode=SanitizeMode.RESPONSE_ONLY)

# Disable via mode (equivalent to sanitizer=None)
processor = AutoMappedMessageProcessor(..., sanitize_mode=SanitizeMode.NONE)

# Custom sanitizer
class MySanitizer(Sanitizer):
    def _types_to_sanitize(self):
        return [str]

    def _sanitize(self, value):
        return my_custom_clean(value)

processor = AutoMappedMessageProcessor(..., sanitizer=MySanitizer())

# Custom sanitizer with different logic per direction
class DirectionalSanitizer(Sanitizer):
    def _types_to_sanitize(self):
        return [str]

    def _sanitize(self, value):
        return value

    def sanitize_request(self, value):
        # strict cleaning for incoming data
        return strict_clean(value)

    def sanitize_response(self, value):
        # allow some HTML in outgoing data
        return allow_safe_html(value)
```

The `Sanitizer` base class provides `sanitize_request(value)` and `sanitize_response(value)` methods that default to calling `sanitize(value)`. Override them independently to apply different sanitization logic for requests vs responses.

#### Changing the default sanitizer

Use `set_default_sanitizer()` to change the sanitizer used by all future instances:

```python
# Use a custom sanitizer by default
AutoMappedMessageProcessor.set_default_sanitizer(MySanitizer())

# Disable sanitization by default
AutoMappedMessageProcessor.set_default_sanitizer(None)
```

Passing an explicit `sanitizer=` to the constructor always overrides the class default. `sanitizer=None` disables sanitization regardless of `sanitize_mode`.

## Data flow

```
MessageBus (HttpRequestMessage)
  → HttpMessageProcessorMessageHandler
    → HttpMessageProcessor          # route dispatch
      → AutoMappedMessageProcessor.parse_data()
          → ApiRequestParser.parse()   # builds typed DTO; sanitizes strings
      → AutoMappedMessageProcessor.call_business()
          → interactor.execute(dto)
      → AutoMappedMessageProcessor.format_response() / format_exception()
  → MessageBus (HttpResponseMessage)
```

## Response serialization

- Default: JSON (`application/json`), status 200 (or `success_status`).
- `Decimal` values are serialised as `float` rounded to 8 decimal places.
- `Enum` values are serialised by their `.value`.
- Pass `response_content_type` to force a custom `Content-Type` and skip JSON serialisation.

## Requirements

- Python ≥ 3.8
- `MessageProcessor ~= 4.1`
- `SimpleHttpRouter ~= 1.0`
- `iso8601 == 2.1.0`
- `nh3 >= 0.3.3, < 0.4.0`
