rAPIdy - write quickly - write beautifully
Project description
rAPIdy
- is a minimalist web framework for those who prioritize speed and convenience.
Built upon aiohttp and pydantic,
allowing you to fully leverage the advantages of this stack.
Key features
- ✏️ Minimalism: Retrieve and check data effortlessly using just a single line of code
- 🐍 Native Python Support: Offers seamless compatibility with Python native types
- 📔 Pydantic Integration: Fully integrated with pydantic for robust data validation
- 🚀 Powered by aiohttp: Utilizes aiohttp to leverage its powerful asynchronous features
- 📤 Efficient Data Handling: Simplifies the extraction of basic types from incoming data in Python
Documentation
[!TIP] Coming soon: 2024.06
Installation
[!NOTE]
pip install rapidy
Server
Quickstart
Handlers
from rapidy import web
routes = web.RouteTableDef()
@routes.post('/')
async def handler(
auth_token: str = web.Header(alias='Authorization'),
username: str = web.JsonBody(),
password: str = web.JsonBody(min_length=8),
) -> web.Response:
print({'auth_token': auth_token, 'username': username, 'password': password})
return web.json_response({'data': 'success'})
app = web.Application()
app.add_routes(routes)
if __name__ == '__main__':
web.run_app(app, host='127.0.0.1', port=8080)
[!NOTE]
rAPIdy
allows you to define handlers just like in aiohttp-quickstart, but with a key improvement: therequest
parameter is no longer required for functional handlersIf you need to access the current
request
within a handler, simply declare an attribute with any name of your choosing and specify its type asweb.Request
. rAPIdy will automatically replace this attribute with the current instance ofweb.Request
.
from typing import Annotated
from rapidy import web
routes = web.RouteTableDef()
@routes.get('/default_handler_example')
async def default_handler_example(
request: str = web.Request,
) -> web.Response:
print({'request': request})
return web.json_response({'data': 'success'})
@routes.get('/handler_without_request_example')
async def handler_without_request_example() -> web.Response:
return web.json_response({'data': 'success'})
@routes.get('/handler_request_as_snd_attr_example')
async def handler_request_as_snd_attr_example(
host: Annotated[str, web.Header(alias='Host')],
request: web.Request,
) -> web.Response:
print({'host': host, 'request': request})
return web.json_response({'data': 'success'})
app = web.Application()
app.add_routes(routes)
if __name__ == '__main__':
web.run_app(app, host='127.0.0.1', port=8080)
Middlewares
Processing an Authorization Token in Middleware
from rapidy import web
from rapidy.typedefs import HandlerType
@web.middleware
async def hello_middleware(
request: web.Request,
handler: HandlerType,
bearer_token: str = web.Header(alias='Authorization'),
) -> web.StreamResponse:
request['token'] = bearer_token
return await handler(request)
async def handler(
request: web.Request,
host: str = web.Header(alias='Host'),
username: str = web.JsonBody(),
) -> web.Response:
example_data = {'token': request['token'], 'host': host, 'username': username}
return web.json_response(example_data)
app = web.Application(middlewares=[hello_middleware])
app.add_routes([web.post('/', handler)])
if __name__ == '__main__':
web.run_app(app, host='127.0.0.1', port=8080)
[!IMPORTANT] The first two attributes in a middleware are mandatory and must always represent the
request
and thehandler
respectively. These attributes are essential for the correct functioning of the middleware
Request validation
rAPIdy
utilizes custom parameters to validate incoming HTTP request data, ensuring integrity and compliance with expected formats
[!TIP] A parameter in
rAPIdy
represents an object that mcontains meta-information about the type of data it retrieves
A parameter in rAPIdy
offers all the functionalities of pydantic.Field
, and even more. This means rAPIdy-parameter
support every type of validation that pydantic
provides, ensuring comprehensive data integrity and conformity
from decimal import Decimal
from pydantic import BaseModel, Field
from rapidy import web
routes = web.RouteTableDef()
class Schema(BaseModel):
positive: int = Field(gt=0)
non_negative: int = Field(ge=0)
negative: int = Field(lt=0)
non_positive: int = Field(le=0)
even: int = Field(multiple_of=2)
love_for_pydantic: float = Field(allow_inf_nan=True)
short: str = Field(min_length=3)
long: str = Field(max_length=10)
regex: str = Field(pattern=r'^\d*$')
precise: Decimal = Field(max_digits=5, decimal_places=2)
@routes.get('/')
async def handler(
positive: int = web.JsonBody(gt=0),
non_negative: int = web.JsonBody(ge=0),
negative: int = web.JsonBody(lt=0),
non_positive: int = web.JsonBody(le=0),
even: int = web.JsonBody(multiple_of=2),
love_for_pydantic: float = web.JsonBody(allow_inf_nan=True),
short: str = web.JsonBody(min_length=3),
long: str = web.JsonBody(max_length=10),
regex: str = web.JsonBody(pattern=r'^\d*$'),
precise: Decimal = web.JsonBody(max_digits=5, decimal_places=2),
) -> web.Response:
return web.Response()
@routes.get('/schema')
async def handler_schema(
body: Schema = web.JsonBodySchema(),
) -> web.Response:
return web.Response()
app = web.Application()
app.add_routes(routes)
if __name__ == '__main__':
web.run_app(app, host='127.0.0.1', port=8080)
Validation Example
from rapidy import web
from pydantic import BaseModel, Field
routes = web.RouteTableDef()
class BodyRequestSchema(BaseModel):
username: str = Field(min_length=3, max_length=20)
password: str = Field(min_length=8, max_length=40)
@routes.post('/api/{user_id}')
async def handler(
user_id: str = web.Path(),
host: str = web.Header(alias='Host'),
body: BodyRequestSchema = web.JsonBodySchema(),
) -> web.Response:
return web.json_response({'data': 'success'})
app = web.Application()
app.add_routes(routes)
if __name__ == '__main__':
web.run_app(app, host='127.0.0.1', port=8080)
Success request validation
curl -X POST \
-H "Content-Type: application/json" -d '{"username": "User", "password": "myAwesomePass"}' -v http://127.0.0.1:8080/api/1
< HTTP/1.1 200 OK ... {"data": "success"}
Failed request validation
curl -X POST \
-H "Content-Type: application/json" -d '{"username": "U", "password": "m"}' -v http://127.0.0.1:8080/api/1
< HTTP/1.1 422 Unprocessable Entity ...
{
"errors": [
{
"loc": ["body", "username"],
"type": "string_too_short",
"msg": "String should have at least 3 characters",
"ctx": {"min_length": 3}
},
{
"type": "string_too_short",
"loc": ["body", "password"],
"msg": "String should have at least 8 characters",
"ctx": {"min_length": 8}
}
]
}
Types of request parameters
rAPIdy
supports 3 basic types for defining incoming parameters:
- Param
- Schema
- Raw data
Single parameter
Single
parameter, used when you need to spot-retrieve incoming data.
from rapidy import web
async def handler(
path_param: str = web.Path(),
# headers
host: str = web.Header(alias='Host'),
user_agent: str = web.Header(alias='User-Agent'),
# cookie
user_cookie1: str = web.Cookie(alias='UserCookie1'),
user_cookie2: str = web.Cookie(alias='UserCookie2'),
# query params
user_param1: str = web.Query(alias='UserQueryParam1'),
user_param2: str = web.Cookie(alias='UserQueryParam2'),
# body
username: str = web.JsonBody(min_length=3, max_length=20),
password: str = web.JsonBody(min_length=8, max_length=40),
) -> web.Response:
# write your code here
# ...
return web.Response()
app = web.Application()
app.add_routes([web.post('/api/{path_param}', handler)])
[!NOTE] All single parameters
- Path
- Header
- Cookie
- Query
- BodyJson
- FormDataBody
- MultipartBody
Schema
Schema-parameter
is useful when you want to extract a large amount of data.
from rapidy import web
from pydantic import BaseModel, Field
class PathRequestSchema(BaseModel):
path_param: str
class HeaderRequestSchema(BaseModel):
host: str = Field(alias='Host')
user_agent: str = Field(alias='User-Agent')
class CookieRequestSchema(BaseModel):
user_cookie1: str = Field(alias='UserCookie1')
user_cookie2: str = Field(alias='UserCookie2')
class QueryRequestSchema(BaseModel):
user_cookie1: str = Field(alias='UserQueryParam1')
user_cookie2: str = Field(alias='UserQueryParam1')
class BodyRequestSchema(BaseModel):
username: str = Field(min_length=3, max_length=20)
password: str = Field(min_length=8, max_length=40)
async def handler(
path: PathRequestSchema = web.PathSchema(),
headers: HeaderRequestSchema = web.HeaderSchema(),
cookies: CookieRequestSchema = web.Cookie(),
query: QueryRequestSchema = web.QuerySchema(),
body: BodyRequestSchema = web.JsonBodySchema(),
) -> web.Response:
# write your code here
# ...
return web.Response()
app = web.Application()
app.add_routes([web.post('/api/{path_param}', handler)])
[!NOTE] All schema parameters
- PathSchema
- HeaderSchema
- CookieSchema
- QuerySchema
- BodyJsonSchema
- FormDataBodySchema
- MultipartBodySchema
Raw
Use Raw-parameter
when you don't need validation.
from typing import Any
from rapidy import web
async def handler(
path: dict[str, str] = web.PathRaw,
headers: dict[str, str] = web.HeaderRaw,
cookies: dict[str, str] = web.CookieRaw,
query: dict[str, str] = web.QueryRaw,
body: dict[str, Any] = web.JsonBodyRaw,
) -> web.Response:
# write your code here
# ...
return web.Response()
app = web.Application()
app.add_routes([web.post('/api/{path_param}', handler)])
[!NOTE] All raw parameters
- PathRaw -
dict[str, str]
- HeaderRaw -
dict[str, str]
- CookieRaw -
dict[str, str]
- QueryRaw -
dict[str, str]
- BodyJsonRaw -
dict[str, Any]
- FormDataBodyRaw -
dict[str, str]
ordict[str, list[str]]
- MultipartBodyRaw -
dict[str, Any]
ordict[str, list[Any]]
- TextBody -
str
- BytesBody -
bytes
- StreamBody -
aiohttp.streams.StreamReader
Combining Different Approaches
async def handler(
path_param: str = web.Path(),
headers: dict[str, str] = web.HeaderRaw(),
body: BodyRequestSchema = web.JsonBodySchema(),
) -> web.Response:
Ways to define metadata for a query parameter
There are a total of two ways to define query parameters.
- Using the Annotated Auxiliary Type: Specify metadata directly within the Annotated type
- Using the Default Attribute Value: Define the query parameter's default value along with its metadata
from typing import Annotated # use typing_extensions if py version == 3.8
from rapidy import web
async def handler(
param_1: Annotated[str, web.JsonBody()], # Annotated definition
param_2: str = web.JsonBody(), # Default definition
) -> web.Response:
Special attributes
Some rAPIdy-parameters
include additional attributes that are not found in Pydantic
, offering extended functionality beyond standard data validation
Body
Currently, only parameters of the body type include special attributes in rAPIdy
[!WARNING] Additional attributes can only be specified for
Schema-parameters
andRaw-parameters
.
In rAPIdy
, the body_max_size
attribute associated with each body parameter restricts the maximum allowable size of the request body
for a specific handler
body_max_size
(int) - indicating the maximum number of bytes the handler expects.
async def handler(
body: str = web.JsonBodySchema(body_max_size=10),
) -> web.Response:
async def handler(
body: str = web.JsonBodyRaw(body_max_size=10),
) -> web.Response:
Json
json_decoder
(typing.Callable[[], Any]) - attribute that accepts the function to be called when decoding the body of the incoming request.
FormData
attrs_case_sensitive
(bool) - attribute that tells the data extractor whether the incoming key register should be considered.
duplicated_attrs_parse_as_array
(bool) - attribute that tells the data extractor what to do with duplicated keys in a query.
If duplicated_attrs_parse_as_array=True, a list will be created for each key and all values will be placed in it.
[!NOTE]
duplicated_attrs_parse_as_array
flat changes the type of data that the data extractor returns.If duplicated_attrs_parse_as_array=
True
, then the data will always be of type dict[str, list[str]] (by default,formdata
has the extractable type dict[str, str])
Multipart
attrs_case_sensitive
(bool) - attribute that tells the data extractor whether the incoming key register should be considered.
duplicated_attrs_parse_as_array
(bool) - attribute that tells the data extractor what to do with duplicated keys in a query.
[!NOTE]
duplicated_attrs_parse_as_array
flat changes the type of data that the data extractor returns.If duplicated_attrs_parse_as_array=
True
, then the data will always be of type dict[str, list[Any]] (by default,multipart
has the extractable type dict[str, Any])
Catch client errors
The HTTPValidationFailure
exception will be raised if the data in the query is incorrect.
This error can be caught with a try/except block. The error values can be accessed using the validation_errors
attribute.
This may be necessary, for example, if you need to log a customer error before giving them a response.
import logging
from rapidy import web
from rapidy.typedefs import Handler
from rapidy.web_exceptions import HTTPValidationFailure
logger = logging.getLogger(__name__)
routes = web.RouteTableDef()
@routes.get('/')
async def handler(
auth_token: str = web.Header(alias='Authorization'),
) -> web.Response:
return web.json_response({'data': 'success'})
@web.middleware
async def error_catch_middleware(request: web.Request, handler: Handler) -> web.StreamResponse:
try:
return await handler(request)
except HTTPValidationFailure as validation_failure_error:
client_errors = validation_failure_error.validation_errors
logger.error('Client error catch, errors: %s', client_errors)
raise validation_failure_error
except Exception as unhandled_error:
logger.error('Unhandled error: %s', unhandled_error)
return web.json_response(status=500)
app = web.Application(middlewares=[error_catch_middleware])
app.add_routes(routes)
if __name__ == '__main__':
web.run_app(app, host='127.0.0.1', port=8080)
Default values for parameters
Some rAPIdy parameters may contain default values.
async def handler(
header_param_1: str = web.Header('default'),
header_param_2: Annotated[str, web.Header()] = 'default',
cookie_param_1: str = web.Cookie('default'),
cookie_param_2: Annotated[str, web.Cookie()] = 'default',
query_param_1: str = web.Query('default'),
query_param_2: Annotated[str, web.Query()] = 'default',
json_param_1: str = web.JsonBody('default'),
json_param_2: Annotated[str, web.JsonBody()] = 'default',
) -> web.Response:
[!NOTE] Default values support some single parameters and schemes
Raw and Path parameters cannot have default values.
Some body types do not contain
Raw
in their name, but they are also parameters that receive raw data, such asStreamBody
orTextBody
.
Client
[!TIP] Coming soon: 2024.09
OpenAPI
[!TIP] Already in development
Coming soon: 2024.09
Mypy support
rAPIdy
has its own plugin for mypy.
# example for pyproject.toml
# ...
[tool.mypy]
plugins = [
"pydantic.mypy",
"rapidy.mypy"
]
# ...
Migration from aiohttp to rAPIdy
rAPIdy
neatly extends aiohttp
- meaning that anything already written in aiohttp
will work as before without any modifications
rAPIdy
has exactly the same overridden module names as aiohttp
.
[!WARNING]
rAPIdy
does not override allaiohttp
modules, only those that are necessary for it to work, or those that will be extended in the near future.
If the aiohttp
module you are trying to override is not found in rAPIdy
, don't change it, everything will work as is.
[!WARNING]
rAPIdy
supports defining handlers in the same way as aiohttp-quickstart except that request is no longer a required parameter for functional handlers.If you need to get the current
request
in the handler, create an attribute with an arbitrary name and be sure to specify theweb.Request
type, and rAPIdy will substitute the currentweb.Request
instance in that place.
For Developers
[!TIP] Coming soon: 2024.06
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file rapidy-0.2.2.tar.gz
.
File metadata
- Download URL: rapidy-0.2.2.tar.gz
- Upload date:
- Size: 38.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.7.0 CPython/3.10.14 Linux/4.18.0-544.el8.x86_64
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | d6c3adc7317256e87e96499ba86f7c6391e9d3504c9bcd971a3951908955a886 |
|
MD5 | 4558c4d4bac301b21ab248fe5acb0761 |
|
BLAKE2b-256 | 521225c465d6d5bef6ab2c19d57760929f448e69ecb35a4b0b12fa2e98594e90 |
File details
Details for the file rapidy-0.2.2-py3-none-any.whl
.
File metadata
- Download URL: rapidy-0.2.2-py3-none-any.whl
- Upload date:
- Size: 40.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.7.0 CPython/3.10.14 Linux/4.18.0-544.el8.x86_64
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 7be2d9a17dfd41533927b93a68ea88f4bb8028f13446fad928b12fad52112b63 |
|
MD5 | 3633eea91b2bb3ba6cc457a3e414f803 |
|
BLAKE2b-256 | 395a2466bf99020a7a35ccb42d7b8b02e303272328899c0022a716116c55e810 |