<p align="center">
<a href="https://github.com/daniil-grois/rAPIdy" target="blank">
<img src="docs/assets/logo-teal.png" alt="rAPIdy">
</a>
</p>
<p align="center">
<a href="https://pypi.org/project/rapidy" target="blank">
<img src="https://img.shields.io/pypi/dm/rapidy?style=flat&logo=rapidy&logoColor=%237e56c2&color=%237e56c2" alt="Package downloads">
</a>
<a href="https://pypi.org/project/rapidy" target="blank">
<img src="https://img.shields.io/pypi/v/rapidy?style=flat&logo=rapidy&logoColor=%237e56c2&color=%237e56c2&label=pypi%20rAPIdy" alt="Package version">
</a>
<a href="https://pypi.org/project/rapidy" target="blank">
<img src="https://img.shields.io/pypi/pyversions/rapidy?style=flat&logo=rapidy&logoColor=%237e56c2&color=%237e56c2" alt="Python versions">
</a>
<a href="https://pypi.org/project/rapidy" target="blank">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v1.json&logoColor=%237e56c2&color=%237e56c2" alt="Pydantic V1">
</a>
<a href="https://pypi.org/project/rapidy" target="blank">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json&logoColor=%237e56c2&color=%237e56c2" alt="Pydantic V2">
</a>
</p>
<p align="center">
<i>
<a href="https://github.com/daniil-grois/rAPIdy" target="blank">rAPIdy</a>
- is a minimalist web framework for those who prioritize speed and convenience.<br/>
Built upon <a href="https://github.com/aio-libs/aiohttp" target="blank">aiohttp</a> and <a href="https://github.com/pydantic/pydantic" target="blank">pydantic</a>,
allowing you to fully leverage the advantages of this stack.
</i>
</p>
# 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 <a href="https://github.com/pydantic/pydantic">pydantic</a> for robust data validation
* 🚀 **Powered by aiohttp**: Utilizes <a href="https://github.com/aio-libs/aiohttp">aiohttp</a> 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]
> ```bash
> pip install rapidy
> ```
---
# Server
## Quickstart
### Handlers
```python
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 <a href="https://docs.aiohttp.org/en/stable/web_quickstart.html" target="blank">aiohttp-quickstart</a>,
> but with a key improvement:
> the `request` parameter is no longer required for functional handlers
>
> If you need to access the current `request` within a handler,
> simply declare an attribute with any name of your choosing and specify its type as `web.Request`. rAPIdy will automatically replace this attribute with the current instance of `web.Request`.
```python
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
```python
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 the `handler` 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
```python
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
```python
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.
```python
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.
```python
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.
```python
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]` or `dict[str, list[str]]`
> * MultipartBodyRaw - `dict[str, Any]` or `dict[str, list[Any]]`
> * TextBody - `str`
> * BytesBody - `bytes`
> * StreamBody - `aiohttp.streams.StreamReader`
#### Combining Different Approaches
```python
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.
1. **Using the Annotated Auxiliary Type**: Specify metadata directly within the Annotated type
2. **Using the Default Attribute Value**: Define the query parameter's default value along with its metadata
```python
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` and `Raw-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.
```python
async def handler(
body: str = web.JsonBodySchema(body_max_size=10),
) -> web.Response:
```
```python
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.<br>
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.
```python
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 <span style="color:#7e56c2">rAPIdy</span> parameters may contain default values.
```python
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 as `StreamBody` or `TextBody`.
---
# Client
> [!TIP]
> Coming soon: 2024.09
---
# OpenAPI
> [!TIP]
> Already in development
>
> Coming soon: 2024.09
---
# Mypy support
`rAPIdy` has its own plugin for <a href="https://mypy.readthedocs.io/en/stable/getting_started.html" target="blank">mypy</a>.
```toml
# 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 all `aiohttp` 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 <a href="https://docs.aiohttp.org/en/stable/web_quickstart.html" target="blank">aiohttp-quickstart</a> 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 the `web.Request` type, and rAPIdy will substitute the current `web.Request` instance in that place.
---
# For Developers
> [!TIP]
> Coming soon: 2024.06
Raw data
{
"_id": null,
"home_page": null,
"name": "rAPIdy",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.8",
"maintainer_email": null,
"keywords": "rAPIdy, aiohttp, pydantic, api, fast, http server, Daniil Grois, Lev Zaplatin",
"author": "Daniil Grois",
"author_email": "daniil.grois@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/52/12/25c465d6d5bef6ab2c19d57760929f448e69ecb35a4b0b12fa2e98594e90/rapidy-0.2.2.tar.gz",
"platform": null,
"description": "<p align=\"center\">\n <a href=\"https://github.com/daniil-grois/rAPIdy\" target=\"blank\">\n <img src=\"docs/assets/logo-teal.png\" alt=\"rAPIdy\">\n </a>\n</p>\n\n<p align=\"center\">\n <a href=\"https://pypi.org/project/rapidy\" target=\"blank\">\n <img src=\"https://img.shields.io/pypi/dm/rapidy?style=flat&logo=rapidy&logoColor=%237e56c2&color=%237e56c2\" alt=\"Package downloads\">\n </a>\n <a href=\"https://pypi.org/project/rapidy\" target=\"blank\">\n <img src=\"https://img.shields.io/pypi/v/rapidy?style=flat&logo=rapidy&logoColor=%237e56c2&color=%237e56c2&label=pypi%20rAPIdy\" alt=\"Package version\">\n </a>\n <a href=\"https://pypi.org/project/rapidy\" target=\"blank\">\n <img src=\"https://img.shields.io/pypi/pyversions/rapidy?style=flat&logo=rapidy&logoColor=%237e56c2&color=%237e56c2\" alt=\"Python versions\">\n </a>\n <a href=\"https://pypi.org/project/rapidy\" target=\"blank\">\n <img src=\"https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v1.json&logoColor=%237e56c2&color=%237e56c2\" alt=\"Pydantic V1\">\n </a>\n <a href=\"https://pypi.org/project/rapidy\" target=\"blank\">\n <img src=\"https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json&logoColor=%237e56c2&color=%237e56c2\" alt=\"Pydantic V2\">\n </a>\n</p>\n\n<p align=\"center\">\n <i>\n <a href=\"https://github.com/daniil-grois/rAPIdy\" target=\"blank\">rAPIdy</a>\n - is a minimalist web framework for those who prioritize speed and convenience.<br/>\n Built upon <a href=\"https://github.com/aio-libs/aiohttp\" target=\"blank\">aiohttp</a> and <a href=\"https://github.com/pydantic/pydantic\" target=\"blank\">pydantic</a>,\n allowing you to fully leverage the advantages of this stack.\n </i>\n</p>\n\n\n# Key features\n\n* \u270f\ufe0f **Minimalism**: Retrieve and check data effortlessly using just a single line of code\n* \ud83d\udc0d **Native Python Support**: Offers seamless compatibility with Python native types\n* \ud83d\udcd4 **Pydantic Integration**: Fully integrated with <a href=\"https://github.com/pydantic/pydantic\">pydantic</a> for robust data validation\n* \ud83d\ude80 **Powered by aiohttp**: Utilizes <a href=\"https://github.com/aio-libs/aiohttp\">aiohttp</a> to leverage its powerful asynchronous features\n* \ud83d\udce4 **Efficient Data Handling**: Simplifies the extraction of basic types from incoming data in Python\n\n\n---\n# Documentation\n> [!TIP]\n> Coming soon: 2024.06\n---\n\n\n# Installation\n> [!NOTE]\n> ```bash\n> pip install rapidy\n> ```\n---\n\n# Server\n## Quickstart\n### Handlers\n```python\nfrom rapidy import web\n\nroutes = web.RouteTableDef()\n\n@routes.post('/')\nasync def handler(\n auth_token: str = web.Header(alias='Authorization'),\n username: str = web.JsonBody(),\n password: str = web.JsonBody(min_length=8),\n) -> web.Response:\n print({'auth_token': auth_token, 'username': username, 'password': password})\n return web.json_response({'data': 'success'})\n\napp = web.Application()\napp.add_routes(routes)\n\nif __name__ == '__main__':\n web.run_app(app, host='127.0.0.1', port=8080)\n```\n\n> [!NOTE]\n> `rAPIdy` allows you to define handlers just like in <a href=\"https://docs.aiohttp.org/en/stable/web_quickstart.html\" target=\"blank\">aiohttp-quickstart</a>,\n> but with a key improvement:\n> the `request` parameter is no longer required for functional handlers\n>\n> If you need to access the current `request` within a handler,\n> simply declare an attribute with any name of your choosing and specify its type as `web.Request`. rAPIdy will automatically replace this attribute with the current instance of `web.Request`.\n```python\nfrom typing import Annotated\nfrom rapidy import web\n\nroutes = web.RouteTableDef()\n\n@routes.get('/default_handler_example')\nasync def default_handler_example(\n request: str = web.Request,\n) -> web.Response:\n print({'request': request})\n return web.json_response({'data': 'success'})\n\n@routes.get('/handler_without_request_example')\nasync def handler_without_request_example() -> web.Response:\n return web.json_response({'data': 'success'})\n\n@routes.get('/handler_request_as_snd_attr_example')\nasync def handler_request_as_snd_attr_example(\n host: Annotated[str, web.Header(alias='Host')],\n request: web.Request,\n) -> web.Response:\n print({'host': host, 'request': request})\n return web.json_response({'data': 'success'})\n\napp = web.Application()\napp.add_routes(routes)\n\nif __name__ == '__main__':\n web.run_app(app, host='127.0.0.1', port=8080)\n```\n\n### Middlewares\nProcessing an Authorization Token in Middleware\n\n```python\nfrom rapidy import web\nfrom rapidy.typedefs import HandlerType\n\n@web.middleware\nasync def hello_middleware(\n request: web.Request,\n handler: HandlerType,\n bearer_token: str = web.Header(alias='Authorization'),\n) -> web.StreamResponse:\n request['token'] = bearer_token\n return await handler(request)\n\nasync def handler(\n request: web.Request,\n host: str = web.Header(alias='Host'),\n username: str = web.JsonBody(),\n) -> web.Response:\n example_data = {'token': request['token'], 'host': host, 'username': username}\n return web.json_response(example_data)\n\napp = web.Application(middlewares=[hello_middleware])\napp.add_routes([web.post('/', handler)])\n\nif __name__ == '__main__':\n web.run_app(app, host='127.0.0.1', port=8080)\n```\n\n> [!IMPORTANT]\n> The first two attributes in a middleware are mandatory and must always represent the `request` and the `handler` respectively. These attributes are essential for the correct functioning of the middleware\n---\n\n## Request validation\n`rAPIdy` utilizes custom parameters to validate incoming HTTP request data, ensuring integrity and compliance with expected formats\n\n> [!TIP]\n> A parameter in `rAPIdy` represents an object that mcontains meta-information about the type of data it retrieves\n\nA 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\n\n```python\nfrom decimal import Decimal\nfrom pydantic import BaseModel, Field\nfrom rapidy import web\n\nroutes = web.RouteTableDef()\n\nclass Schema(BaseModel):\n positive: int = Field(gt=0)\n non_negative: int = Field(ge=0)\n negative: int = Field(lt=0)\n non_positive: int = Field(le=0)\n even: int = Field(multiple_of=2)\n love_for_pydantic: float = Field(allow_inf_nan=True)\n short: str = Field(min_length=3)\n long: str = Field(max_length=10)\n regex: str = Field(pattern=r'^\\d*$')\n precise: Decimal = Field(max_digits=5, decimal_places=2)\n\n@routes.get('/')\nasync def handler(\n positive: int = web.JsonBody(gt=0),\n non_negative: int = web.JsonBody(ge=0),\n negative: int = web.JsonBody(lt=0),\n non_positive: int = web.JsonBody(le=0),\n even: int = web.JsonBody(multiple_of=2),\n love_for_pydantic: float = web.JsonBody(allow_inf_nan=True),\n short: str = web.JsonBody(min_length=3),\n long: str = web.JsonBody(max_length=10),\n regex: str = web.JsonBody(pattern=r'^\\d*$'),\n precise: Decimal = web.JsonBody(max_digits=5, decimal_places=2),\n) -> web.Response:\n return web.Response()\n\n@routes.get('/schema')\nasync def handler_schema(\n body: Schema = web.JsonBodySchema(),\n) -> web.Response:\n return web.Response()\n\napp = web.Application()\napp.add_routes(routes)\n\nif __name__ == '__main__':\n web.run_app(app, host='127.0.0.1', port=8080)\n```\n\n### Validation Example\n```python\nfrom rapidy import web\nfrom pydantic import BaseModel, Field\n\nroutes = web.RouteTableDef()\n\nclass BodyRequestSchema(BaseModel):\n username: str = Field(min_length=3, max_length=20)\n password: str = Field(min_length=8, max_length=40)\n\n@routes.post('/api/{user_id}')\nasync def handler(\n user_id: str = web.Path(),\n host: str = web.Header(alias='Host'),\n body: BodyRequestSchema = web.JsonBodySchema(),\n) -> web.Response:\n return web.json_response({'data': 'success'})\n\napp = web.Application()\napp.add_routes(routes)\n\nif __name__ == '__main__':\n web.run_app(app, host='127.0.0.1', port=8080)\n```\n\n#### Success request validation\n```\ncurl -X POST \\\n-H \"Content-Type: application/json\" -d '{\"username\": \"User\", \"password\": \"myAwesomePass\"}' -v http://127.0.0.1:8080/api/1\n\n< HTTP/1.1 200 OK ... {\"data\": \"success\"}\n```\n\n#### Failed request validation\n\n```\ncurl -X POST \\\n-H \"Content-Type: application/json\" -d '{\"username\": \"U\", \"password\": \"m\"}' -v http://127.0.0.1:8080/api/1\n\n< HTTP/1.1 422 Unprocessable Entity ...\n{\n \"errors\": [\n {\n \"loc\": [\"body\", \"username\"],\n \"type\": \"string_too_short\",\n \"msg\": \"String should have at least 3 characters\",\n \"ctx\": {\"min_length\": 3}\n },\n {\n \"type\": \"string_too_short\",\n \"loc\": [\"body\", \"password\"],\n \"msg\": \"String should have at least 8 characters\",\n \"ctx\": {\"min_length\": 8}\n }\n ]\n}\n```\n\n### Types of request parameters\n`rAPIdy` supports 3 basic types for defining incoming parameters:\n* Param\n* Schema\n* Raw data\n\n#### Single parameter\n`Single` parameter, used when you need to spot-retrieve incoming data.\n\n```python\nfrom rapidy import web\n\nasync def handler(\n path_param: str = web.Path(),\n # headers\n host: str = web.Header(alias='Host'),\n user_agent: str = web.Header(alias='User-Agent'),\n # cookie\n user_cookie1: str = web.Cookie(alias='UserCookie1'),\n user_cookie2: str = web.Cookie(alias='UserCookie2'),\n # query params\n user_param1: str = web.Query(alias='UserQueryParam1'),\n user_param2: str = web.Cookie(alias='UserQueryParam2'),\n # body\n username: str = web.JsonBody(min_length=3, max_length=20),\n password: str = web.JsonBody(min_length=8, max_length=40),\n) -> web.Response:\n # write your code here\n # ...\n return web.Response()\n\napp = web.Application()\napp.add_routes([web.post('/api/{path_param}', handler)])\n```\n> [!NOTE]\n> All single parameters\n> * Path\n> * Header\n> * Cookie\n> * Query\n> * BodyJson\n> * FormDataBody\n> * MultipartBody\n\n#### Schema\n`Schema-parameter` is useful when you want to extract a large amount of data.\n\n```python\nfrom rapidy import web\nfrom pydantic import BaseModel, Field\n\nclass PathRequestSchema(BaseModel):\n path_param: str\n\nclass HeaderRequestSchema(BaseModel):\n host: str = Field(alias='Host')\n user_agent: str = Field(alias='User-Agent')\n\nclass CookieRequestSchema(BaseModel):\n user_cookie1: str = Field(alias='UserCookie1')\n user_cookie2: str = Field(alias='UserCookie2')\n\nclass QueryRequestSchema(BaseModel):\n user_cookie1: str = Field(alias='UserQueryParam1')\n user_cookie2: str = Field(alias='UserQueryParam1')\n\nclass BodyRequestSchema(BaseModel):\n username: str = Field(min_length=3, max_length=20)\n password: str = Field(min_length=8, max_length=40)\n\nasync def handler(\n path: PathRequestSchema = web.PathSchema(),\n headers: HeaderRequestSchema = web.HeaderSchema(),\n cookies: CookieRequestSchema = web.Cookie(),\n query: QueryRequestSchema = web.QuerySchema(),\n body: BodyRequestSchema = web.JsonBodySchema(),\n) -> web.Response:\n # write your code here\n # ...\n return web.Response()\n\napp = web.Application()\napp.add_routes([web.post('/api/{path_param}', handler)])\n```\n> [!NOTE]\n> All schema parameters\n> * PathSchema\n> * HeaderSchema\n> * CookieSchema\n> * QuerySchema\n> * BodyJsonSchema\n> * FormDataBodySchema\n> * MultipartBodySchema\n\n#### Raw\nUse `Raw-parameter` when you don't need validation.\n\n```python\nfrom typing import Any\nfrom rapidy import web\n\nasync def handler(\n path: dict[str, str] = web.PathRaw,\n headers: dict[str, str] = web.HeaderRaw,\n cookies: dict[str, str] = web.CookieRaw,\n query: dict[str, str] = web.QueryRaw,\n body: dict[str, Any] = web.JsonBodyRaw,\n) -> web.Response:\n # write your code here\n # ...\n return web.Response()\n\napp = web.Application()\napp.add_routes([web.post('/api/{path_param}', handler)])\n```\n> [!NOTE]\n> All raw parameters\n> * PathRaw - `dict[str, str]`\n> * HeaderRaw - `dict[str, str]`\n> * CookieRaw - `dict[str, str]`\n> * QueryRaw - `dict[str, str]`\n> * BodyJsonRaw - `dict[str, Any]`\n> * FormDataBodyRaw - `dict[str, str]` or `dict[str, list[str]]`\n> * MultipartBodyRaw - `dict[str, Any]` or `dict[str, list[Any]]`\n> * TextBody - `str`\n> * BytesBody - `bytes`\n> * StreamBody - `aiohttp.streams.StreamReader`\n\n\n#### Combining Different Approaches\n```python\nasync def handler(\n path_param: str = web.Path(),\n headers: dict[str, str] = web.HeaderRaw(),\n body: BodyRequestSchema = web.JsonBodySchema(),\n) -> web.Response:\n```\n\n### Ways to define metadata for a query parameter\nThere are a total of two ways to define query parameters.\n\n1. **Using the Annotated Auxiliary Type**: Specify metadata directly within the Annotated type\n2. **Using the Default Attribute Value**: Define the query parameter's default value along with its metadata\n\n\n```python\nfrom typing import Annotated # use typing_extensions if py version == 3.8\nfrom rapidy import web\n\nasync def handler(\n param_1: Annotated[str, web.JsonBody()], # Annotated definition\n param_2: str = web.JsonBody(), # Default definition\n) -> web.Response:\n```\n\n### Special attributes\n\nSome `rAPIdy-parameters` include additional attributes that are not found in `Pydantic`, offering extended functionality beyond standard data validation\n\n#### Body\n Currently, only parameters of the body type include special attributes in `rAPIdy`\n\n> [!WARNING]\n> Additional attributes can only be specified for `Schema-parameters` and `Raw-parameters`.\n\nIn `rAPIdy`, the `body_max_size` attribute associated with each body parameter restricts the maximum allowable size of the request `body` for a specific handler\n\n`body_max_size` (_int_) - indicating the maximum number of bytes the handler expects.\n\n```python\nasync def handler(\n body: str = web.JsonBodySchema(body_max_size=10),\n) -> web.Response:\n```\n```python\nasync def handler(\n body: str = web.JsonBodyRaw(body_max_size=10),\n) -> web.Response:\n```\n\n##### Json\n`json_decoder` (_typing.Callable[[], Any]_) - attribute that accepts the function to be called when decoding the body of the incoming request.\n\n##### FormData\n`attrs_case_sensitive` (_bool_) - attribute that tells the data extractor whether the incoming key register should be considered.\n\n`duplicated_attrs_parse_as_array` (_bool_) - attribute that tells the data extractor what to do with duplicated keys in a query.\n\nIf duplicated_attrs_parse_as_array=True, a list will be created for each key and all values will be placed in it.\n\n> [!NOTE]\n> `duplicated_attrs_parse_as_array` flat changes the type of data that the data extractor returns.\n>\n> If duplicated_attrs_parse_as_array=`True`, then the data\n> will always be of type _dict[str, list[str]]_ (_by default, `formdata` has the extractable type dict[str, str]_)\n\n##### Multipart\n\n`attrs_case_sensitive` (_bool_) - attribute that tells the data extractor whether the incoming key register should be considered.\n\n`duplicated_attrs_parse_as_array` (_bool_) - attribute that tells the data extractor what to do with duplicated keys in a query.\n\n> [!NOTE]\n> `duplicated_attrs_parse_as_array` flat changes the type of data that the data extractor returns.\n>\n> If duplicated_attrs_parse_as_array=`True`, then the data\n> will always be of type _dict[str, list[Any]]_ (_by default, `multipart` has the extractable type dict[str, Any]_)\n\n---\n\n### Catch client errors\nThe `HTTPValidationFailure` exception will be raised if the data in the query is incorrect.<br>\nThis error can be caught with a try/except block. The error values can be accessed using the `validation_errors` attribute.\n\nThis may be necessary, for example, if you need to log a customer error before giving them a response.\n\n```python\nimport logging\nfrom rapidy import web\nfrom rapidy.typedefs import Handler\nfrom rapidy.web_exceptions import HTTPValidationFailure\n\nlogger = logging.getLogger(__name__)\n\nroutes = web.RouteTableDef()\n\n@routes.get('/')\nasync def handler(\n auth_token: str = web.Header(alias='Authorization'),\n) -> web.Response:\n return web.json_response({'data': 'success'})\n\n@web.middleware\nasync def error_catch_middleware(request: web.Request, handler: Handler) -> web.StreamResponse:\n try:\n return await handler(request)\n\n except HTTPValidationFailure as validation_failure_error:\n client_errors = validation_failure_error.validation_errors\n logger.error('Client error catch, errors: %s', client_errors)\n raise validation_failure_error\n\n except Exception as unhandled_error:\n logger.error('Unhandled error: %s', unhandled_error)\n return web.json_response(status=500)\n\napp = web.Application(middlewares=[error_catch_middleware])\napp.add_routes(routes)\n\nif __name__ == '__main__':\n web.run_app(app, host='127.0.0.1', port=8080)\n```\n\n## Default values for parameters\nSome <span style=\"color:#7e56c2\">rAPIdy</span> parameters may contain default values.\n\n```python\nasync def handler(\n header_param_1: str = web.Header('default'),\n header_param_2: Annotated[str, web.Header()] = 'default',\n\n cookie_param_1: str = web.Cookie('default'),\n cookie_param_2: Annotated[str, web.Cookie()] = 'default',\n\n query_param_1: str = web.Query('default'),\n query_param_2: Annotated[str, web.Query()] = 'default',\n\n json_param_1: str = web.JsonBody('default'),\n json_param_2: Annotated[str, web.JsonBody()] = 'default',\n) -> web.Response:\n```\n> [!NOTE]\n> Default values support some single parameters and schemes\n>\n> Raw and Path parameters cannot have default values.\n>\n> Some body types do not contain `Raw` in their name, but they are also parameters that receive raw data, such as `StreamBody` or `TextBody`.\n---\n\n\n# Client\n> [!TIP]\n> Coming soon: 2024.09\n---\n\n\n# OpenAPI\n> [!TIP]\n> Already in development\n>\n> Coming soon: 2024.09\n---\n\n\n# Mypy support\n`rAPIdy` has its own plugin for <a href=\"https://mypy.readthedocs.io/en/stable/getting_started.html\" target=\"blank\">mypy</a>.\n\n```toml\n# example for pyproject.toml\n# ...\n[tool.mypy]\nplugins = [\n \"pydantic.mypy\",\n \"rapidy.mypy\"\n]\n# ...\n```\n---\n\n\n# Migration from aiohttp to rAPIdy\n`rAPIdy` neatly extends `aiohttp` - meaning that anything already written in `aiohttp` will work as before without any modifications\n\n`rAPIdy` has exactly the same overridden module names as `aiohttp`.\n\n> [!WARNING]\n> `rAPIdy` does not override all `aiohttp` modules, only those that are necessary for it to work, or those that will be extended in the near future.\n\nIf the `aiohttp` module you are trying to override is not found in `rAPIdy`, don't change it, everything will work as is.\n\n> [!WARNING]\n> `rAPIdy` supports defining handlers in the same way as <a href=\"https://docs.aiohttp.org/en/stable/web_quickstart.html\" target=\"blank\">aiohttp-quickstart</a> except that request is no longer a required parameter for functional handlers.\n>\n> If you need to get the current `request` in the handler,\n> create an attribute with an arbitrary name and be sure to specify the `web.Request` type, and rAPIdy will substitute the current `web.Request` instance in that place.\n---\n\n\n# For Developers\n> [!TIP]\n> Coming soon: 2024.06\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "rAPIdy - write quickly - write beautifully",
"version": "0.2.2",
"project_urls": null,
"split_keywords": [
"rapidy",
" aiohttp",
" pydantic",
" api",
" fast",
" http server",
" daniil grois",
" lev zaplatin"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "395a2466bf99020a7a35ccb42d7b8b02e303272328899c0022a716116c55e810",
"md5": "3633eea91b2bb3ba6cc457a3e414f803",
"sha256": "7be2d9a17dfd41533927b93a68ea88f4bb8028f13446fad928b12fad52112b63"
},
"downloads": -1,
"filename": "rapidy-0.2.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "3633eea91b2bb3ba6cc457a3e414f803",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.8",
"size": 40653,
"upload_time": "2024-05-11T12:21:50",
"upload_time_iso_8601": "2024-05-11T12:21:50.752733Z",
"url": "https://files.pythonhosted.org/packages/39/5a/2466bf99020a7a35ccb42d7b8b02e303272328899c0022a716116c55e810/rapidy-0.2.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "521225c465d6d5bef6ab2c19d57760929f448e69ecb35a4b0b12fa2e98594e90",
"md5": "4558c4d4bac301b21ab248fe5acb0761",
"sha256": "d6c3adc7317256e87e96499ba86f7c6391e9d3504c9bcd971a3951908955a886"
},
"downloads": -1,
"filename": "rapidy-0.2.2.tar.gz",
"has_sig": false,
"md5_digest": "4558c4d4bac301b21ab248fe5acb0761",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.8",
"size": 37989,
"upload_time": "2024-05-11T12:21:52",
"upload_time_iso_8601": "2024-05-11T12:21:52.126741Z",
"url": "https://files.pythonhosted.org/packages/52/12/25c465d6d5bef6ab2c19d57760929f448e69ecb35a4b0b12fa2e98594e90/rapidy-0.2.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-05-11 12:21:52",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "rapidy"
}