# BirchRest
[![PyPI version](https://badge.fury.io/py/birchrest.svg)](https://pypi.org/project/birchrest/)
![GitHub Release Date](https://img.shields.io/github/release-date/alexandengstrom/birchrest)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
![GitHub issues](https://img.shields.io/github/issues/alexandengstrom/birchrest)
![GitHub last commit](https://img.shields.io/github/last-commit/alexandengstrom/birchrest)
![Unit Tests](https://github.com/alexandengstrom/birchrest/actions/workflows/unit_test.yml/badge.svg)
![Type Checking](https://github.com/alexandengstrom/birchrest/actions/workflows/type_checking.yml/badge.svg)
![Linting](https://github.com/alexandengstrom/birchrest/actions/workflows/linting.yml/badge.svg)
[![codecov](https://codecov.io/gh/alexandengstrom/birchrest/branch/main/graph/badge.svg)](https://codecov.io/gh/alexandengstrom/birchrest)
[![Downloads](https://img.shields.io/pypi/dm/birchrest)](https://pypi.org/project/birchrest/)
[![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://alexandengstrom.github.io/birchrest/)
![Repo Size](https://img.shields.io/github/repo-size/alexandengstrom/birchrest)
**BirchRest** is a simple, lightweight framework for setting up RESTful APIs with minimal configuration. It is designed to be intuitive and flexible.
Full documentation is available here:
https://alexandengstrom.github.io/birchrest
## Quickstart
1. **Install**: You can install the latest version of birchrest using pip:
```bash
pip install birchrest
```
2. **Init**: Create a boilerplate project with ```birch init``` command:
```bash
birch init
```
3. **Start**: Start the server via command line:
```bash
birch serve
```
## Table of Contents
## Table of Contents
1. [Introduction](#introduction)
2. [Defining Controllers](#defining-controllers)
- [Key Concepts](#key-concepts)
- [Defining Endpoints](#defining-endpoints)
- [Nesting Controllers](#nesting-controllers)
3. [Middleware](#middleware)
- [Custom Middlewares](#custom-middlewares)
- [Requirements](#requirements)
- [Built-in Middlewares](#built-in-middlewares)
- [Rate Limiter](#rate-limiter)
- [Cors](#cors)
4. [Data Validation](#data-validation)
- [Body Validation](#body-validation)
- [Query and URL Param Validation](#query-and-url-param-validation)
- [Supported Validation Constraints](#supported-validation-constraints)
- [Type Validation](#type-validation)
- [String Constraints](#string-constraints)
- [Numeric Constraints](#numeric-constraints)
- [Optional Fields](#optional-fields)
- [List Constraints](#list-constraints)
- [Nested Dataclasses](#nested-dataclasses)
5. [Authentication](#authentication)
- [Custom Auth Handlers](#custom-auth-handlers)
- [Protecting Routes](#protecting-routes)
6. [Error Handling](#error-handling)
- [ApiError](#apierror)
- [Custom Error Handler](#custom-error-handler)
7. [Unit Testing](#unit-testing)
- [Test Adapter](#test-adapter)
- [BirchRestTestCase](#birchresttestcase)
8. [Requests And Responses](#requests-and-responses)
- [Request](#request)
- [Response](#response)
- [Request and Response Lifecycle](#request-and-response-lifecycle)
- [1. Receiving and Parsing the Request](#1-receiving-and-parsing-the-request)
- [2. Passing the Request to the App](#2-passing-the-request-to-the-app)
- [3. Handling the Request in the App](#3-handling-the-request-in-the-app)
- [4. Route Execution](#4-route-execution)
- [5. Returning the Response](#5-returning-the-response)
9. [OpenAPI](#openapi)
- [How It Works](#how-it-works-2)
- [Automatic Response Code Detection](#automatic-response-code-detection)
- [Customizing](#customizing-openapi-information)
- [Generating](#generating-the-openapi-spec)
10. [Contributing](#contributing)
11. [License](#license)
## Introduction
BirchRest follows a controller-based architecture, where each controller represents a logical grouping of API routes. The framework automatically constructs your API at runtime from the controllers you define. To make this work, simply create a file named ```__birch__.py``` and import all your controllers into this file. BirchRest will use this file to discover and configure your API routes.
```python
from birchrest import Controller
from birchrest.decorators import get, controller
from birchrest.http import Request, Response
@controller("api")
class MyController(Controller):
@get("hello")
async def hello(self, req: Request, res: Response):
return res.send({"message": "Hello from the app!"})
```
To start the server, instantiate the BirchRest class and call its serve method.
```python
from birchrest import BirchRest
app = BirchRest()
app.serve()
```
Or start the server via command line:
```bash
birch serve --port [PORT] --host [HOST] --log-level [LOG_LEVEL]
```
## Defining Controllers
In Birchrest, controllers are the building blocks of your API. Each controller defines multiple endpoints, and controllers can be nested to create hierarchical routes.
### Key Concepts
- **Base Path**: Each controller has a base path that defines where its routes are accessible. If a controller has subcontrollers, their base paths are combined, creating a nested structure.
- **Controller Setup**: To create a controller:
1. Inherit from the Controller class
2. Use the @controller decorator on the class, passing the base path as an argument.
### Defining Endpoints
Inside a controller, use HTTP method decorators like @get or @post to define endpoints. These decorators can take an optional path to extend the controller’s base path for that specific route.
```python
# Create an endpoint that accepts PATCH method on route /myendpoint.
@patch("myendpoint")
async def patch(self, req: Request, res: Response):
print(req.body)
return res.send({"message": "success"})
```
To define path variables, use a colon (```:```) in the path. You can then access these variables through the ```req.params``` object.
```python
@get("user/:id")
async def patch(self, req: Request, res: Response):
userId = req.params.get("id")
return res.send({"id": userId})
```
A route can also access queries in the same way:
```python
@get("user")
async def patch(self, req: Request, res: Response):
name = req.queries.get("name")
return res.send({"name": name})
```
It is possible to set automatic contraints for the body, queries and params via validation decorators. See section about validation.
BirchRest is fully asynchronous, meaning all route handlers and middleware must be defined as async functions. This allows the framework to handle multiple requests concurrently without blocking. Ensure that all I/O-bound operations, such as database queries, file handling, or external API requests, are awaited properly. Failing to use async or forgetting to await asynchronous operations can lead to blocking behavior, defeating the purpose of using an asynchronous framework.
### Nesting Controllers
BirchRest supports hierarchical route structures by allowing controllers to inherit from other controllers. This creates nested routes where the child controller's base path is combined with the parent controller's base path. In BirchRest, subcontrollers are created by having one controller class inherit from another controller class.
This approach makes it easy to group related endpoints under a common path and manage them as a logical structure.
#### Example
Let’s say we have a base API controller and we want to nest a resource controller under it:
```python
from birchrest import Controller
from birchrest.decorators import get, controller
from birchrest.http import Request, Response
# Define the parent controller
@controller("api")
class BaseController(Controller):
@get("status")
async def status(self, req: Request, res: Response):
return res.send({"message": "API is running"})
# Define the child controller that inherits from the parent
@controller("resource")
class ResourceController(BaseController):
@get("hello")
async def hello(self, req: Request, res: Response):
return res.send({"message": "Hello from resource!"})
```
This will create the endpoint /api/resouce/hello.
In this example:
- The ```BaseController``` is the parent controller that handles routes under ```/api```.
- The ```ResourceController``` inherits from ```BaseController```, making it a child controller nested under ```/api/resource```.
- The route for the "hello" endpoint in ```ResourceController``` becomes ```/api/resource/hello```.
- The route for the "status" endpoint from ```BaseController``` is ```/api/status```.
By inheriting from BaseController, the ResourceController becomes a child, automatically inheriting and extending the parent’s routing structure.
## Middleware
Middleware allows you to perform tasks before or after a request is processed by a controller, such as logging, modifying the request, or checking permissions. Birchrest provides built-in middleware for common tasks and the ability to define your own custom middleware.
### Custom Middlewares
You can create custom middleware to handle specific logic or modify request and response objects. This section explains how to define and register middleware in your application.
Middleware operates hierarchically, meaning it applies to all routes below the point where it’s defined. You can set up global middleware directly at the application level, or use decorators on controllers and routes. When applied to a controller, the middleware will affect all routes within that controller, as well as any nested controllers attached to it. If applied to a route it will be applied only on that route.
#### Requirements
A middleware should be a class that inherits from the Middleware class and it must implement an async call method. The call method will receive a Request, Response and NextFunction. If the NextFunction is called the call will continue to the next middleware or route handler. If not called, we wont continue. The next function must be awaited.
```python
from birchrest.http import Request, Response, Middleware
from birchrest.types import NextFunction
from birchrest.exceptions import BadRequest
class MyMiddleware(Middleware):
def __init__(self, state: int):
self.state = state
async def __call__(self, req: Request, res: Response, next: NextFunction):
if self.state:
await next()
else:
raise BadRequest
```
It is possible to execute things after next is called aswell, this means you can use middlewares for postprocessing aswell. Just like route handlers, all middleware in BirchRest must be asynchronous.
### Built-in Middlewares
Birchrest comes with several built-in middleware options that help manage common use cases, such as request logging, rate limiting or CORS support. These can be easily added to your API with minimal configuration. These can be imported from the middlewares module.
```python
from birchrest.middlewares import Cors, Logger, RateLimiter
```
#### Rate Limiter
The RateLimiter middleware in BirchRest helps protect your API from abuse by limiting the number of requests a client (identified by their IP address or token) can make within a specified time window. It is particularly useful for preventing denial-of-service (DoS) attacks or enforcing fair usage limits.
##### How it works:
- The rate limiter tracks the number of requests made by each client within a rolling time window.
- If a client exceeds the allowed number of requests within the window, the middleware responds with a ```429 Too Many Requests``` error.
- Requests older than the current time window are automatically cleared from the log to allow new requests.
##### Configuration Options:
- ```max_requests```: The maximum number of requests a client can make within the time window (default is 2).
- ```window_seconds```: The length of the time window in seconds during which the requests are counted (default is 10 seconds).
##### Example:
```python
from birchrest.middlewares import RateLimiter
# Apply rate limiting globally
app.middleware(RateLimiter(max_requests=5, window_seconds=60))
```
In this example, the middleware limits each client to a maximum of 5 requests per 60 seconds. If the limit is exceeded, any additional requests within that time will receive a ```429``` error response.
#### Cors
The CORS (```Cross-Origin Resource Sharing```) middleware in BirchRest enables your API to respond to cross-origin requests securely by controlling which origins, methods, and headers are allowed. It also handles preflight (```OPTIONS```) requests for methods other than ```GET``` and ```POST```, or when using custom headers.
##### How It Works:
- The middleware inspects each request and adds the necessary CORS headers to the response based on the configured settings. This allows browsers to enforce the CORS policy and determine if the request is permitted.
- For preflight requests (```OPTIONS``` method), it sends the appropriate response headers to indicate which origins, methods, and headers are allowed.
- For regular requests, it ensures the appropriate headers are added to allow cross-origin resource sharing.
##### Configuration Options:
- ```allow_origins```: List of allowed origins (default is ["*"], allowing all origins).
- ```allow_methods```: List of allowed HTTP methods (default includes GET, POST, PUT, DELETE, PATCH, OPTIONS).
- ```allow_headers```: List of allowed request headers (default is ["Content-Type", "Authorization"]).
- ```allow_credentials```: Whether credentials (cookies, HTTP authentication, etc.) are allowed (default is False).
- ```max_age```: The time (in seconds) that preflight request results can be cached by the browser (default is 86400 seconds or 24 hours).
##### Example:
```python
from birchrest.middlewares import Cors
# Apply CORS middleware globally with default settings
app.middleware(Cors(allow_origins=["https://example.com"], allow_credentials=True))
```
In this example, only requests from https://example.com are allowed, and credentials (like cookies) are permitted to be sent with cross-origin requests. The middleware ensures that the appropriate CORS headers are added to all responses.
## Data Validation
Data validation in Birchrest is supported via Python data classes. This allows for strict validation of request data (body, queries, and params) to ensure that all incoming data adheres to the expected structure.
To be able to use validation, you must also define the models.
### Body Validation
#### Example:
```python
@dataclass
class Address:
street: str = field(metadata={"min_length": 3, "max_length": 100})
city: str = field(metadata={"min_length": 2, "max_length": 50})
@dataclass
class User:
username: str = field(metadata={"min_length": 3, "max_length": 20})
email: str = field(metadata={"regex": r"^[\w\.-]+@[\w\.-]+\.\w+$"})
age: int = field(metadata={"min_value": 0, "max_value": 120})
address: Address
```
You can then use the @body, @queries and @params decorator with the dataclass as argument.
Example:
```python
@post("user")
@body(User)
async def create_user(self, req: Request, res: Response):
# It is safe to pass the body directly since we have already validated it.
save_to_database(request.body)
return res.status(201).send()
```
If the validation fails, the user will get an automatic response. For example, if we try to post a user to the route above but passes a username with only two letters. We will receive this response:
```json
{
"error": {
"status": 400,
"code": "Bad Request",
"correlationId": "67ad2218-262e-478b-b767-04cfafd4315b",
"message": "Body validation failed: Field 'username' must have at least 3 characters."
}
}
```
Read more about how automatic error responses are handled in the error section.
### Query and URL Param Validation
Validating queries and params is done in the same way, just use the @queries and @params decorators instead.
### Supported Validation Constraints
Below is a list of all the validation constraints you can define using ```field(metadata={...})```:
1. #### Type Validation:
Data is automatically validated against the field's type. Supported types include int, float, str, list, and nested dataclasses.
Example:
```python
@dataclass
class User:
age: int
```
2. #### String Constraints:
- ```min_length```: Ensures that the string has at least a certain number of characters.
- ```max_length```: Ensures that the string does not exceed a certain number of characters.
- ```regex```: Ensures that the string matches a given regular expression.
- Example:
```python
@dataclass
class User:
username: str = field(metadata={"min_length": 3, "max_length": 20})
email: str = field(metadata={"regex": r"[^@]+@[^@]+\.[^@]+"})
```
3. #### Numeric Constraints:
- ```min_value```: Ensures that the number is at least a certain value.
- ```max_value```: Ensures that the number does not exceed a certain value.
- ```Example```:
```python
@dataclass
class User:
age: int = field(metadata={"min_value": 18, "max_value": 120})
```
4. #### Optional Fields:
- Fields can be marked as optional by specifying ```is_optional: True``` in the metadata. This allows a field to be omitted from the input data without causing a validation error.
- Example:
```python
@dataclass
class User:
age: Optional[int] = field(metadata={"is_optional": True})
phone: Optional[str] = field(metadata={"is_optional": True, "regex": r"^\d{10}$"})
```
5. #### List Constraints:
- ```min_items```: Ensures that a list has at least a certain number of items.
- ```max_items```: Ensures that a list does not exceed a certain number of items.
- ```unique```: Ensures that all items in the list are unique.
- You can also nest dataclasses inside lists and apply validation to each item.
- Example:
```python
@dataclass
class Address:
street: str = field(metadata={"min_length": 5, "max_length": 100})
@dataclass
class User:
addresses: List[Address] = field(metadata={"min_items": 1, "max_items": 3})
```
6. #### Nested Dataclasses:
- You can nest dataclasses inside each other, and BirchRest will automatically validate nested structures.
- Example:
```python
@dataclass
class ContactInfo:
email: str = field(metadata={"regex": r"[^@]+@[^@]+\.[^@]+"})
@dataclass
class User:
username: str = field(metadata={"min_length": 3, "max_length": 20})
contact_info: ContactInfo
```
## Authentication
Birchrest makes it easy to protect your API routes with authentication mechanisms. It allows you to define custom authentication handlers and easily mark routes as protected, ensuring that only authenticated requests are allowed access.
### Custom Auth Handlers
You can define your own authentication handler to manage how users are authenticated in your system. Once defined, Birchrest will handle the integration with the API. If your route handler returns a falsy value or raises an Exception, the execution will be stopped. Otherwise the return value from this function will be put under the user property in the request object. It is therefore possible to put information there that tells you which user sent a request.
### Protecting Routes
You can easily protect individual routes or entire controllers by marking them as requiring authentication. Birchrest will automatically handle unauthorized access by returning predefined error messages.
```python
from birchrest import BirchRest, Controller
from birchrest.decorators import get, controller
from birchrest.http import Request, Response
async def auth_handler(req: Request, res: Response):
if req.headers.get("Authorization"):
# Do your logic
return { "id": 1 }
return False
@controller("api")
class MyController(Controller):
@protected()
@get("protected")
async def hello(self, req: Request, res: Response):
return res.send({"message": "Hello from the app!"})
app = BirchRest()
app.register(MyController)
app.serve()
```
## Error Handling
By default, BirchRest responds with standardized error messages and provides as much detail as possible when an error occurs. Common error responses like 404 (Not Found) when a route doesn't exist, or 400 (Bad Request) when body validation fails, are handled automatically. If an unhandled exception occurs within your controllers, a 500 Internal Server Error will be returned.
### ApiError
The **ApiError** class is the base class for a variety of HTTP exceptions such as NotFound, BadRequest, Unauthorized, and more. If any of these exceptions are raised during request handling, BirchRest will automatically convert them into the appropriate HTTP response with the correct status code and error message.
```python
from birchrest.exceptions import NotFound
raise NotFound
```
This will automatically generate a 404 Not Found HTTP response to the client, with the provided user-friendly message.
Each ApiError has the following attributes:
- ```status_code```: The HTTP status code (e.g., 404, 400, 500).
- ```base_message```: A default message associated with the status code (e.g., "Not Found" for 404).
- ```user_message```: An optional custom message that can provide more specific details about the error.
BirchRest supports the following common HTTP exceptions out-of-the-box:
- ```BadRequest``` (400)
- ```Unauthorized``` (401)
- ```Forbidden``` (403)
- ```NotFound``` (404)
- ```MethodNotAllowed``` (405)
- ```Conflict``` (409)
- ```UnprocessableEntity``` (422)
- ```InternalServerError``` (500)
- ```ServiceUnavailable``` (503)
- ```PaymentRequired``` (402)
- ```RequestTimeout``` (408)
- ```Gone``` (410)
- ```LengthRequired``` (411)
- ```PreconditionFailed``` (412)
- ```PayloadTooLarge``` (413)
- ```UnsupportedMediaType``` (415)
- ```TooManyRequests``` (429)
- ```UpgradeRequired``` (426)
The framework handles everything behind the scenes if any of these exceptions are raised. You don't need to manually craft the response or worry about setting the correct status code—BirchRest takes care of it.
### Custom Error Handler
If you need more control over how errors are handled, you can define your own custom error handler. This handler will receive the request, response, and exception as arguments. The handler must manage the exception explicitly; otherwise, a ```500 Internal Server Error``` will be returned by default.
#### Example:
```python
from birchrest.http import Request, Response
from birchrest.exceptions import ApiError
async def error_handler(req: Request, res: Response, e: Exception) -> Response:
if isinstance(e, ApiError):
# If it an ApiError, use the build in converter if you want
return e.convert_to_response(res)
# Do your own error handling here...
return res.status(500).send({"error": "This was not supposed to happen...."})
```
## Unit Testing
### Test Adapter
To simplify testing, the framework includes a test adapter class that simulates sending HTTP requests to your API. This allows you to test everything except the server itself, with all middlewares, authentication handlers, and other components functioning exactly as they would in a real request. The adapter returns the final response object, which you can inspect and assert in your tests.
The TestAdapter class takes an instance of your app and then provides methods like get, post etc that accepts a path, headers and body.
#### Example
```python
from birchrest import BirchRest
from birchrest.unittest import TestAdapter
app = BirchRest()
runner = TestAdapter(app)
response = runner.get("/your-route")
```
### BirchRestTestCase
BirchRest also provides a custom TestCase class (BirchRestTestCase) that includes helper methods to make it easier to assert HTTP responses. These methods help ensure that your API responds as expected. Below is a list of the available assertion methods and their descriptions:
- ```assertOk(response)```: Asserts that the response status code is in the range of 2xx, indicating a successful request.
- ```assertNotOk(response)```: Asserts that the response status code is not in the range of 2xx, indicating a failure.
- ```assertBadRequest(response)```: Asserts that the response status code is 400, indicating a Bad Request.
- ```assertNotFound(response)```: Asserts that the response status code is 404, indicating a resource was not found.
- ```assertUnauthorized(response)```: Asserts that the response status code is 401, indicating an Unauthorized request.
- ```assertForbidden(response)```: Asserts that the response status code is 403, indicating a Forbidden request.
- ```assertInternalServerError(response)```: Asserts that the response status code is 500, indicating an Internal Server Error.
- ```assertStatus(response, expected_status)```: Asserts that the response status code matches the expected_status.
- ```assertHasHeader(response, expected_key)```: Asserts that the response contains a specific header.
- ```assertHeader(response, header_name, expected_value)```: Asserts that a specific header in the response matches the expected value.
- ```assertRedirect(response, expected_url)```: Asserts that the response status is a redirect (3xx) and that the Location header matches the expected URL.
- ```assertBodyContains(response, expected_key)``: Asserts that the response body contains a specific property or key.
#### Example:
```python
import unittest
from birchrest import BirchRest
from birchrest.unittest import TestAdapter, BirchRestTestCase
class ApiTest(BirchRestTestCase):
def setUp(self) -> None:
app = BirchRest(log_level="test")
self.runner = TestAdapter(app)
async def test_user_route(self) -> None:
response = await self.runner.get("/user")
self.assertOk(response)
async def test_invalid_id(self) -> None:
response = await self.runner.get("/user/0")
self.assertNotOk(response)
self.assertStatus(response, 400)
if __name__ == "__main__":
unittest.main()
```
## Requests And Responses
### Request
The Request object represents the incoming HTTP request. It encapsulates all relevant information about the request, including the HTTP method, URL path, headers, body, query parameters, and more. The Request object provides access to these components, allowing you to handle and process the request effectively.
Properties:
- ```method: str```
The HTTP method used for the request (e.g., GET, POST, PUT, DELETE).
- ```path: str```
The full requested URL path, including any query string (e.g., /api/users?id=123).
- ```clean_path: str```
The URL path without query parameters (e.g., /api/users).
- ```version: str```
The HTTP version used for the request (e.g., HTTP/1.1).
- ```headers: Dict[str, str]```
A dictionary of all HTTP request headers. Headers are case-insensitive.
Example:
```python
content_type = req.headers.get("content-type")
```
- ```queries: dataclass```
A dataclass of query parameters extracted from the URL. If a query parameter has multiple values, it will be a list. Otherwise, it will be a single string. If you have used the ```@queries``` decorator it will be your custom dataclass, otherwhise the request would have failed.
Example:
```python
search_query = req.queries.name
```
- ```params: dataclass```
A dataclass of URL path parameters (if any) extracted from the route. This is populated during route matching. You can also define this via the ```@params``` decorator.
Example:
```python
user_id = req.params.id
```
- ```body: dataclass```
The parsed body of the request. You can ensure this follows your exact dataclass model by using the ```@body``` decorator.
Example:
```python
user_data = req.body.user.name.firstName
```
- ```client_address: str```
The IP address of the client making the request.
- ```client_port: Optional[int]```
The port number used by the client to send the request. This is helpful for identifying the source of the request.
Example:
```python
print(f"Request received from {req.client_address}:{req.client_port}")
```
- ```correlation_id: str```
A unique ID automatically generated for each request. This can be used to track and correlate requests across different systems.
- ```received: datetime```
The timestamp indicating when the request was received by the server. This can be useful for logging and performance tracking.
Example:
```python
print(f"Request received at {req.received}")
```
- ```user: Optional[Any]```
Placeholder for any authenticated user information. This will be populated if the request goes through an authentication handler.
- ```host: Optional[str]```
The Host header from the request, which indicates the server's host (domain or IP) to which the request was made.
Example:
```python
print(f"Host: {req.host}")
```
- ```referrer: Optional[str]```
The Referer header (if present) from the request, which indicates the page from which the request originated.
- ```user_agent: Optional[str]```
The User-Agent header from the request, which identifies the client software (browser, bot, etc.).
### Response
The Response object in BirchRest is responsible for crafting the outgoing HTTP response that is sent back to the client. It provides methods to set the status code, headers, and body, with support for automatically sending JSON-encoded responses.
#### Important Methods
- ```status(code: int) -> Response```
This method sets the HTTP status code for the response. You can either pass an integer representing the status code (e.g., 200 for OK or 404 for Not Found) or use the HttpStatus enum for predefined status codes. However, it is recommended to raise ```ApiError``` instead if you want to return errors. A good use would be when responding with 201. The status code is set to 200 by default, so if you intend to return that, you dont have to call status method.
Example:
```python
res.status(404)
```
- ```send(data: Any = {}) -> Response```
This method sets the response body by encoding the provided data as JSON and automatically sets the Content-Type header to application/json. The response can only be sent once; attempting to call send a second time will raise an exception.
*Note: All responses in BirchRest are automatically sent as JSON.*
Example:
```python
res.status(200).send({"message": "Success"}) # Sends a JSON response with a 200 status code
```
**Important:**
- Once send is called, the response is finalized, and calling send again will result in an error ("Request was sent twice").
- The Content-Length header is automatically set based on the length of the JSON-encoded response.
### Request and Response Lifecycle
The BirchRest framework handles HTTP requests using a structured flow to ensure that all incoming requests are processed correctly, including middleware execution, validation, and error handling. This section explains the lifecycle of a request from when it is received by the server to when a response is sent back to the client.
#### 1. Receiving and Parsing the Request
When a client sends an HTTP request to the server, the server parses the raw request data into a Request object. This object encapsulates all details about the incoming request, such as headers, method (e.g., GET, POST), query parameters, URL parameters, and body data.
#### 2. Passing the Request to the App
Once the request object is created, it is passed to the main application (BirchRest) for handling. The app creates a new Response object, which will later be populated and returned to the client. The app then looks for a matching route by searching through all defined routes based on the request’s URL and HTTP method.
#### 3. Handling the Request in the App
The main request handling logic is performed by the handle_request method in the app. This method attempts to match the incoming request to a route and execute the following key steps:
- **Route Matching**: The app searches through all registered routes to find one that matches the URL path and HTTP method of the request. If a matching route is found, the request proceeds to that route. If no route matches, a ```404 Not Found``` error is raised, or if the route exists but the HTTP method is incorrect, a ```405 Method Not Allowed``` error is raised.
- **Passing the Request to the Route**: Once a route is matched, the app passes both the request and response objects to that route for further processing.
- **Error Handling**: If an exception occurs during request handling (such as an invalid request or missing route), the app catches the exception and attempts to generate an appropriate error response using predefined or custom error handlers.
#### 4. Route Execution
Each route in BirchRest is responsible for executing its logic and handling the request:
- **Middleware Execution**: When the request reaches the matched route, the route begins by executing any middleware associated with it. Middleware can modify the request or response objects, perform tasks such as logging or authentication, and decide whether to continue processing the request. Middleware runs in a chain, meaning each middleware can pass control to the next one, or halt the chain and send a response early.
The route's ```__call__``` method initiates this process by calling the first middleware in the stack. If no middleware interrupts the chain, the request proceeds to the route handler.
- **Authentication**: If the route is protected by authentication, the request must pass through an authentication handler. This handler validates the request (e.g., checking tokens or credentials). If authentication fails, a ```401 Unauthorized``` error is raised, and the response is sent back to the client.
- **Validation**: If the route requires validation of the request body, query parameters, or URL parameters, the request data is checked against predefined data classes. If the data fails validation (e.g., missing required fields or incorrect types), a 400 Bad Request error is raised.
- **Executing the Route Handler**: Once middleware, authentication, and validation checks pass, the route handler function is executed. The route handler is responsible for performing the main business logic, such as fetching data, processing the request, or interacting with external services. After processing, the route handler populates the response object with the appropriate data and status code.
#### 5. Returning the Response
After the route handler completes, the response object (which was initially created at the beginning of the request) contains the data to be sent back to the client. This includes the HTTP status code, headers, and response body.
The final response is then returned to the server, which sends it to the client. If any errors occurred during the request lifecycle, they are automatically converted into error responses by the app’s error handler.
## OpenAPI
One of the features of the framework is its ability to automatically generate OpenAPI specifications based on the defined routes, parameters, and decorators. This process integrates with the core functionality of the framework to ensure that your API documentation is always up-to-date without requiring manual intervention.
### How It Works
The framework reuses as much information as possible to define the structure of your API, which are then converted into OpenAPI schemas:
- **`@body`**, **`@queries`**, and **`@params`**: These decorators define the request body, query parameters, and path parameters respectively. The framework converts these into OpenAPI-compliant schemas, ensuring the input structure is well-documented.
- **`@produces`**: This decorator is used to specify the return type of a route, generating the corresponding schema for the response object. Note that while `@produces` defines the schema for the OpenAPI specification, it does not affect the runtime logic of the route itself.
- **`@tag`**: The `@tag` decorator allows you to attach tags to routes or controllers. These tags are used purely for documentation purposes in the OpenAPI spec, helping to categorize and organize your API endpoints.
- **`docstrings`**: The python docstrings for your route handlers will be reused to describe your endpoints.
### Automatic Response Code Detection
The framework analyzes the entire flow of a route to detect what HTTP status codes can be returned. By inspecting the **Abstract Syntax Tree (AST)** of the route handler, auth handler, middlewares, the framework can identify the status codes specified in the code, whether they are success or error codes. This process includes analyzing any exceptions (such as `ApiError`) raised within the route or its middlewares to ensure that all possible responses are captured.
### Customizing OpenAPI Information
In addition to automatically generating endpoint definitions, the framework allows you to specify additional metadata about your API. You can define information such as license, terms of service, and contact details by adding these fields to the `__birch__.py` file in your project. This information will be reflected in the OpenAPI specification's `info` section, making it easy to provide consumers with full details about your API.
### Generating the OpenAPI Spec
To generate the OpenAPI specification, simply run the following command:
```bash
birch openapi
```
It will create a file named openapi.json. It is possible to override the filename by providing the flag `--filename`.
```bash
birch openapi --filename myspec.json
```
## Contributing
Contributions are welcome! Please refer to the [CONTRIBUTING.md](./CONTRIBUTING.md) file for details on how to get involved, submit pull requests, and report issues.
## License
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
Raw data
{
"_id": null,
"home_page": null,
"name": "birchrest",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": "api, rest, framework, web, microservices, controller, middleware, auth management, response handling, fastapi alternative, flask alternative, restful api, python web framework",
"author": null,
"author_email": "Alexander Engstr\u00f6m <alexander@engstrom.ae>",
"download_url": "https://files.pythonhosted.org/packages/5c/60/9877f3cdace3ca7ddac0f90adf7fa8bc594121a97d1a7445b1e7addd8e19/birchrest-1.1.0.tar.gz",
"platform": null,
"description": "# BirchRest\n\n[![PyPI version](https://badge.fury.io/py/birchrest.svg)](https://pypi.org/project/birchrest/)\n![GitHub Release Date](https://img.shields.io/github/release-date/alexandengstrom/birchrest)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n![GitHub issues](https://img.shields.io/github/issues/alexandengstrom/birchrest)\n![GitHub last commit](https://img.shields.io/github/last-commit/alexandengstrom/birchrest)\n![Unit Tests](https://github.com/alexandengstrom/birchrest/actions/workflows/unit_test.yml/badge.svg)\n![Type Checking](https://github.com/alexandengstrom/birchrest/actions/workflows/type_checking.yml/badge.svg)\n![Linting](https://github.com/alexandengstrom/birchrest/actions/workflows/linting.yml/badge.svg)\n[![codecov](https://codecov.io/gh/alexandengstrom/birchrest/branch/main/graph/badge.svg)](https://codecov.io/gh/alexandengstrom/birchrest)\n[![Downloads](https://img.shields.io/pypi/dm/birchrest)](https://pypi.org/project/birchrest/)\n[![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://alexandengstrom.github.io/birchrest/)\n![Repo Size](https://img.shields.io/github/repo-size/alexandengstrom/birchrest)\n\n**BirchRest** is a simple, lightweight framework for setting up RESTful APIs with minimal configuration. It is designed to be intuitive and flexible.\n\nFull documentation is available here:\nhttps://alexandengstrom.github.io/birchrest\n\n## Quickstart\n1. **Install**: You can install the latest version of birchrest using pip:\n ```bash\n pip install birchrest\n ```\n\n2. **Init**: Create a boilerplate project with ```birch init``` command:\n ```bash\n birch init\n ```\n\n3. **Start**: Start the server via command line:\n ```bash\n birch serve\n ```\n\n## Table of Contents\n## Table of Contents\n1. [Introduction](#introduction)\n2. [Defining Controllers](#defining-controllers)\n - [Key Concepts](#key-concepts)\n - [Defining Endpoints](#defining-endpoints)\n - [Nesting Controllers](#nesting-controllers)\n3. [Middleware](#middleware)\n - [Custom Middlewares](#custom-middlewares)\n - [Requirements](#requirements)\n - [Built-in Middlewares](#built-in-middlewares)\n - [Rate Limiter](#rate-limiter)\n - [Cors](#cors)\n4. [Data Validation](#data-validation)\n - [Body Validation](#body-validation)\n - [Query and URL Param Validation](#query-and-url-param-validation)\n - [Supported Validation Constraints](#supported-validation-constraints)\n - [Type Validation](#type-validation)\n - [String Constraints](#string-constraints)\n - [Numeric Constraints](#numeric-constraints)\n - [Optional Fields](#optional-fields)\n - [List Constraints](#list-constraints)\n - [Nested Dataclasses](#nested-dataclasses)\n5. [Authentication](#authentication)\n - [Custom Auth Handlers](#custom-auth-handlers)\n - [Protecting Routes](#protecting-routes)\n6. [Error Handling](#error-handling)\n - [ApiError](#apierror)\n - [Custom Error Handler](#custom-error-handler)\n7. [Unit Testing](#unit-testing)\n - [Test Adapter](#test-adapter)\n - [BirchRestTestCase](#birchresttestcase)\n8. [Requests And Responses](#requests-and-responses)\n - [Request](#request)\n - [Response](#response)\n - [Request and Response Lifecycle](#request-and-response-lifecycle)\n - [1. Receiving and Parsing the Request](#1-receiving-and-parsing-the-request)\n - [2. Passing the Request to the App](#2-passing-the-request-to-the-app)\n - [3. Handling the Request in the App](#3-handling-the-request-in-the-app)\n - [4. Route Execution](#4-route-execution)\n - [5. Returning the Response](#5-returning-the-response)\n9. [OpenAPI](#openapi)\n - [How It Works](#how-it-works-2)\n - [Automatic Response Code Detection](#automatic-response-code-detection)\n - [Customizing](#customizing-openapi-information)\n - [Generating](#generating-the-openapi-spec)\n10. [Contributing](#contributing)\n11. [License](#license)\n\n\n## Introduction\nBirchRest follows a controller-based architecture, where each controller represents a logical grouping of API routes. The framework automatically constructs your API at runtime from the controllers you define. To make this work, simply create a file named ```__birch__.py``` and import all your controllers into this file. BirchRest will use this file to discover and configure your API routes.\n```python\nfrom birchrest import Controller\nfrom birchrest.decorators import get, controller\nfrom birchrest.http import Request, Response\n\n@controller(\"api\")\nclass MyController(Controller):\n\n @get(\"hello\")\n async def hello(self, req: Request, res: Response):\n return res.send({\"message\": \"Hello from the app!\"})\n```\nTo start the server, instantiate the BirchRest class and call its serve method.\n```python\nfrom birchrest import BirchRest\n\napp = BirchRest()\napp.serve()\n```\n\nOr start the server via command line:\n```bash\nbirch serve --port [PORT] --host [HOST] --log-level [LOG_LEVEL]\n```\n## Defining Controllers\nIn Birchrest, controllers are the building blocks of your API. Each controller defines multiple endpoints, and controllers can be nested to create hierarchical routes.\n### Key Concepts\n- **Base Path**: Each controller has a base path that defines where its routes are accessible. If a controller has subcontrollers, their base paths are combined, creating a nested structure.\n\n- **Controller Setup**: To create a controller:\n 1. Inherit from the Controller class\n 2. Use the @controller decorator on the class, passing the base path as an argument.\n### Defining Endpoints\nInside a controller, use HTTP method decorators like @get or @post to define endpoints. These decorators can take an optional path to extend the controller\u2019s base path for that specific route.\n\n```python\n# Create an endpoint that accepts PATCH method on route /myendpoint.\n@patch(\"myendpoint\")\nasync def patch(self, req: Request, res: Response):\n print(req.body)\n return res.send({\"message\": \"success\"})\n```\n\nTo define path variables, use a colon (```:```) in the path. You can then access these variables through the ```req.params``` object.\n```python\n@get(\"user/:id\")\nasync def patch(self, req: Request, res: Response):\n userId = req.params.get(\"id\")\n return res.send({\"id\": userId})\n```\n\nA route can also access queries in the same way:\n```python\n@get(\"user\")\nasync def patch(self, req: Request, res: Response):\n name = req.queries.get(\"name\")\n return res.send({\"name\": name})\n```\n\nIt is possible to set automatic contraints for the body, queries and params via validation decorators. See section about validation.\n\nBirchRest is fully asynchronous, meaning all route handlers and middleware must be defined as async functions. This allows the framework to handle multiple requests concurrently without blocking. Ensure that all I/O-bound operations, such as database queries, file handling, or external API requests, are awaited properly. Failing to use async or forgetting to await asynchronous operations can lead to blocking behavior, defeating the purpose of using an asynchronous framework.\n\n### Nesting Controllers\nBirchRest supports hierarchical route structures by allowing controllers to inherit from other controllers. This creates nested routes where the child controller's base path is combined with the parent controller's base path. In BirchRest, subcontrollers are created by having one controller class inherit from another controller class.\n\nThis approach makes it easy to group related endpoints under a common path and manage them as a logical structure.\n\n#### Example\nLet\u2019s say we have a base API controller and we want to nest a resource controller under it:\n```python\nfrom birchrest import Controller\nfrom birchrest.decorators import get, controller\nfrom birchrest.http import Request, Response\n\n# Define the parent controller\n@controller(\"api\")\nclass BaseController(Controller):\n @get(\"status\")\n async def status(self, req: Request, res: Response):\n return res.send({\"message\": \"API is running\"})\n\n# Define the child controller that inherits from the parent\n@controller(\"resource\")\nclass ResourceController(BaseController):\n @get(\"hello\")\n async def hello(self, req: Request, res: Response):\n return res.send({\"message\": \"Hello from resource!\"})\n\n```\nThis will create the endpoint /api/resouce/hello.\n\nIn this example:\n\n- The ```BaseController``` is the parent controller that handles routes under ```/api```.\n- The ```ResourceController``` inherits from ```BaseController```, making it a child controller nested under ```/api/resource```.\n- The route for the \"hello\" endpoint in ```ResourceController``` becomes ```/api/resource/hello```.\n- The route for the \"status\" endpoint from ```BaseController``` is ```/api/status```.\n\nBy inheriting from BaseController, the ResourceController becomes a child, automatically inheriting and extending the parent\u2019s routing structure.\n\n## Middleware\nMiddleware allows you to perform tasks before or after a request is processed by a controller, such as logging, modifying the request, or checking permissions. Birchrest provides built-in middleware for common tasks and the ability to define your own custom middleware.\n\n### Custom Middlewares\nYou can create custom middleware to handle specific logic or modify request and response objects. This section explains how to define and register middleware in your application.\n\nMiddleware operates hierarchically, meaning it applies to all routes below the point where it\u2019s defined. You can set up global middleware directly at the application level, or use decorators on controllers and routes. When applied to a controller, the middleware will affect all routes within that controller, as well as any nested controllers attached to it. If applied to a route it will be applied only on that route.\n\n#### Requirements\nA middleware should be a class that inherits from the Middleware class and it must implement an async call method. The call method will receive a Request, Response and NextFunction. If the NextFunction is called the call will continue to the next middleware or route handler. If not called, we wont continue. The next function must be awaited.\n\n```python\nfrom birchrest.http import Request, Response, Middleware\nfrom birchrest.types import NextFunction\nfrom birchrest.exceptions import BadRequest\n\nclass MyMiddleware(Middleware):\n def __init__(self, state: int):\n self.state = state\n\n async def __call__(self, req: Request, res: Response, next: NextFunction):\n if self.state:\n await next()\n else:\n raise BadRequest\n```\n\nIt is possible to execute things after next is called aswell, this means you can use middlewares for postprocessing aswell. Just like route handlers, all middleware in BirchRest must be asynchronous.\n### Built-in Middlewares\nBirchrest comes with several built-in middleware options that help manage common use cases, such as request logging, rate limiting or CORS support. These can be easily added to your API with minimal configuration. These can be imported from the middlewares module.\n\n```python\nfrom birchrest.middlewares import Cors, Logger, RateLimiter\n```\n#### Rate Limiter\nThe RateLimiter middleware in BirchRest helps protect your API from abuse by limiting the number of requests a client (identified by their IP address or token) can make within a specified time window. It is particularly useful for preventing denial-of-service (DoS) attacks or enforcing fair usage limits.\n\n##### How it works:\n- The rate limiter tracks the number of requests made by each client within a rolling time window.\n- If a client exceeds the allowed number of requests within the window, the middleware responds with a ```429 Too Many Requests``` error.\n- Requests older than the current time window are automatically cleared from the log to allow new requests.\n##### Configuration Options:\n- ```max_requests```: The maximum number of requests a client can make within the time window (default is 2).\n- ```window_seconds```: The length of the time window in seconds during which the requests are counted (default is 10 seconds).\n##### Example:\n```python\nfrom birchrest.middlewares import RateLimiter\n\n# Apply rate limiting globally\napp.middleware(RateLimiter(max_requests=5, window_seconds=60))\n```\nIn this example, the middleware limits each client to a maximum of 5 requests per 60 seconds. If the limit is exceeded, any additional requests within that time will receive a ```429``` error response.\n#### Cors\nThe CORS (```Cross-Origin Resource Sharing```) middleware in BirchRest enables your API to respond to cross-origin requests securely by controlling which origins, methods, and headers are allowed. It also handles preflight (```OPTIONS```) requests for methods other than ```GET``` and ```POST```, or when using custom headers.\n##### How It Works:\n- The middleware inspects each request and adds the necessary CORS headers to the response based on the configured settings. This allows browsers to enforce the CORS policy and determine if the request is permitted.\n- For preflight requests (```OPTIONS``` method), it sends the appropriate response headers to indicate which origins, methods, and headers are allowed.\n- For regular requests, it ensures the appropriate headers are added to allow cross-origin resource sharing.\n##### Configuration Options:\n- ```allow_origins```: List of allowed origins (default is [\"*\"], allowing all origins).\n- ```allow_methods```: List of allowed HTTP methods (default includes GET, POST, PUT, DELETE, PATCH, OPTIONS).\n- ```allow_headers```: List of allowed request headers (default is [\"Content-Type\", \"Authorization\"]).\n- ```allow_credentials```: Whether credentials (cookies, HTTP authentication, etc.) are allowed (default is False).\n- ```max_age```: The time (in seconds) that preflight request results can be cached by the browser (default is 86400 seconds or 24 hours).\n##### Example:\n```python\nfrom birchrest.middlewares import Cors\n\n# Apply CORS middleware globally with default settings\napp.middleware(Cors(allow_origins=[\"https://example.com\"], allow_credentials=True))\n```\nIn this example, only requests from https://example.com are allowed, and credentials (like cookies) are permitted to be sent with cross-origin requests. The middleware ensures that the appropriate CORS headers are added to all responses.\n## Data Validation\nData validation in Birchrest is supported via Python data classes. This allows for strict validation of request data (body, queries, and params) to ensure that all incoming data adheres to the expected structure.\n\nTo be able to use validation, you must also define the models. \n### Body Validation\n#### Example:\n```python\n@dataclass\nclass Address:\n street: str = field(metadata={\"min_length\": 3, \"max_length\": 100})\n city: str = field(metadata={\"min_length\": 2, \"max_length\": 50})\n\n@dataclass\nclass User:\n username: str = field(metadata={\"min_length\": 3, \"max_length\": 20})\n email: str = field(metadata={\"regex\": r\"^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$\"})\n age: int = field(metadata={\"min_value\": 0, \"max_value\": 120})\n address: Address\n```\n\nYou can then use the @body, @queries and @params decorator with the dataclass as argument.\n\nExample:\n```python\n@post(\"user\")\n@body(User)\nasync def create_user(self, req: Request, res: Response):\n # It is safe to pass the body directly since we have already validated it.\n save_to_database(request.body)\n return res.status(201).send()\n```\nIf the validation fails, the user will get an automatic response. For example, if we try to post a user to the route above but passes a username with only two letters. We will receive this response:\n```json\n{\n \"error\": {\n \"status\": 400,\n \"code\": \"Bad Request\",\n \"correlationId\": \"67ad2218-262e-478b-b767-04cfafd4315b\",\n \"message\": \"Body validation failed: Field 'username' must have at least 3 characters.\"\n }\n}\n```\n\nRead more about how automatic error responses are handled in the error section.\n\n### Query and URL Param Validation\nValidating queries and params is done in the same way, just use the @queries and @params decorators instead.\n\n### Supported Validation Constraints\nBelow is a list of all the validation constraints you can define using ```field(metadata={...})```:\n1. #### Type Validation: \n Data is automatically validated against the field's type. Supported types include int, float, str, list, and nested dataclasses.\n Example:\n\n ```python\n @dataclass\n class User:\n age: int\n ```\n2. #### String Constraints:\n\n - ```min_length```: Ensures that the string has at least a certain number of characters.\n - ```max_length```: Ensures that the string does not exceed a certain number of characters.\n - ```regex```: Ensures that the string matches a given regular expression.\n - Example:\n ```python\n @dataclass\n class User:\n username: str = field(metadata={\"min_length\": 3, \"max_length\": 20})\n email: str = field(metadata={\"regex\": r\"[^@]+@[^@]+\\.[^@]+\"})\n\n ```\n3. #### Numeric Constraints:\n - ```min_value```: Ensures that the number is at least a certain value.\n - ```max_value```: Ensures that the number does not exceed a certain value.\n - ```Example```:\n ```python\n @dataclass\n class User:\n age: int = field(metadata={\"min_value\": 18, \"max_value\": 120})\n\n ```\n4. #### Optional Fields:\n - Fields can be marked as optional by specifying ```is_optional: True``` in the metadata. This allows a field to be omitted from the input data without causing a validation error.\n - Example:\n ```python\n @dataclass\n class User:\n age: Optional[int] = field(metadata={\"is_optional\": True})\n phone: Optional[str] = field(metadata={\"is_optional\": True, \"regex\": r\"^\\d{10}$\"})\n\n ```\n5. #### List Constraints:\n - ```min_items```: Ensures that a list has at least a certain number of items.\n - ```max_items```: Ensures that a list does not exceed a certain number of items.\n - ```unique```: Ensures that all items in the list are unique.\n - You can also nest dataclasses inside lists and apply validation to each item.\n - Example:\n ```python\n @dataclass\n class Address:\n street: str = field(metadata={\"min_length\": 5, \"max_length\": 100})\n\n @dataclass\n class User:\n addresses: List[Address] = field(metadata={\"min_items\": 1, \"max_items\": 3})\n\n ```\n6. #### Nested Dataclasses:\n - You can nest dataclasses inside each other, and BirchRest will automatically validate nested structures.\n - Example:\n ```python\n @dataclass\n class ContactInfo:\n email: str = field(metadata={\"regex\": r\"[^@]+@[^@]+\\.[^@]+\"})\n\n @dataclass\n class User:\n username: str = field(metadata={\"min_length\": 3, \"max_length\": 20})\n contact_info: ContactInfo\n ```\n## Authentication\nBirchrest makes it easy to protect your API routes with authentication mechanisms. It allows you to define custom authentication handlers and easily mark routes as protected, ensuring that only authenticated requests are allowed access.\n### Custom Auth Handlers\nYou can define your own authentication handler to manage how users are authenticated in your system. Once defined, Birchrest will handle the integration with the API. If your route handler returns a falsy value or raises an Exception, the execution will be stopped. Otherwise the return value from this function will be put under the user property in the request object. It is therefore possible to put information there that tells you which user sent a request.\n### Protecting Routes\nYou can easily protect individual routes or entire controllers by marking them as requiring authentication. Birchrest will automatically handle unauthorized access by returning predefined error messages.\n\n```python\nfrom birchrest import BirchRest, Controller\nfrom birchrest.decorators import get, controller\nfrom birchrest.http import Request, Response\n\nasync def auth_handler(req: Request, res: Response):\n if req.headers.get(\"Authorization\"):\n # Do your logic\n return { \"id\": 1 }\n \n return False\n\n@controller(\"api\")\nclass MyController(Controller):\n\n @protected()\n @get(\"protected\")\n async def hello(self, req: Request, res: Response):\n return res.send({\"message\": \"Hello from the app!\"})\n\napp = BirchRest()\napp.register(MyController)\napp.serve()\n\n```\n\n## Error Handling\nBy default, BirchRest responds with standardized error messages and provides as much detail as possible when an error occurs. Common error responses like 404 (Not Found) when a route doesn't exist, or 400 (Bad Request) when body validation fails, are handled automatically. If an unhandled exception occurs within your controllers, a 500 Internal Server Error will be returned.\n\n### ApiError\nThe **ApiError** class is the base class for a variety of HTTP exceptions such as NotFound, BadRequest, Unauthorized, and more. If any of these exceptions are raised during request handling, BirchRest will automatically convert them into the appropriate HTTP response with the correct status code and error message.\n```python\nfrom birchrest.exceptions import NotFound\n\nraise NotFound\n```\nThis will automatically generate a 404 Not Found HTTP response to the client, with the provided user-friendly message.\n\nEach ApiError has the following attributes:\n\n- ```status_code```: The HTTP status code (e.g., 404, 400, 500).\n- ```base_message```: A default message associated with the status code (e.g., \"Not Found\" for 404).\n- ```user_message```: An optional custom message that can provide more specific details about the error.\n\nBirchRest supports the following common HTTP exceptions out-of-the-box:\n- ```BadRequest``` (400)\n- ```Unauthorized``` (401)\n- ```Forbidden``` (403)\n- ```NotFound``` (404)\n- ```MethodNotAllowed``` (405)\n- ```Conflict``` (409)\n- ```UnprocessableEntity``` (422)\n- ```InternalServerError``` (500)\n- ```ServiceUnavailable``` (503)\n\n- ```PaymentRequired``` (402)\n- ```RequestTimeout``` (408)\n- ```Gone``` (410)\n- ```LengthRequired``` (411)\n- ```PreconditionFailed``` (412)\n- ```PayloadTooLarge``` (413)\n- ```UnsupportedMediaType``` (415)\n- ```TooManyRequests``` (429)\n- ```UpgradeRequired``` (426)\n\nThe framework handles everything behind the scenes if any of these exceptions are raised. You don't need to manually craft the response or worry about setting the correct status code\u2014BirchRest takes care of it.\n\n### Custom Error Handler\nIf you need more control over how errors are handled, you can define your own custom error handler. This handler will receive the request, response, and exception as arguments. The handler must manage the exception explicitly; otherwise, a ```500 Internal Server Error``` will be returned by default.\n\n#### Example:\n```python\nfrom birchrest.http import Request, Response\nfrom birchrest.exceptions import ApiError\n\nasync def error_handler(req: Request, res: Response, e: Exception) -> Response:\n if isinstance(e, ApiError):\n # If it an ApiError, use the build in converter if you want\n return e.convert_to_response(res)\n\n # Do your own error handling here...\n return res.status(500).send({\"error\": \"This was not supposed to happen....\"})\n```\n## Unit Testing\n### Test Adapter\nTo simplify testing, the framework includes a test adapter class that simulates sending HTTP requests to your API. This allows you to test everything except the server itself, with all middlewares, authentication handlers, and other components functioning exactly as they would in a real request. The adapter returns the final response object, which you can inspect and assert in your tests.\n\nThe TestAdapter class takes an instance of your app and then provides methods like get, post etc that accepts a path, headers and body.\n\n#### Example\n```python\nfrom birchrest import BirchRest\nfrom birchrest.unittest import TestAdapter\n\napp = BirchRest()\nrunner = TestAdapter(app)\n\nresponse = runner.get(\"/your-route\")\n```\n\n### BirchRestTestCase\nBirchRest also provides a custom TestCase class (BirchRestTestCase) that includes helper methods to make it easier to assert HTTP responses. These methods help ensure that your API responds as expected. Below is a list of the available assertion methods and their descriptions:\n\n- ```assertOk(response)```: Asserts that the response status code is in the range of 2xx, indicating a successful request.\n\n- ```assertNotOk(response)```: Asserts that the response status code is not in the range of 2xx, indicating a failure.\n\n- ```assertBadRequest(response)```: Asserts that the response status code is 400, indicating a Bad Request.\n\n- ```assertNotFound(response)```: Asserts that the response status code is 404, indicating a resource was not found.\n\n- ```assertUnauthorized(response)```: Asserts that the response status code is 401, indicating an Unauthorized request.\n\n- ```assertForbidden(response)```: Asserts that the response status code is 403, indicating a Forbidden request.\n\n- ```assertInternalServerError(response)```: Asserts that the response status code is 500, indicating an Internal Server Error.\n\n- ```assertStatus(response, expected_status)```: Asserts that the response status code matches the expected_status.\n\n- ```assertHasHeader(response, expected_key)```: Asserts that the response contains a specific header.\n\n- ```assertHeader(response, header_name, expected_value)```: Asserts that a specific header in the response matches the expected value.\n\n- ```assertRedirect(response, expected_url)```: Asserts that the response status is a redirect (3xx) and that the Location header matches the expected URL.\n\n- ```assertBodyContains(response, expected_key)``: Asserts that the response body contains a specific property or key.\n\n#### Example:\n```python\nimport unittest\n\nfrom birchrest import BirchRest\nfrom birchrest.unittest import TestAdapter, BirchRestTestCase\n\nclass ApiTest(BirchRestTestCase):\n \n def setUp(self) -> None:\n app = BirchRest(log_level=\"test\")\n self.runner = TestAdapter(app)\n \n async def test_user_route(self) -> None:\n response = await self.runner.get(\"/user\")\n self.assertOk(response)\n \n async def test_invalid_id(self) -> None:\n response = await self.runner.get(\"/user/0\")\n self.assertNotOk(response)\n self.assertStatus(response, 400)\n \n \nif __name__ == \"__main__\":\n unittest.main()\n```\n\n## Requests And Responses\n\n### Request\nThe Request object represents the incoming HTTP request. It encapsulates all relevant information about the request, including the HTTP method, URL path, headers, body, query parameters, and more. The Request object provides access to these components, allowing you to handle and process the request effectively.\nProperties:\n\n- ```method: str```\n The HTTP method used for the request (e.g., GET, POST, PUT, DELETE).\n\n- ```path: str```\n The full requested URL path, including any query string (e.g., /api/users?id=123).\n\n- ```clean_path: str```\n The URL path without query parameters (e.g., /api/users).\n\n- ```version: str```\n The HTTP version used for the request (e.g., HTTP/1.1).\n\n- ```headers: Dict[str, str]```\n A dictionary of all HTTP request headers. Headers are case-insensitive.\n\n Example:\n\n ```python\n content_type = req.headers.get(\"content-type\")\n ```\n\n- ```queries: dataclass```\nA dataclass of query parameters extracted from the URL. If a query parameter has multiple values, it will be a list. Otherwise, it will be a single string. If you have used the ```@queries``` decorator it will be your custom dataclass, otherwhise the request would have failed.\n\n Example:\n\n ```python\n search_query = req.queries.name\n ```\n\n- ```params: dataclass```\nA dataclass of URL path parameters (if any) extracted from the route. This is populated during route matching. You can also define this via the ```@params``` decorator.\n\n Example:\n\n ```python\n user_id = req.params.id\n ```\n\n- ```body: dataclass```\nThe parsed body of the request. You can ensure this follows your exact dataclass model by using the ```@body``` decorator.\n\n Example:\n\n ```python\n user_data = req.body.user.name.firstName\n ```\n\n- ```client_address: str```\nThe IP address of the client making the request.\n\n- ```client_port: Optional[int]```\nThe port number used by the client to send the request. This is helpful for identifying the source of the request.\n\n Example:\n\n ```python\n print(f\"Request received from {req.client_address}:{req.client_port}\")\n ```\n\n- ```correlation_id: str```\nA unique ID automatically generated for each request. This can be used to track and correlate requests across different systems.\n\n- ```received: datetime```\nThe timestamp indicating when the request was received by the server. This can be useful for logging and performance tracking.\n\n Example:\n\n ```python\n print(f\"Request received at {req.received}\")\n ```\n\n- ```user: Optional[Any]```\nPlaceholder for any authenticated user information. This will be populated if the request goes through an authentication handler.\n\n- ```host: Optional[str]```\nThe Host header from the request, which indicates the server's host (domain or IP) to which the request was made.\n\n Example:\n\n ```python\n print(f\"Host: {req.host}\")\n ```\n- ```referrer: Optional[str]```\nThe Referer header (if present) from the request, which indicates the page from which the request originated.\n\n- ```user_agent: Optional[str]```\nThe User-Agent header from the request, which identifies the client software (browser, bot, etc.).\n\n### Response\nThe Response object in BirchRest is responsible for crafting the outgoing HTTP response that is sent back to the client. It provides methods to set the status code, headers, and body, with support for automatically sending JSON-encoded responses.\n\n#### Important Methods\n\n- ```status(code: int) -> Response```\n This method sets the HTTP status code for the response. You can either pass an integer representing the status code (e.g., 200 for OK or 404 for Not Found) or use the HttpStatus enum for predefined status codes. However, it is recommended to raise ```ApiError``` instead if you want to return errors. A good use would be when responding with 201. The status code is set to 200 by default, so if you intend to return that, you dont have to call status method.\n\n Example:\n ```python\n res.status(404)\n ```\n\n- ```send(data: Any = {}) -> Response```\nThis method sets the response body by encoding the provided data as JSON and automatically sets the Content-Type header to application/json. The response can only be sent once; attempting to call send a second time will raise an exception.\n\n *Note: All responses in BirchRest are automatically sent as JSON.*\n\n Example:\n ```python\n res.status(200).send({\"message\": \"Success\"}) # Sends a JSON response with a 200 status code\n ```\n\n **Important:**\n - Once send is called, the response is finalized, and calling send again will result in an error (\"Request was sent twice\").\n - The Content-Length header is automatically set based on the length of the JSON-encoded response.\n### Request and Response Lifecycle\nThe BirchRest framework handles HTTP requests using a structured flow to ensure that all incoming requests are processed correctly, including middleware execution, validation, and error handling. This section explains the lifecycle of a request from when it is received by the server to when a response is sent back to the client.\n\n#### 1. Receiving and Parsing the Request\nWhen a client sends an HTTP request to the server, the server parses the raw request data into a Request object. This object encapsulates all details about the incoming request, such as headers, method (e.g., GET, POST), query parameters, URL parameters, and body data.\n#### 2. Passing the Request to the App\nOnce the request object is created, it is passed to the main application (BirchRest) for handling. The app creates a new Response object, which will later be populated and returned to the client. The app then looks for a matching route by searching through all defined routes based on the request\u2019s URL and HTTP method.\n#### 3. Handling the Request in the App\nThe main request handling logic is performed by the handle_request method in the app. This method attempts to match the incoming request to a route and execute the following key steps:\n- **Route Matching**: The app searches through all registered routes to find one that matches the URL path and HTTP method of the request. If a matching route is found, the request proceeds to that route. If no route matches, a ```404 Not Found``` error is raised, or if the route exists but the HTTP method is incorrect, a ```405 Method Not Allowed``` error is raised.\n- **Passing the Request to the Route**: Once a route is matched, the app passes both the request and response objects to that route for further processing.\n- **Error Handling**: If an exception occurs during request handling (such as an invalid request or missing route), the app catches the exception and attempts to generate an appropriate error response using predefined or custom error handlers.\n#### 4. Route Execution\nEach route in BirchRest is responsible for executing its logic and handling the request:\n- **Middleware Execution**: When the request reaches the matched route, the route begins by executing any middleware associated with it. Middleware can modify the request or response objects, perform tasks such as logging or authentication, and decide whether to continue processing the request. Middleware runs in a chain, meaning each middleware can pass control to the next one, or halt the chain and send a response early.\n\n The route's ```__call__``` method initiates this process by calling the first middleware in the stack. If no middleware interrupts the chain, the request proceeds to the route handler.\n- **Authentication**: If the route is protected by authentication, the request must pass through an authentication handler. This handler validates the request (e.g., checking tokens or credentials). If authentication fails, a ```401 Unauthorized``` error is raised, and the response is sent back to the client.\n- **Validation**: If the route requires validation of the request body, query parameters, or URL parameters, the request data is checked against predefined data classes. If the data fails validation (e.g., missing required fields or incorrect types), a 400 Bad Request error is raised.\n- **Executing the Route Handler**: Once middleware, authentication, and validation checks pass, the route handler function is executed. The route handler is responsible for performing the main business logic, such as fetching data, processing the request, or interacting with external services. After processing, the route handler populates the response object with the appropriate data and status code.\n#### 5. Returning the Response\nAfter the route handler completes, the response object (which was initially created at the beginning of the request) contains the data to be sent back to the client. This includes the HTTP status code, headers, and response body.\n\nThe final response is then returned to the server, which sends it to the client. If any errors occurred during the request lifecycle, they are automatically converted into error responses by the app\u2019s error handler.\n## OpenAPI\nOne of the features of the framework is its ability to automatically generate OpenAPI specifications based on the defined routes, parameters, and decorators. This process integrates with the core functionality of the framework to ensure that your API documentation is always up-to-date without requiring manual intervention.\n\n### How It Works\n\nThe framework reuses as much information as possible to define the structure of your API, which are then converted into OpenAPI schemas:\n\n- **`@body`**, **`@queries`**, and **`@params`**: These decorators define the request body, query parameters, and path parameters respectively. The framework converts these into OpenAPI-compliant schemas, ensuring the input structure is well-documented.\n \n- **`@produces`**: This decorator is used to specify the return type of a route, generating the corresponding schema for the response object. Note that while `@produces` defines the schema for the OpenAPI specification, it does not affect the runtime logic of the route itself.\n\n- **`@tag`**: The `@tag` decorator allows you to attach tags to routes or controllers. These tags are used purely for documentation purposes in the OpenAPI spec, helping to categorize and organize your API endpoints.\n\n- **`docstrings`**: The python docstrings for your route handlers will be reused to describe your endpoints.\n\n### Automatic Response Code Detection\n\nThe framework analyzes the entire flow of a route to detect what HTTP status codes can be returned. By inspecting the **Abstract Syntax Tree (AST)** of the route handler, auth handler, middlewares, the framework can identify the status codes specified in the code, whether they are success or error codes. This process includes analyzing any exceptions (such as `ApiError`) raised within the route or its middlewares to ensure that all possible responses are captured.\n\n### Customizing OpenAPI Information\n\nIn addition to automatically generating endpoint definitions, the framework allows you to specify additional metadata about your API. You can define information such as license, terms of service, and contact details by adding these fields to the `__birch__.py` file in your project. This information will be reflected in the OpenAPI specification's `info` section, making it easy to provide consumers with full details about your API.\n\n### Generating the OpenAPI Spec\n\nTo generate the OpenAPI specification, simply run the following command:\n\n```bash\nbirch openapi\n```\n\nIt will create a file named openapi.json. It is possible to override the filename by providing the flag `--filename`.\n\n```bash\nbirch openapi --filename myspec.json\n```\n## Contributing\nContributions are welcome! Please refer to the [CONTRIBUTING.md](./CONTRIBUTING.md) file for details on how to get involved, submit pull requests, and report issues.\n\n## License\nThis project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.\n\n\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A lightweight Python framework for building REST APIs with a controller-based approach. It includes built-in middleware, error handling, auth handling, automatic response management and more.",
"version": "1.1.0",
"project_urls": {
"changelog": "https://github.com/alexandengstrom/birchrest/releases",
"documentation": "https://alexandengstrom.github.io/birchrest",
"homepage": "https://alexandengstrom.github.io/birchrest",
"issues": "https://github.com/alexandengstrom/birchrest/issues",
"repository": "https://github.com/alexandengstrom/birchrest"
},
"split_keywords": [
"api",
" rest",
" framework",
" web",
" microservices",
" controller",
" middleware",
" auth management",
" response handling",
" fastapi alternative",
" flask alternative",
" restful api",
" python web framework"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "13dba6c767bbd16b31f0d07946a91edebe3e56c4b5d327e60702f877fdd6ab4c",
"md5": "a4667c8e665409219529c6d3188c47d6",
"sha256": "0e569dd51094c1938ab87f76aae958a9d4a48a6a1e15895513919057dbc73594"
},
"downloads": -1,
"filename": "birchrest-1.1.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "a4667c8e665409219529c6d3188c47d6",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 64667,
"upload_time": "2024-10-16T19:54:29",
"upload_time_iso_8601": "2024-10-16T19:54:29.789419Z",
"url": "https://files.pythonhosted.org/packages/13/db/a6c767bbd16b31f0d07946a91edebe3e56c4b5d327e60702f877fdd6ab4c/birchrest-1.1.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "5c609877f3cdace3ca7ddac0f90adf7fa8bc594121a97d1a7445b1e7addd8e19",
"md5": "a9c4dba6cb959f20a0edca03e84a5a53",
"sha256": "050f5813fdaaad33d7fc2cd6d438664f0e2057bb3fdcb06962262339092b8cf7"
},
"downloads": -1,
"filename": "birchrest-1.1.0.tar.gz",
"has_sig": false,
"md5_digest": "a9c4dba6cb959f20a0edca03e84a5a53",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 70999,
"upload_time": "2024-10-16T19:54:30",
"upload_time_iso_8601": "2024-10-16T19:54:30.868225Z",
"url": "https://files.pythonhosted.org/packages/5c/60/9877f3cdace3ca7ddac0f90adf7fa8bc594121a97d1a7445b1e7addd8e19/birchrest-1.1.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-10-16 19:54:30",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "alexandengstrom",
"github_project": "birchrest",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"requirements": [],
"lcname": "birchrest"
}