Skip to main content

REST Canvas

OpenAPI-driven decorator library for REST API endpoints in Django and Flask.

Overview

REST Canvas provides a decorator-based approach to building REST APIs that are automatically validated against OpenAPI specifications. It handles request validation, response formatting, pagination, sorting, filtering, and error handling based on your OpenAPI YAML files.

Features

  • OpenAPI-driven: All validation and behavior driven by OpenAPI specifications
  • Framework-agnostic: Works with Django, Flask, and standalone mode
  • Automatic validation: Path and query parameters validated against the spec
  • Request-centric design: All features accessible via unified Request object
  • Feature declaration: Declare endpoint capabilities with simple markers (DetailLevel, Sort, Filter)
  • Pagination support: Both cursor-based and page-based pagination
  • RCQL filtering: REST Canvas Query Language for powerful filtering (e.g., status==active&age>18)
  • Sort support: Multi-field sorting with ascending/descending (e.g., name,-createdAt)
  • Detail level selection: Control response verbosity with field groups (e.g., minimal, full)
  • Type safety: Generic type hints for pagination: Request[PagePagination]
  • Testing utilities: Built-in MockRequest for testing view functions in isolation

Installation

# Install from PyPI
pip install rest-canvas

# Or install in editable mode for development
pip install -e .

# With development dependencies
pip install -e ".[dev]"

# With Django
pip install -e ".[django]"

# With Flask
pip install -e ".[flask]"

Configuration

REST Canvas is configured via the Config class, which allows you to customize behavior:

from rest_canvas import Config, LoggerConfig, LogLevel, LogToConsole, RestCanvas

# Basic usage - starts with Standalone adapter
api = RestCanvas('openapi.yaml')

# Custom configuration
config = Config(
    logger_config=LoggerConfig(
        min_log_level=LogLevel.INFO,    # Set logging level
        enabled=True,                    # Enable/disable logging
        processors=[LogToConsole()]      # Custom log processors
    )
)
api = RestCanvas('openapi.yaml', config=config)

Configuration Options

  • logger_config: LoggerConfig instance for customizing logging behavior
  • debug: Reserved flag (default: False). Accepted by Config but not yet read anywhere in the library, so setting it currently has no effect

Framework Adapter Selection

REST Canvas always starts with the Standalone adapter. The adapter automatically switches when you register routes:

# Flask - adapter switches when you call register_flask_routes()
api = RestCanvas('openapi.yaml')
api.register_flask_routes(app)  # Now using FlaskAdapter

# Django - adapter switches when you call register_django_urls()
api = RestCanvas('openapi.yaml')
urlpatterns = api.register_django_urls()  # Now using DjangoAdapter

# Standalone - no registration needed (default)
api = RestCanvas('openapi.yaml')
# Call endpoints directly - stays with StandaloneAdapter

Note: You cannot switch adapters once routes are registered. Calling register_flask_routes() or register_django_urls() more than once will raise a RuntimeError.

Quick Start

Flask Example

from flask import Flask
from rest_canvas import RestCanvas, Request, PagePagination, Sort, Filter

app = Flask(__name__)

# Initialize REST API with OpenAPI spec
api = RestCanvas('openapi.yaml', mount_point='/api/v1')


# Declare features in decorator, access via request object
@api.endpoint('GET /users', Sort, Filter)
def list_users(request: Request[PagePagination]):
    """List users with pagination, sorting, and filtering."""
    users = User.query.all()

    # Filter with RCQL: ?filter=status==active&age>18
    if request.filter:
        users = apply_filter(users, request.filter)

    # Sort: ?sort=name,-createdAt (ascending name, descending createdAt)
    if request.sort:
        users = apply_sort(users, request.sort)

    # Pagination automatically populated from query params
    offset = request.pagination.offset
    users_page = users[offset:offset + request.pagination.limit]
    request.pagination.total = len(users)

    return [user.to_dict() for user in users_page]


# Auto-register all routes with Flask
api.register_flask_routes(app)

Django Example

from rest_canvas import RestCanvas, Request, CursorPagination, DetailLevel

# Initialize REST API
api = RestCanvas('openapi.yaml', mount_point='/api/v1')


@api.endpoint('GET /users/{userId}', DetailLevel)
def get_user_by_id(request: Request, user_id: int):
    """Get user by ID with detail level selection."""
    user = User.objects.get(id=user_id)

    # Return different fields based on request.detail_level
    # Query: ?detailLevel=minimal
    if request.detail_level == "minimal":
        return {"id": user.id, "name": user.name}
    else:
        return user.to_dict()


# In urls.py
urlpatterns = api.register_django_urls()

Key Features

  • Request-centric design: All features (pagination, sort, filter, detail_level) accessible via request object
  • Feature declaration: Declare features in decorator: @api.endpoint('GET /path', Sort, Filter, DetailLevel)
  • Type-safe: Generic type hints for pagination: Request[PagePagination]
  • Automatic parsing: Sort, filter, pagination, and detail level parameters parsed automatically
  • OpenAPI validation: Function signatures and features validated against OpenAPI spec at startup
  • Spec-driven status codes: Success status read from the operation's responses: block
  • RCQL filtering: Powerful query language with operators: ==, !=, <, <=, >, >=, =like=, ={val1,val2,...}
  • Multi-field sorting: Sort by multiple fields with direction: ?sort=name,-createdAt
  • Framework adapters: Seamless adaptation for Flask, Django, and standalone modes

Development Status

Current Version: 0.11.0 (Alpha)

This library is in active development. Recent refactoring has established a modular, request-centric architecture.

Completed Features

  • ✅ OpenAPI-driven endpoint validation
  • ✅ Request-centric design with unified Request object
  • ✅ Framework adapters (Django, Flask, Standalone)
  • ✅ Feature declaration system (Sort, Filter, DetailLevel)
  • ✅ Pagination (Page-based and Cursor-based)
  • ✅ RCQL filtering with comparison and logical operators
  • ✅ Multi-field sorting with ascending/descending
  • ✅ Detail level (field group) selection
  • ✅ Automatic type coercion for query parameters
  • ✅ CamelCase to snake_case conversion
  • ✅ Testing utilities (MockRequest)
  • ✅ Spec-declared success status codes
  • ✅ Extensible exception-to-status mapping via HttpError

In Progress

  • 🔄 Additional ORM appliers for filtering and sorting
  • 🔄 Enhanced error messages and validation
  • 🔄 Documentation improvements

Requirements

  • Python >= 3.14
  • PyYAML >= 6.0
  • jsonschema >= 4.0
  • prance >= 25.4
  • openapi-spec-validator >= 0.7

Development

# Install development dependencies
pip install -e ".[dev]"

# Run tests (coverage is enabled by default)
pytest

# Format code
ruff format .

# Lint code
ruff check src tests

Testing Your Views

The library provides MockRequest for testing your view functions in isolation:

from rest_canvas.testing import MockRequest

def test_list_users():
    # Create mock request with pagination, sort, and filter
    request = (MockRequest()
        .with_page_pagination(page=1, size=10)
        .with_sort('name,-createdAt')
        .with_filter('status==active'))

    # Call view function directly
    result = list_users(request)

    # Assert results
    assert len(result) <= 10
    assert request.pagination.total > 0

def test_get_user_by_id():
    # Create mock request for single resource with detail level
    request = MockRequest().with_detail_level('minimal')

    # Call view with path parameter
    result = get_user_by_id(request, user_id=42)

    # Assert minimal fields returned
    assert 'id' in result
    assert 'name' in result
    assert 'email' not in result  # Not included in minimal

Project Structure

rest_canvas/
├── public/                # Public API surface
│   ├── rest_canvas.py     # RestCanvas class (main entry point)
│   ├── features.py        # Feature markers (DetailLevel, Sort, Filter)
│   └── exceptions.py      # API exceptions
├── core/                  # Core implementation
│   ├── config/            # Config dataclass
│   ├── endpoint/          # Endpoint decorator and validation
│   ├── envelope/          # Response envelope formatting
│   ├── filter/            # RCQL parsing and ORM appliers
│   ├── log/               # Logger and log processors
│   ├── open_api/          # OpenAPI spec loading and parsing
│   ├── pagination/        # Page and cursor pagination
│   ├── request/           # Request wrapper and validation
│   ├── response/          # Response object and validation
│   ├── sort/              # Sort parsing and ORM appliers
│   ├── view/              # View function introspection
│   └── serializer.py      # Datetime serialization
├── adapters/              # Framework adapters
│   ├── django_adapter.py
│   ├── flask_adapter.py
│   └── standalone_adapter.py
├── utils/                 # Utilities
└── testing/               # Testing utilities (MockRequest)

tests/
├── unit/                  # Unit tests
├── integration/           # Integration tests
└── fixtures/              # Test fixtures and OpenAPI specs

Key Concepts

RCQL (REST Canvas Query Language)

RCQL provides a powerful filtering syntax using URL-friendly operators:

# Comparison operators
?filter=status==active           # Equal
?filter=age>18                   # Greater than
?filter=age<=65                  # Less than or equal
?filter=name=like=%john%         # Pattern matching
?filter=color={red,blue}         # Set membership (IN)

# Logical operators (left-to-right evaluation)
?filter=status==active&age>18    # AND
?filter=role==admin|role==mod    # OR

# Complex expressions
?filter=status==active&age>18|premium==true
?filter=category={1,2,3}&status==active

Sort Syntax

Multi-field sorting with direction control:

?sort=name              # Ascending by name
?sort=-createdAt        # Descending by createdAt
?sort=name,-createdAt   # Multiple fields

Detail Levels

Control response verbosity with field groups defined in your OpenAPI spec:

?detailLevel=minimal    # Only essential fields
?detailLevel=full       # All fields including relations

The detailLevel enum in the OpenAPI spec must list levels from smallest to largest payload. When the parameter is not specified, the smallest (first-listed) level is returned by default.

Response Envelope

All responses share the {data, meta, message} envelope:

{
  "data": { "id": 123, "name": "John Doe" },
  "meta": {
    "timestamp": "2024-12-16T10:30:00Z",
    "requestId": "abc-123"
  },
  "message": ""
}
  • data — the resource payload (empty object {} for error responses)
  • meta — operational/technical metadata (timestamps, request tracking, pagination)
  • message — user-facing communication at the envelope root; always present, empty string when there is nothing important to communicate. Error responses carry the error description here.

Success Status Codes

The success status code comes from the responses: block of the OpenAPI operation. Views return data only — never a (data, status) tuple:

paths:
  /events:
    post:
      operationId: ingestEvent
      responses:
        '202':
          description: Event accepted for processing
@api.endpoint('POST /events')
def ingest_event(request: Request):
    return {'queued': True}  # responds 202, no view-side code needed

Rules:

  • The smallest concrete 2xx code wins. An operation declaring both 200 and 206 returns 200, so ambiguity resolves deterministically rather than by mapping order.
  • Every operation must declare a concrete 2xx code. The default key and the 2XX wildcard do not count; a spec without one raises EndpointValidationError when the endpoint is declared.
  • 204 No Content is rejected. Every response carries a {data, meta, message} body, so there is no coherent way to honour a 204.
  • Returning a tuple raises ResponseValidationError, which responds 500. The status belongs in the contract, not in the view.

Exceptions

Raise these from a view to produce an error response. Each carries the status code it maps to:

Exception Status
ValidationError 400 Bad Request
UnauthorizedError 401 Unauthorized
ForbiddenError 403 Forbidden
NotFoundError 404 Not Found
ConflictError 409 Conflict
TooManyRequestsError 429 Too Many Requests
InternalServerError 500 Internal Server Error

Any other exception becomes a 500. All of them produce the standard envelope with the exception message in message and {} in data.

The framework raises ResponseValidationError itself when a view returns something the endpoint cannot serialize — None, a tuple, or a list on a non-paginated endpoint. It maps to 500: the request was well formed, the view is at fault.

HttpError is the base for every status-carrying exception above. Subclass it to define your own — the pipeline reads status_code directly, so nothing else needs to change:

from rest_canvas import HttpError

class PaymentRequiredError(HttpError):
    status_code = 402

Request Headers

request.headers is a plain dict keyed exactly as the framework supplied the headers. Use request.header() for lookups that do not depend on casing:

token = request.header('X-App-Token')          # same result as 'x-app-token'
tenant = request.header('X-Tenant', 'default')  # default when absent

Note: security and securitySchemes in the spec are documentation only — they are not enforced. Authenticate in the view and raise UnauthorizedError.

Request Body

request.json gives you the parsed body. REST Canvas does not validate it.

The requestBody schema in the spec is documentation only: a body that contradicts it still reaches your view. Validate it and raise ValidationError to produce a 400.

Architecture

REST Canvas uses a request-centric design where all query features flow through a unified Request object:

  1. Decorator declaration: Features declared in @api.endpoint() decorator
  2. Automatic parsing: Query parameters parsed and validated at request time
  3. Request population: Parsed values populated in request.sort, request.filter, etc.
  4. Type safety: Generic type hints for pagination: Request[PagePagination]
  5. Framework agnostic: Same code works with Django, Flask, or standalone

This design ensures your view functions remain clean, testable, and framework-independent.

License

MIT License

Contributing

This is currently an internal project. Contribution guidelines will be added once the library reaches beta status.

Support

For issues and questions, please refer to the issue tracker (to be set up).

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rest_canvas-0.11.0.tar.gz (55.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rest_canvas-0.11.0-py3-none-any.whl (66.0 kB view details)

Uploaded Python 3

File details

Details for the file rest_canvas-0.11.0.tar.gz.

File metadata

  • Download URL: rest_canvas-0.11.0.tar.gz
  • Upload date:
  • Size: 55.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.14.7

File hashes

Hashes for rest_canvas-0.11.0.tar.gz
Algorithm Hash digest
SHA256 3ae3473a1f09db2fc1d969237894dea1230cce635f9917f2a41568decda7762d
MD5 a4a9c89695855277b02a5d0115aed330
BLAKE2b-256 0b4e60465632b3913e0d7739a1c36c1f3754c735d4f13454efaac6ba5972c997

See more details on using hashes here.

File details

Details for the file rest_canvas-0.11.0-py3-none-any.whl.

File metadata

  • Download URL: rest_canvas-0.11.0-py3-none-any.whl
  • Upload date:
  • Size: 66.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.14.7

File hashes

Hashes for rest_canvas-0.11.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1fc9f7b2b5e665bd9bd2f91a16446f3e38b64f9869d60297a140b3e8cc772bee
MD5 c83c39883b13df134550ca9dbb18d4b2
BLAKE2b-256 cfd8f43edb4dd99c9cec7bf7451c94116f5511c7eb9351232793450bc6541357

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.11.0 This release

2 files

0.10.0

2 files

0.9.0

2 files

0.8.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page