Skip to content

Latest commit

 

History

History
506 lines (376 loc) · 10.3 KB

File metadata and controls

506 lines (376 loc) · 10.3 KB

FrappeAPI

Better APIs for Frappe.

FrappeAPI brings FastAPI-style routing and validation to the Frappe Framework. Define endpoints with type hints, get automatic validation and documentation.

Documentation

Documentation

Installation

pip install frappeapi

Quick Start

from frappeapi import FrappeAPI

app = FrappeAPI()

@app.get()
def hello(name: str = "World"):
    return {"message": f"Hello, {name}!"}

Examples

Path Parameters

Enable FastAPI-style paths for cleaner URLs:

from frappeapi import FrappeAPI

app = FrappeAPI(fastapi_path_format=True)

@app.get("/items/{item_id}")
def get_item(item_id: str):
    return {"id": item_id}

# GET /api/items/abc123
# Response: {"id": "abc123"}

Multiple path parameters:

@app.get("/users/{user_id}/orders/{order_id}")
def get_user_order(user_id: str, order_id: int):
    return {"user_id": user_id, "order_id": order_id}

# GET /api/users/john/orders/42
# Response: {"user_id": "john", "order_id": 42}

Combine path and query parameters:

@app.get("/products/{category}")
def list_products(
    category: str,           # Path parameter
    sort_by: str = "name",   # Query parameter
    limit: int = 10          # Query parameter
):
    return {"category": category, "sort_by": sort_by, "limit": limit}

# GET /api/products/electronics?sort_by=price&limit=20

Query Parameters

Automatic type parsing:

@app.get()
def get_product_details(
    product_id: int,
    unit_price: float,
    in_stock: bool
):
    return {
        "product_id": product_id,  # "123" -> 123
        "unit_price": unit_price,  # "9.99" -> 9.99
        "in_stock": in_stock       # "true" -> True
    }

Optional parameters with defaults:

@app.get()
def list_products(
    category: str = "all",
    page: int = 1,
    search: str | None = None
):
    return {"category": category, "page": page, "search": search}

Enum parameters:

from enum import Enum

class OrderStatus(str, Enum):
    pending = "pending"
    processing = "processing"
    completed = "completed"

@app.get()
def list_orders(status: OrderStatus = OrderStatus.pending):
    return {"status": status}

List parameters:

from frappeapi import Query

@app.get()
def search_products(
    tags: List[str] = Query(default=[]),
    categories: List[int] = Query(default=[])
):
    return {"tags": tags, "categories": categories}

# GET ?tags=electronics&tags=sale&categories=1&categories=2
# Response: {"tags": ["electronics", "sale"], "categories": [1, 2]}

Aliased parameters:

from typing import Annotated
from frappeapi import Query

@app.get()
def search_items(
    search_text: Annotated[str, Query(alias="q")] = "",
    page_number: Annotated[int, Query(alias="p")] = 1
):
    return {"search": search_text, "page": page_number}

# GET ?q=laptop&p=2

Query parameter models:

from pydantic import BaseModel, Field

class ProductFilter(BaseModel):
    search: str | None = None
    category: str = "all"
    min_price: float = Field(0, ge=0)
    in_stock: bool = True

@app.get()
def filter_products(filters: Annotated[ProductFilter, Query()]):
    return filters

Request Body

Single model:

from pydantic import BaseModel, Field

class Item(BaseModel):
    name: str = Field(..., min_length=1, max_length=50)
    description: str | None = None
    price: float = Field(..., gt=0)

@app.post()
def create_item(item: Item):
    return item

Multiple body parameters:

class User(BaseModel):
    username: str
    email: str

class Item(BaseModel):
    name: str
    price: float

@app.post()
def create_user_item(user: User, item: Item):
    return {"user": user, "item": item}

# Request body:
# {
#     "user": {"username": "john", "email": "john@example.com"},
#     "item": {"name": "Laptop", "price": 999.99}
# }

Nested models:

from pydantic import HttpUrl

class Image(BaseModel):
    url: HttpUrl
    name: str

class Product(BaseModel):
    name: str
    price: float
    images: List[Image]

@app.post()
def create_product(product: Product):
    return product

Form Data

from typing import Annotated
from frappeapi import Form

@app.post()
def create_user_profile(
    username: Annotated[str, Form()],
    email: Annotated[str, Form()],
    bio: Annotated[str | None, Form()] = None
):
    return {"username": username, "email": email, "bio": bio}

File Uploads

Small files (in-memory):

from typing import Annotated
from frappeapi import File, Form

@app.post()
def upload_document(
    file: Annotated[bytes, File()],
    description: Annotated[str | None, Form()] = None
):
    return {"file_size": len(file), "description": description}

Large files (streamed):

from frappeapi import UploadFile

@app.post()
def upload_large_file(file: UploadFile):
    return {
        "filename": file.filename,
        "content_type": file.content_type
    }

Response Models

Filter response data:

class UserResponse(BaseModel):
    id: int
    username: str
    email: str

@app.get(response_model=UserResponse)
def get_user(user_id: int):
    return {
        "id": user_id,
        "username": "john_doe",
        "email": "john@example.com",
        "password": "secret"  # Filtered out
    }

List responses:

class Product(BaseModel):
    id: int
    name: str
    price: float

@app.get(response_model=List[Product])
def list_products():
    return [
        {"id": 1, "name": "Laptop", "price": 999.99},
        {"id": 2, "name": "Mouse", "price": 24.99}
    ]

Error Handling

Raise HTTP exceptions:

from frappeapi.exceptions import HTTPException

@app.get()
def get_item(item_id: int):
    if item_id < 0:
        raise HTTPException(status_code=400, detail="Item ID must be positive")
    return {"id": item_id}

Custom exception handlers:

from frappeapi import JSONResponse, Request

class ItemNotFound(Exception):
    def __init__(self, item_id: int):
        self.item_id = item_id

@app.exception_handler(ItemNotFound)
def item_not_found_handler(request: Request, exc: ItemNotFound):
    return JSONResponse(
        status_code=404,
        content={"error": "ITEM_NOT_FOUND", "detail": f"Item {exc.item_id} not found"}
    )

Override validation error handler:

from frappeapi.exceptions import RequestValidationError

@app.exception_handler(RequestValidationError)
def validation_error_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=422,
        content={
            "error": "VALIDATION_ERROR",
            "details": [{"field": e["loc"][-1], "message": e["msg"]} for e in exc.errors()]
        }
    )

Header Parameters

from typing import Annotated
from frappeapi import Header

@app.get()
def get_user_info(
    user_agent: Annotated[str, Header()],
    x_custom_header: Annotated[str, Header()]
):
    return {"user_agent": user_agent, "custom_header": x_custom_header}

# Headers: User-Agent, X-Custom-Header (hyphen converted to underscore)

Field Validation

String validation:

class Product(BaseModel):
    name: str = Field(min_length=3, max_length=50)
    sku: str = Field(pattern="^[A-Z]{2}-[0-9]{4}$")  # Format: XX-0000

Numeric validation:

class Order(BaseModel):
    quantity: int = Field(gt=0, le=100)
    unit_price: float = Field(gt=0)
    discount_percent: float = Field(ge=0, le=100)

Version Compatibility

FrappeAPI automatically detects your Frappe version:

Frappe Version Support
v14.x Stable
v15.x Stable
v16.x Beta

Check detected version:

import frappeapi
print(frappeapi.get_detected_frappe_version())  # Returns: 14, 15, or 16

Documentation

FrappeAPI follows FastAPI's interface. For detailed information, see FastAPI's documentation.

Roadmap

Frappe Versions

  • Frappe V14 support
  • Frappe V15 support
  • Frappe V16 support (develop branch)

Methods

  • app.get(...)
  • app.post(...)
  • app.put(...)
  • app.patch(...)
  • app.delete(...)

Query Parameters

  • Automatic type parsing based on type hints
  • Required parameters (needy: str, needy: str = ...)
  • Optional parameters with defaults (skip: int = 0)
  • Optional parameters without defaults (limit: int | None = None)
  • Enum support
  • Boolean parameters
  • List parameters (?q=foo&q=bar)
  • Aliased parameters (Query(alias="query"))
  • Query parameter models
  • Automatic documentation generation

Body Parameters

  • Pydantic model body (item: Item)
  • Multiple body parameters
  • Singular values with Body()
  • Embed body parameter
  • Nested models
  • Automatic type parsing

Header Parameters

  • Basic header support
  • Header parameter models
  • Duplicate headers
  • Forbid extra headers

Cookie Parameters

  • Cookie parameter support

Form Data

  • Form fields with Form()
  • Multiple form fields
  • Form data as Pydantic model
  • Forbid extra form fields

File Uploads

  • Small files with File()
  • Large files with UploadFile
  • Optional file uploads
  • Multiple file uploads

Error Handling

  • HTTPException
  • RequestValidationError
  • ResponseValidationError
  • Custom exception handlers
  • Override default handlers
  • Frappe transaction management

Response Models

  • response_model parameter
  • Return type annotations
  • Output filtering
  • response_model takes precedence over return type

Validation

  • String validations (min_length, max_length, pattern)
  • Numeric validations (gt, ge, lt, le)
  • Metadata (title, description, deprecated)
  • include_in_schema

Planned

  • Rate limiting
  • Dependencies
  • Middleware
  • Debugging capabilities
  • Dotted path parameters

Related