Metadata-Version: 2.4
Name: fastapi-restly
Version: 0.5.0
Summary: A REST Framework for FastAPI
Author-email: Rutger Prins <rutgerprins@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/rjprins/fastapi-restly
Project-URL: Repository, https://github.com/rjprins/fastapi-restly
Project-URL: Source, https://github.com/rjprins/fastapi-restly
Project-URL: Documentation, https://rjprins.github.io/fastapi-restly/
Project-URL: Issues, https://github.com/rjprins/fastapi-restly/issues
Project-URL: Changelog, https://github.com/rjprins/fastapi-restly/blob/main/CHANGELOG.md
Keywords: fastapi,rest,crud,crud-api,sqlalchemy,pydantic,api,framework,react-admin
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Database
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: alembic>=1.15.2
Requires-Dist: fastapi>=0.115.0
Requires-Dist: orjson>=3.10.18
Requires-Dist: pydantic>=2.11.0
Requires-Dist: pydantic-settings>=2.9.1
Requires-Dist: sqlalchemy[asyncio]>=2.0.22
Provides-Extra: standard
Requires-Dist: aiosqlite>=0.21.0; extra == "standard"
Requires-Dist: fastapi[standard]>=0.115.0; extra == "standard"
Requires-Dist: httpx>=0.27.0; extra == "standard"
Requires-Dist: pytest>=8.3.5; extra == "standard"
Requires-Dist: pytest-asyncio>=0.24.0; extra == "standard"
Requires-Dist: pytest-cov>=6.1.1; extra == "standard"
Provides-Extra: docs
Requires-Dist: ghp-import>=2.1.0; extra == "docs"
Requires-Dist: myst-parser>=4.0.1; extra == "docs"
Requires-Dist: pydata-sphinx-theme>=0.16.0; extra == "docs"
Requires-Dist: sphinx>=8.1.3; extra == "docs"
Requires-Dist: sphinx-autobuild>=2024.10.3; extra == "docs"
Requires-Dist: sphinx-copybutton>=0.5.2; extra == "docs"
Requires-Dist: sphinx-design>=0.6.0; extra == "docs"
Requires-Dist: sphinx-sitemap>=2.9.0; extra == "docs"
Provides-Extra: testing
Requires-Dist: httpx>=0.27.0; extra == "testing"
Requires-Dist: pytest>=8.3.5; extra == "testing"
Requires-Dist: pytest-asyncio>=0.24.0; extra == "testing"
Requires-Dist: pytest-cov>=6.1.1; extra == "testing"
Dynamic: license-file

# FastAPI-Restly

[![CI](https://github.com/rjprins/fastapi-restly/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/rjprins/fastapi-restly/actions/workflows/ci.yml)
[![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue)](https://github.com/rjprins/fastapi-restly/blob/main/pyproject.toml)
[![License](https://img.shields.io/github/license/rjprins/fastapi-restly)](https://github.com/rjprins/fastapi-restly/blob/main/LICENSE)
[![Coverage](https://rjprins.github.io/fastapi-restly/coverage/badge.svg)](https://rjprins.github.io/fastapi-restly/coverage/)

<p align="center">
  <img src="https://raw.githubusercontent.com/rjprins/fastapi-restly/main/docs/_static/restly-cat-white-bg.png" alt="FastAPI-Restly logo" width="200">
</p>

**Build maintainable REST APIs on FastAPI, SQLAlchemy 2.0, and Pydantic v2 — with real class-based views.**

> **Status:** `0.5.0` — first public beta release.
> ```bash
> pip install "fastapi-restly[standard]"
> ```

**Docs:** <https://rjprins.github.io/fastapi-restly/> · **[Changelog](CHANGELOG.md)** · **[Contributing](CONTRIBUTING.md)** · **[Security](SECURITY.md)** · **[Examples](example-projects/)**

## Why FastAPI-Restly?

FastAPI-Restly is a small REST resource layer on top of FastAPI, SQLAlchemy, and Pydantic:

```text
SQLAlchemy owns persistence.
Pydantic owns validation and serialization.
FastAPI owns routing and dependency injection.
Restly owns repetitive REST resource mechanics.
```

The base `View` class is the foundation. It gives related endpoints a shared
prefix, tags, dependencies, and ordinary Python inheritance without forcing a
model or CRUD shape. `RestView` and `AsyncRestView` build on that foundation for
the common list/get/create/update/delete case.

- **True class-based views** — group endpoints on real Python classes with inheritance and method overrides.
- **REST endpoints in minutes** — use `View` for custom resources, or `AsyncRestView` / `RestView` for generated CRUD.
- **Class-level dependencies** — apply authentication, rate limits, tenant context, or other FastAPI dependencies once per view.
- **Explicit override points** — replace an endpoint, a business-logic handler, or an object helper without awkward hacks.
- **Modern stack** — SQLAlchemy 2.0, Pydantic v2, async and sync support.
- **Filtering, pagination, sorting** — standard HTTP query interface generated from your response schema.
- **Field control** — `ReadOnly` / `WriteOnly` markers, plus scalar foreign-key references via `IDRef[...]`.
- **React Admin ready** — `AsyncReactAdminView` speaks the `ra-data-simple-rest` wire contract, no custom data provider needed.
- **Testing utilities** — `RestlyTestClient` and savepoint-based isolation fixtures.

## Quickstart

FastAPI-Restly turns a SQLAlchemy model into a class-based CRUD resource:

```python
import fastapi_restly as fr
from fastapi import FastAPI
from sqlalchemy.orm import Mapped

app = FastAPI()

class User(fr.IDBase):
    name: Mapped[str]
    email: Mapped[str]

@fr.include_view(app)
class UserView(fr.AsyncRestView):
    prefix = "/users"
    model = User
```

That view exposes list, create, read, patch, and delete endpoints with filtering,
sorting, pagination, and an auto-generated Pydantic schema. For the full
copy-paste app, database setup, and run command, see
[Getting Started](docs/getting_started.md).

For endpoints that are related but not CRUD, start with `View`:

```python
from typing import Annotated
from fastapi import Depends

def get_current_user():
    ...

@fr.include_view(app)
class AccountView(fr.View):
    prefix = "/account"
    tags = ["account"]

    current_user: Annotated[User, Depends(get_current_user)]

    @fr.get("/me")
    async def me(self) -> AccountRead:
        return AccountRead.from_user(self.current_user)

    @fr.post("/password")
    async def change_password(self, payload: PasswordChange) -> AccountRead:
        ...
```

Annotated dependencies become instance attributes, so shared request context
lives on the view class instead of being repeated on every endpoint. The same
pattern works on `RestView` / `AsyncRestView`.

## Philosophy

Restly uses a layered approach. Each layer adds convenience while letting you
drop down for deeper control. The less customization you need, the more you get
out-of-the-box — full customization never requires awkward hacks. Restly stays
close to patterns already provided by FastAPI, Pydantic, and SQLAlchemy.

## Installation (development)

```bash
git clone https://github.com/rjprins/fastapi-restly.git
cd fastapi-restly
uv sync
```

## Advanced features

### Manual schema definition

For custom validation, aliases, or stable public contracts, define an explicit
read schema:

```python
from datetime import datetime

class UserRead(fr.IDSchema):
    name: str
    email: str
    created_at: fr.ReadOnly[datetime]

@fr.include_view(app)
class UserView(fr.AsyncRestView):
    prefix = "/users"
    model = User
    schema = UserRead
```

Restly derives create and update schemas from `UserRead` by default. When you
need full control over write payloads, declare them explicitly:

```python
class UserCreate(fr.BaseSchema):
    name: str
    email: str

class UserUpdate(fr.BaseSchema):
    name: str | None = None
    email: str | None = None

@fr.include_view(app)
class UserView(fr.AsyncRestView):
    prefix = "/users"
    model = User
    schema = UserRead
    creation_schema = UserCreate
    update_schema = UserUpdate
```

Use **auto-schema** for prototypes and internal tools. Use an **explicit schema** when contract stability and validation control matter (public APIs, aliases, strict response shapes).

### List endpoint query parameters

List endpoints expose a stable URL parameter dialect generated from the
response schema:

```bash
GET /users/?name=John&age__gte=21
GET /users/?status=active,pending           # comma-separated → OR (IN)
GET /users/?status__ne=archived,deleted     # comma-separated → NOT IN
GET /users/?email__icontains=example
GET /users/?deleted_at__isnull=true
GET /users/?sort=-created_at,name
GET /users/?page=2&page_size=10
```

Parameter keys follow the **response schema's public names** end-to-end —
including dotted relation paths. If `ArticleRead.author` has
`Field(alias="writer")` and `AuthorRead.name` has
`Field(alias="authorName")`, the URL key is `writer.authorName`. Aliased
fields are only reachable by their alias; `populate_by_name` does not
extend the URL surface with the Python field name.

Pagination is opt-in: omitting `page_size` returns every matching row.
For public/production endpoints set `default_page_size` and
`max_page_size` on the view class:

```python
class UserView(fr.AsyncRestView):
    default_page_size = 25
    max_page_size = 200
```

See [How-To: Filter, Sort, and Paginate Lists](docs/howto_query_modifiers.md)
for the full operator surface, alias rules, and pagination guidance.

### Read-only and write-only fields

`IDSchema` already provides a read-only `id`, so don't redeclare it unless you need to narrow the type.

```python
class UserRead(fr.IDSchema):
    name: str
    email: str
    password: fr.WriteOnly[str]        # stripped by to_response_schema()
    created_at: fr.ReadOnly[datetime]  # cannot be set in requests
```

### Relationships

```python
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column

class Order(fr.IDBase):
    customer_id: Mapped[int] = mapped_column(ForeignKey("customer.id"))
    total: Mapped[float]

class OrderRead(fr.IDSchema):
    customer_id: fr.IDRef[Customer]      # wire format: 123 — resolved to FK
    total: float
```

### Custom endpoints and handlers

Add endpoints with `@fr.get`, `@fr.post`, `@fr.put`, `@fr.patch`, `@fr.delete`, or the generic `@fr.route`. Override `perform_*` handlers (`perform_listing`, `perform_get`, `perform_create`, ...) to customise built-in CRUD logic without replacing the endpoint.

```python
@fr.include_view(app)
class UserView(fr.AsyncRestView):
    prefix = "/users"
    model = User
    schema = UserRead

    @fr.get("/{id}/download")
    async def download_user(self, id: int):
        return {"id": id, "status": "ok"}

    async def perform_listing(self, query_params):
        # Custom logic here
        return await super().perform_listing(query_params)
```

### React Admin integration

Use `AsyncReactAdminView` to get a backend that [react-admin](https://marmelab.com/react-admin/) with [`ra-data-simple-rest`](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-simple-rest) connects to out of the box:

```python
@fr.include_view(app)
class ProductView(fr.AsyncReactAdminView):
    prefix = "/products"
    model = Product
    schema = ProductRead
```

The view speaks the `ra-data-simple-rest` wire contract:

- **List** — translates `sort=["name","ASC"]`, `range=[0,24]`, and `filter={"name":"foo"}` into SQL and returns a JSON array with a `Content-Range: items 0-24/315` header.
- **All other CRUD** — `GET /{id}`, `POST /`, `PATCH /{id}`, `DELETE /{id}` work unchanged.

See [React Admin Integration](https://rjprins.github.io/fastapi-restly/howto_react_admin.html) in the docs for CORS setup and customization.

### Excluding built-in routes

```python
@fr.include_view(app)
class UserView(fr.AsyncRestView):
    prefix = "/users"
    model = User
    exclude_routes = (fr.ViewRoute.DELETE,)
```

### Pagination metadata

```python
@fr.include_view(app)
class UserView(fr.AsyncRestView):
    prefix = "/users"
    model = User
    include_pagination_metadata = True
    # Response: {"items": [...], "total": N, "page": 1, "page_size": 100, "total_pages": N, ...}
```

## Testing

`fastapi_restly.pytest_fixtures` provides namespaced pytest fixtures (`restly_app`, `restly_client`, `restly_async_session`, `restly_session`) for test clients and **savepoint-based isolation**. The testing extra installs a pytest plugin entry point, so pytest auto-loads these fixtures.

Install the testing extra when consuming FastAPI-Restly as a package:

```bash
pip install "fastapi-restly[testing]"
```

Configure Restly for your test database in `conftest.py`.

`RestlyTestClient` automatically asserts the expected HTTP status (`200` for GET, `201` for POST, `204` for DELETE, ...) and raises a descriptive `AssertionError` with the response body on failure:

```python
# test_users.py
def test_create_and_fetch_user(restly_client):
    # Raises AssertionError if status != 201
    response = restly_client.post("/users/", json={"name": "John", "email": "john@example.com"})
    user_id = response.json()["id"]

    # Raises AssertionError if status != 200
    data = restly_client.get(f"/users/{user_id}").json()
    assert data["name"] == "John"
```

Pass `assert_status_code=None` to skip the assertion and inspect the response yourself.

## Configuration

```python
# Async SQLite
fr.configure(async_database_url="sqlite+aiosqlite:///app.db")

# Async PostgreSQL
fr.configure(async_database_url="postgresql+asyncpg://user:pass@localhost/db")

# Sync SQLite
fr.configure(database_url="sqlite:///app.db")
```

Restly has one public process-wide configuration. For per-view databases,
read replicas, or other custom session wiring, use a normal FastAPI dependency
on that view; see the existing-project how-to in the documentation.

## Documentation

- **[Getting Started](https://rjprins.github.io/fastapi-restly/getting_started.html)** — fast path from zero to a working API
- **[User Guide](https://rjprins.github.io/fastapi-restly/user_guide.html)** — tutorial walkthroughs and topic guides
- **[API Reference](https://rjprins.github.io/fastapi-restly/api_reference.html)** — complete API docs

## Examples

Complete applications under [`example-projects/`](example-projects/):

- **[Shop](example-projects/shop/)** — e-commerce API with products, orders, customers
- **[Blog](example-projects/blog/)** — minimal blog with a single `Blog` model
- **[SaaS](example-projects/saas/)** — multi-tenant project management API

## Contributing

Pull requests and issue discussions welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, coding standards, and the test workflow. Security issues: see [SECURITY.md](SECURITY.md).

## License

MIT — see [LICENSE](LICENSE).
