# Handlers, Schemas & Error Handling Reference

## `<svc>/handlers.py` — Route Handlers

```python
from flask import jsonify, request

from oto import response
from oto.adaptors.flask import flaskify
from owsrequest import context

from <svc> import config
from <svc>.api import app
from <svc>.logic import <domain>

# ── Health check ─────────────────────────────────────────────────────────────
@app.route(config.HEALTH_CHECK_PATH, methods=["GET"])
def health():
    return jsonify({"status": "ok"})


# ── Global error handler ──────────────────────────────────────────────────────
@app.errorhandler(500)
def exception_handler(error):
    message = (
        "The server encountered an internal error "
        "and was unable to complete your request."
    )
    app.log_exception(error)
    return flaskify(response.create_fatal_response(message))


# ── Example: simple GET handler ───────────────────────────────────────────────
@app.route(config.MY_RESOURCE_PATH)
def get_my_resource(resource_id: str):
    permissions = _get_permissions()
    data = <domain>.get_resource(resource_id, permissions)
    return flaskify(response.Response(data))


# ── Example: GET with marshmallow-validated query params ──────────────────────
from flask_apispec import use_kwargs, marshal_with
from <svc>.schemas.<domain> import MyResourceSchema

@app.route(config.MY_RESOURCE_LIST_PATH)
@use_kwargs(MyResourceSchema.Request, location="query")
@marshal_with(MyResourceSchema.Response(many=True), apply=True)
def get_my_resources(**data):
    permissions = _get_permissions()
    return <domain>.get_resources(permissions=permissions, **data)


# ── Example: POST handler ─────────────────────────────────────────────────────
@app.route(config.MY_RESOURCE_CREATE_PATH, methods=["POST"])
@use_kwargs(MyResourceSchema.CreateRequest, location="json")
def create_my_resource(**data):
    permissions = _get_permissions()
    result = <domain>.create_resource(permissions=permissions, **data)
    return flaskify(response.Response(result))


# ── Helper ────────────────────────────────────────────────────────────────────
def _get_permissions():
    request_context = context.get_request_context_from_headers(request.headers)
    return <domain>.get_permissions(request_context)
```

**Handler rules:**
- Always thin: extract params → call logic → return. No SQL, no direct DB calls.
- Use `flaskify(response.Response(data))` for success — never `jsonify()` except in the health check.
- Use `flaskify(response.create_fatal_response(msg))` for errors (handled by the global 500 handler automatically for unhandled exceptions).
- Use `flaskify(response.create_error_response(code=..., message=..., status=...))` for expected business errors (e.g. 403 Forbidden, 404 Not Found).
- The 500 global handler uses `app.log_exception(error)` — not `app.logger.exception()` — so Sentry and owslogger both receive it.
- Health check path is excluded from owslogger access logs via `exclude_paths` in `api.py`.
- Use `@app.route(config.CONSTANT)` — never inline path strings.

## `<svc>/schemas/base.py` — Base Schema

```python
from marshmallow import Schema, EXCLUDE
from oto import response
from oto.adaptors.flask import flaskify

from <svc>.api import app


class BaseSchema(Schema):
    """Base for all marshmallow schemas. Silently drops unknown fields."""

    class Meta:
        unknown = EXCLUDE

    @classmethod
    def normalize(cls, obj, many=False):
        return cls().dump(obj, many=many)

    @classmethod
    def normalized_response(cls, obj, many=False):
        return response.Response(cls.normalize(obj, many=many))


class RequestSchema(BaseSchema):
    """Base for request validation schemas.

    Maps marshmallow 422 validation errors → 500 (Orchard standard — never expose
    validation details to callers).
    """

    @app.errorhandler(422)
    def handle_validation_error(self, *args, **kwargs):
        return flaskify(response.create_fatal_response(str(self.exc)))
```

## `<svc>/schemas/<domain>.py` — Domain Schema

**Convention:** one file per endpoint group, with inner `Request` and `Response` classes.

```python
from marshmallow import Schema, fields, validate, post_dump

from <svc>.schemas.base import BaseSchema, RequestSchema


class MyResourceSchema:
    """Schemas for the /my-resource endpoints."""

    class Request(RequestSchema):
        resource_id = fields.UUID(required=True)
        date        = fields.Date(required=True)
        limit       = fields.Integer(load_default=100, validate=validate.Range(min=1, max=1000))
        offset      = fields.Integer(load_default=0)
        filter      = fields.String(required=False)

    class Response(BaseSchema):
        id          = fields.UUID(data_key="resourceId")
        name        = fields.String()
        created_at  = fields.DateTime(data_key="createdAt")
        is_active   = fields.Boolean(data_key="isActive", allow_none=True)

        @post_dump(pass_many=True)
        def wrap_envelope(self, data, many, **kwargs):
            if many:
                return {"data": data, "total": len(data)}
            return data

    class CreateRequest(RequestSchema):
        name = fields.String(required=True, validate=validate.Length(min=1, max=255))
```

**Marshmallow rules:**
- Use `data_key` to map snake_case internal names to camelCase API output.
- `load_default` (not `missing`) for optional fields with defaults (marshmallow 3.x).
- `allow_none=True` for nullable fields.
- `validate.OneOf(...)`, `validate.Range(...)`, `validate.Regexp(...)` for input constraints.
- `fields.Method("method_name")` for computed output fields.
- `Schema.Meta.unknown = EXCLUDE` is inherited from `BaseSchema` — never use `RAISE` for unknown fields.
- Use `fields.List(fields.Nested(...))` for arrays of objects.
- **Never use Pydantic in new code** — marshmallow is the team standard.

## Error Response Shape

All error responses use the `oto` library format:

```python
# 500 — unhandled exception (automatic via global errorhandler)
response.create_fatal_response("The server encountered an internal error...")
# → {"code": "internal_error", "message": "..."}

# 403 — access denied
response.create_error_response(
    code="authorization_error",
    message="You do not have permission to access this resource.",
    status=403,
)
# → {"code": "authorization_error", "message": "..."}, HTTP 403

# 404 — not found
response.create_error_response(code="not_found", message="Resource not found.", status=404)
```

Pre-build common error constants in `<svc>/constants/error.py`:

```python
from oto import response
from oto.adaptors.flask import flaskify
from oto.utils import status

RESPONSE_FORBIDDEN = flaskify(response.create_error_response(
    code="authorization_error",
    message="You do not have permission to access this resource.",
    status=status.FORBIDDEN,
))

RESPONSE_NOT_FOUND = flaskify(response.create_error_response(
    code="not_found",
    message="Resource not found.",
    status=status.NOT_FOUND,
))
```

## Authentication & Request Context

Insights services do **not** use API keys. Auth is header-based from an upstream gateway:

```python
from owsrequest import context

# In any handler that needs identity:
request_context = context.get_request_context_from_headers(request.headers)
```

**Headers provided by the gateway:**
- `Orchard-Profile-Id` — numeric profile ID (e.g. `422069`)
- `Orchard-Profile-Type` — e.g. `InsightsProfile`, `ArtistProfile`
- `Grass-Account-Type` — label/account type (older services)
- `Grass-Account-Id` — label/account ID (older services)
- `Orchard-User-Id` — user ID

**Access decorator pattern** (if service requires permission checks):

```python
# <svc>/validation/access.py
from functools import wraps
from flask import request
from owsrequest import context

from <svc>.constants.error import RESPONSE_FORBIDDEN
from <svc>.permissions import analytics as permissions_module

def verify_profile(access):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            request_context = context.get_request_context_from_headers(request.headers)
            if not permissions_module.has_access(request_context, access):
                return RESPONSE_FORBIDDEN
            return f(*args, **kwargs)
        return wrapper
    return decorator
```

Usage in handlers:
```python
from <svc>.validation.access import verify_profile

ACCESS_ANALYTICS = "analytics"

@app.route(config.MY_PATH)
@verify_profile(access=ACCESS_ANALYTICS)
def my_handler():
    ...
```
