Conversion of User Guide to the SHH stack (#2781)
This commit is contained in:
270
guide/content/en/plugins/sanic-ext/configuration.md
Normal file
270
guide/content/en/plugins/sanic-ext/configuration.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# Configuration
|
||||
|
||||
Sanic Extensions can be configured in all of the same ways that [you can configure Sanic](../../guide/deployment/configuration.md). That makes configuring Sanic Extensions very easy.
|
||||
|
||||
```python
|
||||
app = Sanic("MyApp")
|
||||
app.config.OAS_URL_PREFIX = "/apidocs"
|
||||
```
|
||||
|
||||
However, there are a few more configuration options that should be considered.
|
||||
|
||||
## Manual `extend`
|
||||
|
||||
.. column::
|
||||
|
||||
Even though Sanic Extensions will automatically attach to your application, you can manually choose `extend`. When you do that, you can pass all of the configuration values as a keyword arguments (lowercase).
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
app = Sanic("MyApp")
|
||||
app.extend(oas_url_prefix="/apidocs")
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
Or, alternatively they could be passed all at once as a single `dict`.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
app = Sanic("MyApp")
|
||||
app.extend(config={"oas_url_prefix": "/apidocs"})
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
Both of these solutions suffers from the fact that the names of the configuration settings are not discoverable by an IDE. Therefore, there is also a type annotated object that you can use. This should help the development experience.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import Config
|
||||
|
||||
app = Sanic("MyApp")
|
||||
app.extend(config=Config(oas_url_prefix="/apidocs"))
|
||||
```
|
||||
|
||||
## Settings
|
||||
|
||||
.. note::
|
||||
|
||||
Often, the easiest way to change these for an application (since they likely are not going to change dependent upon an environment), is to set them directly on the `app.config` object.
|
||||
|
||||
Simply use the capitalized version of the configuration key as shown here:
|
||||
|
||||
```python
|
||||
app = Sanic("MyApp")
|
||||
app.config.OAS_URL_PREFIX = "/apidocs"
|
||||
```
|
||||
|
||||
### `cors`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: Whether to enable CORS protection
|
||||
|
||||
### `cors_allow_headers`
|
||||
|
||||
- **Type**: `str`
|
||||
- **Default**: `"*"`
|
||||
- **Description**: Value of the header: `access-control-allow-headers`
|
||||
|
||||
### `cors_always_send`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: Whether to always send the header: `access-control-allow-origin`
|
||||
|
||||
### `cors_automatic_options`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: Whether to automatically generate `OPTIONS` endpoints for routes that do *not* already have one defined
|
||||
|
||||
### `cors_expose_headers`
|
||||
|
||||
- **Type**: `str`
|
||||
- **Default**: `""`
|
||||
- **Description**: Value of the header: `access-control-expose-headers`
|
||||
|
||||
### `cors_max_age`
|
||||
|
||||
- **Type**: `int`
|
||||
- **Default**: `5`
|
||||
- **Description**: Value of the header: `access-control-max-age`
|
||||
|
||||
### `cors_methods`
|
||||
|
||||
- **Type**: `str`
|
||||
- **Default**: `""`
|
||||
- **Description**: Value of the header: `access-control-access-control-allow-methods`
|
||||
|
||||
### `cors_origins`
|
||||
|
||||
- **Type**: `str`
|
||||
- **Default**: `""`
|
||||
- **Description**: Value of the header: `access-control-allow-origin`
|
||||
|
||||
|
||||
.. warning::
|
||||
|
||||
Be very careful if you place `*` here. Do not do this unless you know what you are doing as it can be a security issue.
|
||||
|
||||
|
||||
### `cors_send_wildcard`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `False`
|
||||
- **Description**: Whether to send a wildcard origin instead of the incoming request origin
|
||||
|
||||
### `cors_supports_credentials`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `False`
|
||||
- **Description**: Value of the header: `access-control-allow-credentials`
|
||||
|
||||
### `cors_vary_header`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: Whether to add the `vary` header
|
||||
|
||||
### `http_all_methods`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: Adds the HTTP `CONNECT` and `TRACE` methods as allowable
|
||||
|
||||
### `http_auto_head`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: Automatically adds `HEAD` handlers to any `GET` routes
|
||||
|
||||
### `http_auto_options`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: Automatically adds `OPTIONS` handlers to any routes without
|
||||
|
||||
### `http_auto_trace`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `False`
|
||||
- **Description**: Automatically adds `TRACE` handlers to any routes without
|
||||
|
||||
### `oas`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: Whether to enable OpenAPI specification generation
|
||||
|
||||
### `oas_autodoc`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: Whether to automatically extract OpenAPI details from the docstring of a route function
|
||||
|
||||
### `oas_ignore_head`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: WHen `True`, it will not add `HEAD` endpoints into the OpenAPI specification
|
||||
|
||||
### `oas_ignore_options`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: WHen `True`, it will not add `OPTIONS` endpoints into the OpenAPI specification
|
||||
|
||||
### `oas_path_to_redoc_html`
|
||||
|
||||
- **Type**: `Optional[str]`
|
||||
- **Default**: `None`
|
||||
- **Description**: Path to HTML file to override the existing Redoc HTML
|
||||
|
||||
### `oas_path_to_swagger_html`
|
||||
|
||||
- **Type**: `Optional[str]`
|
||||
- **Default**: `None`
|
||||
- **Description**: Path to HTML file to override the existing Swagger HTML
|
||||
|
||||
### `oas_ui_default`
|
||||
|
||||
- **Type**: `Optional[str]`
|
||||
- **Default**: `"redoc"`
|
||||
- **Description**: Which OAS documentation to serve on the bare `oas_url_prefix` endpoint; when `None` there will be no documentation at that location
|
||||
|
||||
### `oas_ui_redoc`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: Whether to enable the Redoc UI
|
||||
|
||||
### `oas_ui_swagger`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: Whether to enable the Swagger UI
|
||||
|
||||
### `oas_ui_swagger_version`
|
||||
|
||||
- **Type**: `str`
|
||||
- **Default**: `"4.1.0"`
|
||||
- **Description**: Which Swagger version to use
|
||||
|
||||
### `oas_uri_to_config`
|
||||
|
||||
- **Type**: `str`
|
||||
- **Default**: `"/swagger-config"`
|
||||
- **Description**: Path to serve the Swagger configurtaion
|
||||
|
||||
### `oas_uri_to_json`
|
||||
|
||||
- **Type**: `str`
|
||||
- **Default**: `"/openapi.json"`
|
||||
- **Description**: Path to serve the OpenAPI JSON
|
||||
|
||||
### `oas_uri_to_redoc`
|
||||
|
||||
- **Type**: `str`
|
||||
- **Default**: `"/redoc"`
|
||||
- **Description**: Path to Redoc
|
||||
|
||||
### `oas_uri_to_swagger`
|
||||
|
||||
- **Type**: `str`
|
||||
- **Default**: `"/swagger"`
|
||||
- **Description**: Path to Swagger
|
||||
|
||||
### `oas_url_prefix`
|
||||
|
||||
- **Type**: `str`
|
||||
- **Default**: `"/docs"`
|
||||
- **Description**: URL prefix for the Blueprint that all of the OAS documentation witll attach to
|
||||
|
||||
### `swagger_ui_configuration`
|
||||
|
||||
- **Type**: `Dict[str, Any]`
|
||||
- **Default**: `{"apisSorter": "alpha", "operationsSorter": "alpha", "docExpansion": "full"}`
|
||||
- **Description**: The Swagger documentation to be served to the frontend
|
||||
|
||||
### `templating_enable_async`
|
||||
|
||||
- **Type**: `bool`
|
||||
- **Default**: `True`
|
||||
- **Description**: Whether to set `enable_async` on the Jinja `Environment`
|
||||
|
||||
### `templating_path_to_templates`
|
||||
|
||||
- **Type**: `Union[str, os.PathLike, Sequence[Union[str, os.PathLike]]] `
|
||||
- **Default**: `templates`
|
||||
- **Description**: A single path, or multiple paths to where your template files are located
|
||||
|
||||
### `trace_excluded_headers`
|
||||
|
||||
- **Type**: `Sequence[str]`
|
||||
- **Default**: `("authorization", "cookie")`
|
||||
- **Description**: Which headers should be suppresed from responses to `TRACE` requests
|
||||
111
guide/content/en/plugins/sanic-ext/convenience.md
Normal file
111
guide/content/en/plugins/sanic-ext/convenience.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Convenience
|
||||
|
||||
## Fixed serializer
|
||||
|
||||
.. column::
|
||||
|
||||
Often when developing an application, there will be certain routes that always return the same sort of response. When this is the case, you can predefine the return serializer and on the endpoint, and then all that needs to be returned is the content.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import serializer
|
||||
|
||||
@app.get("/<name>")
|
||||
@serializer(text)
|
||||
async def hello_world(request, name: str):
|
||||
if name.isnumeric():
|
||||
return "hello " * int(name)
|
||||
return f"Hello, {name}"
|
||||
```
|
||||
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
The `serializer` decorator also can add status codes.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import serializer
|
||||
|
||||
@app.post("/")
|
||||
@serializer(text, status=202)
|
||||
async def create_something(request):
|
||||
...
|
||||
```
|
||||
|
||||
## Custom serializer
|
||||
|
||||
.. column::
|
||||
|
||||
Using the `@serializer` decorator, you can also pass your own custom functions as long as they also return a valid type (`HTTPResonse`).
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
def message(retval, request, action, status):
|
||||
return json(
|
||||
{
|
||||
"request_id": str(request.id),
|
||||
"action": action,
|
||||
"message": retval,
|
||||
},
|
||||
status=status,
|
||||
)
|
||||
|
||||
@app.post("/<action>")
|
||||
@serializer(message)
|
||||
async def do_action(request, action: str):
|
||||
return "This is a message"
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
Now, returning just a string should return a nice serialized output.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
$ curl localhost:8000/eat_cookies -X POST
|
||||
{
|
||||
"request_id": "ef81c45b-235c-46dd-9dbd-b550f8fa77f9",
|
||||
"action": "eat_cookies",
|
||||
"message": "This is a message"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Request counter
|
||||
|
||||
.. column::
|
||||
|
||||
Sanic Extensions comes with a subclass of `Request` that can be setup to automatically keep track of the number of requests processed per worker process. To enable this, you should pass the `CountedRequest` class to your application contructor.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import CountedRequest
|
||||
|
||||
app = Sanic(..., request_class=CountedRequest)
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
You will now have access to the number of requests served during the lifetime of the worker process.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@app.get("/")
|
||||
async def handler(request: CountedRequest):
|
||||
return json({"count": request.count})
|
||||
```
|
||||
|
||||
If possible, the request count will also be added to the [worker state](../../guide/deployment/manager.md#worker-state).
|
||||
|
||||

|
||||
84
guide/content/en/plugins/sanic-ext/custom.md
Normal file
84
guide/content/en/plugins/sanic-ext/custom.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Custom extensions
|
||||
|
||||
It is possible to create your own custom extensions.
|
||||
|
||||
Version 22.9 added the `Extend.register` [method](#extension-preregistration). This makes it extremely easy to add custom expensions to an application.
|
||||
|
||||
## Anatomy of an extension
|
||||
|
||||
All extensions must subclass `Extension`.
|
||||
|
||||
### Required
|
||||
|
||||
- `name`: By convention, the name is an all-lowercase string
|
||||
- `startup`: A method that runs when the extension is added
|
||||
|
||||
### Optional
|
||||
|
||||
- `label`: A method that returns additional information about the extension in the MOTD
|
||||
- `included`: A method that returns a boolean whether the extension should be enabled or not (could be used for example to check config state)
|
||||
|
||||
### Example
|
||||
|
||||
```python
|
||||
from sanic import Request, Sanic, json
|
||||
from sanic_ext import Extend, Extension
|
||||
|
||||
app = Sanic(__name__)
|
||||
app.config.MONITOR = True
|
||||
|
||||
class AutoMonitor(Extension):
|
||||
name = "automonitor"
|
||||
|
||||
def startup(self, bootstrap) -> None:
|
||||
if self.included():
|
||||
self.app.before_server_start(self.ensure_monitor_set)
|
||||
self.app.on_request(self.monitor)
|
||||
|
||||
@staticmethod
|
||||
async def monitor(request: Request):
|
||||
if request.route and request.route.ctx.monitor:
|
||||
print("....")
|
||||
|
||||
@staticmethod
|
||||
async def ensure_monitor_set(app: Sanic):
|
||||
for route in app.router.routes:
|
||||
if not hasattr(route.ctx, "monitor"):
|
||||
route.ctx.monitor = False
|
||||
|
||||
def label(self):
|
||||
has_monitor = [
|
||||
route
|
||||
for route in self.app.router.routes
|
||||
if getattr(route.ctx, "monitor", None)
|
||||
]
|
||||
return f"{len(has_monitor)} endpoint(s)"
|
||||
|
||||
def included(self):
|
||||
return self.app.config.MONITOR
|
||||
|
||||
Extend.register(AutoMonitor)
|
||||
|
||||
@app.get("/", ctx_monitor=True)
|
||||
async def handler(request: Request):
|
||||
return json({"foo": "bar"})
|
||||
```
|
||||
|
||||
## Extension preregistration
|
||||
|
||||
.. column::
|
||||
|
||||
`Extend.register` simplifies the addition of custom extensions.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import Extend, Extension
|
||||
|
||||
class MyCustomExtension(Extension):
|
||||
...
|
||||
|
||||
Extend.register(MyCustomExtension())
|
||||
```
|
||||
|
||||
*Added in v22.9*
|
||||
81
guide/content/en/plugins/sanic-ext/getting-started.md
Normal file
81
guide/content/en/plugins/sanic-ext/getting-started.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Getting Started
|
||||
|
||||
Sanic Extensions is an *officially supported* plugin developed, and maintained by the SCO. The primary goal of this project is to add additional features to help Web API and Web application development easier.
|
||||
|
||||
## Features
|
||||
|
||||
- CORS protection
|
||||
- Template rendering with Jinja
|
||||
- Dependency injection into route handlers
|
||||
- OpenAPI documentation with Redoc and/or Swagger
|
||||
- Predefined, endpoint-specific response serializers
|
||||
- Request query arguments and body input validation
|
||||
- Auto create `HEAD`, `OPTIONS`, and `TRACE` endpoints
|
||||
|
||||
## Minimum requirements
|
||||
|
||||
- **Python**: 3.8+
|
||||
- **Sanic**: 21.9+
|
||||
|
||||
## Install
|
||||
|
||||
The best method is to just install Sanic Extensions along with Sanic itself:
|
||||
|
||||
```bash
|
||||
pip install sanic[ext]
|
||||
```
|
||||
|
||||
You can of course also just install it by itself.
|
||||
|
||||
```bash
|
||||
pip install sanic-ext
|
||||
```
|
||||
|
||||
## Extend your application
|
||||
|
||||
Out of the box, Sanic Extensions will enable a bunch of features for you.
|
||||
|
||||
.. column::
|
||||
|
||||
To setup Sanic Extensions (v21.12+), you need to do: **nothing**. If it is installed in the environment, it is setup and ready to go.
|
||||
|
||||
This code is the Hello, world app in the [Sanic Getting Started page](../../guide/getting-started.md) _without any changes_, but using Sanic Extensions with `sanic-ext` installed in the background.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic("MyHelloWorldApp")
|
||||
|
||||
@app.get("/")
|
||||
async def hello_world(request):
|
||||
return text("Hello, world.")
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
**_OLD DEPRECATED SETUP_**
|
||||
|
||||
In v21.9, the easiest way to get started is to instantiate it with `Extend`.
|
||||
|
||||
If you look back at the Hello, world app in the [Sanic Getting Started page](../../guide/getting-started.md), you will see the only additions here are the two highlighted lines.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic_ext import Extend
|
||||
|
||||
app = Sanic("MyHelloWorldApp")
|
||||
Extend(app)
|
||||
|
||||
@app.get("/")
|
||||
async def hello_world(request):
|
||||
return text("Hello, world.")
|
||||
```
|
||||
|
||||
Regardless of how it is setup, you should now be able to view the OpenAPI documentation and see some of the functionality in action: [http://localhost:8000/docs](http://localhost:8000/docs).
|
||||
69
guide/content/en/plugins/sanic-ext/health-monitor.md
Normal file
69
guide/content/en/plugins/sanic-ext/health-monitor.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Health monitor
|
||||
|
||||
The health monitor requires both `sanic>=22.9` and `sanic-ext>=22.9`.
|
||||
|
||||
You can setup Sanic Extensions to monitor the health of your worker processes. This requires that you not be in [single process mode](../../guide/deployment/manager.md#single-process-mode).
|
||||
|
||||
## Setup
|
||||
|
||||
.. column::
|
||||
|
||||
Out of the box, the health monitor is disabled. You will need to opt-in if you would like to use it.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
app.config.HEALTH = True
|
||||
```
|
||||
|
||||
## How does it work
|
||||
|
||||
The monitor sets up a new background process that will periodically receive acknowledgements of liveliness from each worker process. If a worker process misses a report too many times, then the monitor will restart that one worker.
|
||||
|
||||
## Diagnostics endpoint
|
||||
|
||||
.. column::
|
||||
|
||||
The health monitor will also enable a diagnostics endpoint that outputs the [worker state](../../guide/deployment/manager.md#worker-state). By default is id disabled.
|
||||
|
||||
.. danger::
|
||||
|
||||
The diagnostics endpoint is not secured. If you are deploying it in a production environment, you should take steps to protect it with a proxy server if you are using one. If not, you may want to consider disabling this feature in production since it will leak details about your server state.
|
||||
|
||||
.. column::
|
||||
|
||||
```
|
||||
$ curl http://localhost:8000/__health__
|
||||
{
|
||||
'Sanic-Main': {'pid': 99997},
|
||||
'Sanic-Server-0-0': {
|
||||
'server': True,
|
||||
'state': 'ACKED',
|
||||
'pid': 9999,
|
||||
'start_at': datetime.datetime(2022, 10, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc),
|
||||
'starts': 2,
|
||||
'restart_at': datetime.datetime(2022, 10, 1, 0, 0, 12, 861332, tzinfo=datetime.timezone.utc)
|
||||
},
|
||||
'Sanic-Reloader-0': {
|
||||
'server': False,
|
||||
'state': 'STARTED',
|
||||
'pid': 99998,
|
||||
'start_at': datetime.datetime(2022, 10, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc),
|
||||
'starts': 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
| Key | Type | Default| Description |
|
||||
|--|--|--|--|
|
||||
| HEALTH | `bool` | `False` | Whether to enable this extension. |
|
||||
| HEALTH_ENDPOINT | `bool` | `False` | Whether to enable the diagnostics endpoint. |
|
||||
| HEALTH_MAX_MISSES | `int` | `3` | The number of consecutive misses before a worker process is restarted. |
|
||||
| HEALTH_MISSED_THRESHHOLD | `int` | `10` | The number of seconds the monitor checks for worker process health. |
|
||||
| HEALTH_MONITOR | `bool` | `True` | Whether to enable the health monitor. |
|
||||
| HEALTH_REPORT_INTERVAL | `int` | `5` | The number of seconds between reporting each acknowledgement of liveliness. |
|
||||
| HEALTH_URI_TO_INFO | `str` | `""` | The URI path of the diagnostics endpoint. |
|
||||
| HEALTH_URL_PREFIX | `str` | `"/__health__"` | The URI prefix of the diagnostics blueprint. |
|
||||
86
guide/content/en/plugins/sanic-ext/http/cors.md
Normal file
86
guide/content/en/plugins/sanic-ext/http/cors.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# CORS protection
|
||||
|
||||
Cross-Origin Resource Sharing (aka CORS) is a *huge* topic by itself. The documentation here cannot go into enough detail about *what* it is. You are highly encouraged to do some research on your own to understand the security problem presented by it, and the theory behind the solutions. [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) are a great first step.
|
||||
|
||||
In super brief terms, CORS protection is a framework that browsers use to facilitate how and when a web page can access information from another domain. It is extremely relevant to anyone building a single-page application. Often times your frontend might be on a domain like `https://portal.myapp.com`, but it needs to access the backend from `https://api.myapp.com`.
|
||||
|
||||
The implementation here is heavily inspired by [`sanic-cors`](https://github.com/ashleysommer/sanic-cors), which is in turn based upon [`flask-cors`](https://github.com/corydolphin/flask-cors). It is therefore very likely that you can achieve a near drop-in replacement of `sanic-cors` with `sanic-ext`.
|
||||
|
||||
## Basic implementation
|
||||
|
||||
.. column::
|
||||
|
||||
As shown in the example in the [auto-endpoints example](methods.md#options), Sanic Extensions will automatically enable CORS protection without further action. But, it does not offer too much out of the box.
|
||||
|
||||
At a *bare minimum*, it is **highly** recommended that you set `config.CORS_ORIGINS` to the intended origin(s) that will be accessing the application.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic import Sanic, text
|
||||
from sanic_ext import Extend
|
||||
|
||||
app = Sanic(__name__)
|
||||
app.config.CORS_ORIGINS = "http://foobar.com,http://bar.com"
|
||||
Extend(app)
|
||||
|
||||
@app.get("/")
|
||||
async def hello_world(request):
|
||||
return text("Hello, world.")
|
||||
```
|
||||
|
||||
```
|
||||
$ curl localhost:8000 -X OPTIONS -i
|
||||
HTTP/1.1 204 No Content
|
||||
allow: GET,HEAD,OPTIONS
|
||||
access-control-allow-origin: http://foobar.com
|
||||
connection: keep-alive
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The true power of CORS protection, however, comes into play once you start configuring it. Here is a table of all of the options.
|
||||
|
||||
| Key | Type | Default| Description |
|
||||
|--|--|--|--|
|
||||
| `CORS_ALLOW_HEADERS` | `str` or `List[str]` | `"*"` | The list of headers that will appear in `access-control-allow-headers`. |
|
||||
| `CORS_ALWAYS_SEND` | `bool` | `True` | When `True`, will always set a value for `access-control-allow-origin`. When `False`, will only set it if there is an `Origin` header. |
|
||||
| `CORS_AUTOMATIC_OPTIONS` | `bool` | `True` | When the incoming preflight request is received, whether to automatically set values for `access-control-allow-headers`, `access-control-max-age`, and `access-control-allow-methods` headers. If `False` these values will only be set on routes that are decorated with the `@cors` decorator. |
|
||||
| `CORS_EXPOSE_HEADERS` | `str` or `List[str]` | `""` | Specific list of headers to be set in `access-control-expose-headers` header. |
|
||||
| `CORS_MAX_AGE` | `str`, `int`, `timedelta` | `0` | The maximum number of seconds the preflight response may be cached using the `access-control-max-age` header. A falsey value will cause the header to not be set. |
|
||||
| `CORS_METHODS` | `str` or `List[str]` | `""` | The HTTP methods that the allowed origins can access, as set on the `access-control-allow-methods` header. |
|
||||
| `CORS_ORIGINS` | `str`, `List[str]`, `re.Pattern` | `"*"` | The origins that are allowed to access the resource, as set on the `access-control-allow-origin` header. |
|
||||
| `CORS_SEND_WILDCARD` | `bool` | `False` | If `True`, will send the wildcard `*` origin instead of the `origin` request header. |
|
||||
| `CORS_SUPPORTS_CREDENTIALS` | `bool` | `False` | Whether to set the `access-control-allow-credentials` header. |
|
||||
| `CORS_VARY_HEADER` | `bool` | `True` | Whether to add `vary` header, when appropriate. |
|
||||
|
||||
*For the sake of brevity, where the above says `List[str]` any instance of a `list`, `set`, `frozenset`, or `tuple` will be acceptable. Alternatively, if the value is a `str`, it can be a comma delimited list.*
|
||||
|
||||
## Route level overrides
|
||||
|
||||
.. column::
|
||||
|
||||
It may sometimes be necessary to override app-wide settings for a specific route. To allow for this, you can use the `@sanic_ext.cors()` decorator to set different route-specific values.
|
||||
|
||||
The values that can be overridden with this decorator are:
|
||||
|
||||
- `origins`
|
||||
- `expose_headers`
|
||||
- `allow_headers`
|
||||
- `allow_methods`
|
||||
- `supports_credentials`
|
||||
- `max_age`
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import cors
|
||||
|
||||
app.config.CORS_ORIGINS = "https://foo.com"
|
||||
|
||||
@app.get("/", host="bar.com")
|
||||
@cors(origins="https://bar.com")
|
||||
async def hello_world(request):
|
||||
return text("Hello, world.")
|
||||
```
|
||||
|
||||
142
guide/content/en/plugins/sanic-ext/http/methods.md
Normal file
142
guide/content/en/plugins/sanic-ext/http/methods.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# HTTP Methods
|
||||
|
||||
## Auto-endpoints
|
||||
|
||||
The default behavior is to automatically generate `HEAD` endpoints for all `GET` routes, and `OPTIONS` endpoints for all
|
||||
routes. Additionally, there is the option to automatically generate `TRACE` endpoints. However, these are not enabled by
|
||||
default.
|
||||
|
||||
### HEAD
|
||||
|
||||
.. column::
|
||||
|
||||
- **Configuration**: `AUTO_HEAD` (default `True`)
|
||||
- **MDN**: [Read more](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD)
|
||||
|
||||
A `HEAD` request provides the headers and an otherwise identical response to what a `GET` request would provide.
|
||||
However, it does not actually return the body.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@app.get("/")
|
||||
async def hello_world(request):
|
||||
return text("Hello, world.")
|
||||
```
|
||||
|
||||
Given the above route definition, Sanic Extensions will enable `HEAD` responses, as seen here.
|
||||
|
||||
```
|
||||
$ curl localhost:8000 --head
|
||||
HTTP/1.1 200 OK
|
||||
access-control-allow-origin: *
|
||||
content-length: 13
|
||||
connection: keep-alive
|
||||
content-type: text/plain; charset=utf-8
|
||||
```
|
||||
|
||||
### OPTIONS
|
||||
|
||||
.. column::
|
||||
|
||||
- **Configuration**: `AUTO_OPTIONS` (default `True`)
|
||||
- **MDN**: [Read more](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS)
|
||||
|
||||
`OPTIONS` requests provide the recipient with details about how the client is allowed to communicate with a given
|
||||
endpoint.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@app.get("/")
|
||||
async def hello_world(request):
|
||||
return text("Hello, world.")
|
||||
```
|
||||
|
||||
Given the above route definition, Sanic Extensions will enable `OPTIONS` responses, as seen here.
|
||||
|
||||
It is important to note that we also see `access-control-allow-origins` in this example. This is because
|
||||
the [CORS protection](cors.md) is enabled by default.
|
||||
|
||||
```
|
||||
$ curl localhost:8000 -X OPTIONS -i
|
||||
HTTP/1.1 204 No Content
|
||||
allow: GET,HEAD,OPTIONS
|
||||
access-control-allow-origin: *
|
||||
connection: keep-alive
|
||||
```
|
||||
|
||||
.. tip::
|
||||
|
||||
Even though Sanic Extensions will setup these routes for you automatically, if you decide to manually create an `@app.options` route, it will *not* be overridden.
|
||||
|
||||
### TRACE
|
||||
|
||||
.. column::
|
||||
|
||||
- **Configuration**: `AUTO_TRACE` (default `False`)
|
||||
- **MDN**: [Read more](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE)
|
||||
|
||||
By default, `TRACE` endpoints will **not** be automatically created. However, Sanic Extensions **will allow** you to
|
||||
create them if you wanted. This is something that is not allowed in vanilla Sanic.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@app.route("/", methods=["trace"])
|
||||
async def handler(request):
|
||||
...
|
||||
```
|
||||
|
||||
To enable auto-creation of these endpoints, you must first enable them when extending Sanic.
|
||||
|
||||
```python
|
||||
from sanic_ext import Extend, Config
|
||||
|
||||
app.extend(config=Config(http_auto_trace=True))
|
||||
```
|
||||
|
||||
Now, assuming you have some endpoints setup, you can trace them as shown here:
|
||||
|
||||
```
|
||||
$ curl localhost:8000 -X TRACE
|
||||
TRACE / HTTP/1.1
|
||||
Host: localhost:9999
|
||||
User-Agent: curl/7.76.1
|
||||
Accept: */*
|
||||
```
|
||||
|
||||
.. tip::
|
||||
|
||||
Setting up `AUTO_TRACE` can be super helpful, especially when your application is deployed behind a proxy since it will help you determine how the proxy is behaving.
|
||||
|
||||
## Additional method support
|
||||
|
||||
Vanilla Sanic allows you to build endpoints with the following HTTP methods:
|
||||
|
||||
- [GET](/en/guide/basics/routing.html#get)
|
||||
- [POST](/en/guide/basics/routing.html#post)
|
||||
- [PUT](/en/guide/basics/routing.html#put)
|
||||
- [HEAD](/en/guide/basics/routing.html#head)
|
||||
- [OPTIONS](/en/guide/basics/routing.html#options)
|
||||
- [PATCH](/en/guide/basics/routing.html#patch)
|
||||
- [DELETE](/en/guide/basics/routing.html#delete)
|
||||
|
||||
See [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) for more.
|
||||
|
||||
.. column::
|
||||
|
||||
There are, however, two more "standard" HTTP methods: `TRACE` and `CONNECT`. Sanic Extensions will allow you to build
|
||||
endpoints using these methods, which would otherwise not be allowed.
|
||||
|
||||
It is worth pointing out that this will *NOT* enable convenience methods: `@app.trace` or `@app.connect`. You need to
|
||||
use `@app.route` as shown in the example here.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@app.route("/", methods=["trace", "connect"])
|
||||
async def handler(_):
|
||||
return empty()
|
||||
```
|
||||
|
||||
356
guide/content/en/plugins/sanic-ext/injection.md
Normal file
356
guide/content/en/plugins/sanic-ext/injection.md
Normal file
@@ -0,0 +1,356 @@
|
||||
# Dependency Injection
|
||||
|
||||
Dependency injection is a method to add arguments to a route handler based upon the defined function signature. Specifically, it looks at the **type annotations** of the arguments in the handler. This can be useful in a number of cases like:
|
||||
|
||||
- Fetching an object based upon request headers (like the current session user)
|
||||
- Recasting certain objects into a specific type
|
||||
- Using the request object to prefetch data
|
||||
- Auto inject services
|
||||
|
||||
The `Extend` instance has two basic methods on it used for dependency injection: a lower level `add_dependency`, and a higher level `dependency`.
|
||||
|
||||
**Lower level**: `app.ext.add_dependency(...)`
|
||||
|
||||
- `type: Type,`: some unique class that will be the type of the object
|
||||
- `constructor: Optional[Callable[..., Any]],` (OPTIONAL): a function that will return that type
|
||||
|
||||
**Higher level**: `app.ext.dependency(...)`
|
||||
|
||||
- `obj: Any`: any object that you would like injected
|
||||
- `name: Optional[str]`: some name that could alternately be used as a reference
|
||||
|
||||
Let's explore some use cases here.
|
||||
|
||||
|
||||
.. warning::
|
||||
|
||||
If you used dependency injection prior to v21.12, the lower level API method was called `injection`. It has since been renamed to `add_dependency` and starting in v21.12 `injection` is an alias for `add_dependency`. The `injection` method has been deprecated for removal in v22.6.
|
||||
|
||||
|
||||
|
||||
## Basic implementation
|
||||
|
||||
The simplest use case would be simply to recast a value.
|
||||
|
||||
.. column::
|
||||
|
||||
This could be useful if you have a model that you want to generate based upon the matched path parameters.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class IceCream:
|
||||
flavor: str
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.flavor.title()} (Yum!)"
|
||||
|
||||
app.ext.add_dependency(IceCream)
|
||||
|
||||
@app.get("/<flavor:str>")
|
||||
async def ice_cream(request, flavor: IceCream):
|
||||
return text(f"You chose: {flavor}")
|
||||
```
|
||||
|
||||
```
|
||||
$ curl localhost:8000/chocolate
|
||||
You chose Chocolate (Yum!)
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
This works by passing a keyword argument to the constructor of the `type` argument. The previous example is equivalent to this.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
flavor = IceCream(flavor="chocolate")
|
||||
```
|
||||
|
||||
## Additional constructors
|
||||
|
||||
.. column::
|
||||
|
||||
Sometimes you may need to also pass a constructor. This could be a function, or perhaps even a classmethod that acts as a constructor. In this example, we are creating an injection that will call `Person.create` first.
|
||||
|
||||
Also important to note on this example, we are actually injecting **two (2)** objects! It of course does not need to be this way, but we will inject objects based upon the function signature.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class PersonID:
|
||||
person_id: int
|
||||
|
||||
@dataclass
|
||||
class Person:
|
||||
person_id: PersonID
|
||||
name: str
|
||||
age: int
|
||||
|
||||
@classmethod
|
||||
async def create(cls, request: Request, person_id: int):
|
||||
return cls(person_id=PersonID(person_id), name="noname", age=111)
|
||||
|
||||
|
||||
app.ext.add_dependency(Person, Person.create)
|
||||
app.ext.add_dependency(PersonID)
|
||||
|
||||
@app.get("/person/<person_id:int>")
|
||||
async def person_details(
|
||||
request: Request, person_id: PersonID, person: Person
|
||||
):
|
||||
return text(f"{person_id}\n{person}")
|
||||
```
|
||||
|
||||
```
|
||||
$ curl localhost:8000/person/123
|
||||
PersonID(person_id=123)
|
||||
Person(person_id=PersonID(person_id=123), name='noname', age=111)
|
||||
```
|
||||
|
||||
When a `constructor` is passed to `ext.add_dependency` (like in this example) that will be called. If not, then the object will be created by calling the `type`. A couple of important things to note about passing a `constructor`:
|
||||
|
||||
1. A positional `request: Request` argument is *usually* expected. See the `Person.create` method above as an example using a `request` and [arbitrary constructors](#arbitrary-constructors) for how to use a callable that does not require a `request`.
|
||||
1. All matched path parameters are injected as keyword arguments.
|
||||
1. Dependencies can be chained and nested. Notice how in the previous example the `Person` dataclass has a `PersonID`? That means that `PersonID` will be called first, and that value is added to the keyword arguments when calling `Person.create`.
|
||||
|
||||
## Arbitrary constructors
|
||||
|
||||
.. column::
|
||||
|
||||
Sometimes you may want to construct your injectable _without_ the `Request` object. This is useful if you have arbitrary classes or functions that create your objects. If the callable does have any required arguments, then they should themselves be injectable objects.
|
||||
|
||||
This is very useful if you have services or other types of objects that should only exist for the lifetime of a single request. For example, you might use this pattern to pull a single connection from your database pool.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
class Alpha:
|
||||
...
|
||||
|
||||
class Beta:
|
||||
def __init__(self, alpha: Alpha) -> None:
|
||||
self.alpha = alpha
|
||||
|
||||
app.ext.add_dependency(Alpha)
|
||||
app.ext.add_dependency(Beta)
|
||||
|
||||
@app.get("/beta")
|
||||
async def handler(request: Request, beta: Beta):
|
||||
assert isinstance(beta.alpha, Alpha)
|
||||
```
|
||||
|
||||
*Added in v22.9*
|
||||
|
||||
## Objects from the `Request`
|
||||
|
||||
.. column::
|
||||
|
||||
Sometimes you may want to extract details from the request and preprocess them. You could, for example, cast the request JSON to a Python object, and then add some additional logic based upon DB queries.
|
||||
|
||||
.. warning::
|
||||
|
||||
If you plan to use this method, you should note that the injection actually happens *before* Sanic has had a chance to read the request body. The headers should already have been consumed. So, if you do want access to the body, you will need to manually consume as seen in this example.
|
||||
|
||||
```python
|
||||
await request.receive_body()
|
||||
```
|
||||
|
||||
|
||||
This could be used in cases where you otherwise might:
|
||||
|
||||
- use middleware to preprocess and add something to the `request.ctx`
|
||||
- use decorators to preprocess and inject arguments into the request handler
|
||||
|
||||
In this example, we are using the `Request` object in the `compile_profile` constructor to run a fake DB query to generate and return a `UserProfile` object.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class User:
|
||||
name: str
|
||||
|
||||
@dataclass
|
||||
class UserProfile:
|
||||
user: User
|
||||
age: int = field(default=0)
|
||||
email: str = field(default="")
|
||||
|
||||
def __json__(self):
|
||||
return ujson.dumps(
|
||||
{
|
||||
"name": self.user.name,
|
||||
"age": self.age,
|
||||
"email": self.email,
|
||||
}
|
||||
)
|
||||
|
||||
async def fake_request_to_db(body):
|
||||
today = date.today()
|
||||
email = f'{body["name"]}@something.com'.lower()
|
||||
difference = today - date.fromisoformat(body["birthday"])
|
||||
age = int(difference.days / 365)
|
||||
return UserProfile(
|
||||
User(body["name"]),
|
||||
age=age,
|
||||
email=email,
|
||||
)
|
||||
|
||||
async def compile_profile(request: Request):
|
||||
await request.receive_body()
|
||||
profile = await fake_request_to_db(request.json)
|
||||
return profile
|
||||
|
||||
app.ext.add_dependency(UserProfile, compile_profile)
|
||||
|
||||
@app.patch("/profile")
|
||||
async def update_profile(request, profile: UserProfile):
|
||||
return json(profile)
|
||||
```
|
||||
|
||||
```
|
||||
$ curl localhost:8000/profile -X PATCH -d '{"name": "Alice", "birthday": "2000-01-01"}'
|
||||
{
|
||||
"name":"Alice",
|
||||
"age":21,
|
||||
"email":"alice@something.com"
|
||||
}
|
||||
```
|
||||
|
||||
## Injecting services
|
||||
|
||||
It is a common pattern to create things like database connection pools and store them on the `app.ctx` object. This makes them available throughout your application, which is certainly a convenience. One downside, however, is that you no longer have a typed object to work with. You can use dependency injections to fix this. First we will show the concept using the lower level `add_dependency` like we have been using in the previous examples. But, there is a better way using the higher level `dependency` method.
|
||||
|
||||
### The lower level API using `add_dependency`
|
||||
|
||||
.. column::
|
||||
|
||||
|
||||
This works very similar to the [last example](#objects-from-the-request) where the goal is the extract something from the `Request` object. In this example, a database object was created on the `app.ctx` instance, and is being returned in the dependency injection constructor.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
class FakeConnection:
|
||||
async def execute(self, query: str, **arguments):
|
||||
return "result"
|
||||
|
||||
@app.before_server_start
|
||||
async def setup_db(app, _):
|
||||
app.ctx.db_conn = FakeConnection()
|
||||
app.ext.add_dependency(FakeConnection, get_db)
|
||||
|
||||
def get_db(request: Request):
|
||||
return request.app.ctx.db_conn
|
||||
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def handler(request, conn: FakeConnection):
|
||||
response = await conn.execute("...")
|
||||
return text(response)
|
||||
```
|
||||
```
|
||||
$ curl localhost:8000/
|
||||
result
|
||||
```
|
||||
|
||||
### The higher level API using `dependency`
|
||||
|
||||
.. column::
|
||||
|
||||
Since we have an actual *object* that is available when adding the dependency injection, we can use the higher level `dependency` method. This will make the pattern much easier to write.
|
||||
|
||||
This method should always be used when you want to inject something that exists throughout the lifetime of the application instance and is not request specific. It is very useful for services, third party clients, and connection pools since they are not request specific.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
class FakeConnection:
|
||||
async def execute(self, query: str, **arguments):
|
||||
return "result"
|
||||
|
||||
@app.before_server_start
|
||||
async def setup_db(app, _):
|
||||
db_conn = FakeConnection()
|
||||
app.ext.dependency(db_conn)
|
||||
|
||||
@app.get("/")
|
||||
async def handler(request, conn: FakeConnection):
|
||||
response = await conn.execute("...")
|
||||
return text(response)
|
||||
```
|
||||
```
|
||||
$ curl localhost:8000/
|
||||
result
|
||||
```
|
||||
|
||||
## Generic types
|
||||
|
||||
Be carefule when using a [generic type](https://docs.python.org/3/library/typing.html#typing.Generic). The way that Sanic's dependency injection works is by matching the entire type definition. Therefore, `Foo` is not the same as `Foo[str]`. This can be particularly tricky when trying to use the [higher-level `dependency` method](#the-higher-level-api-using-dependency) since the type is inferred.
|
||||
|
||||
.. column::
|
||||
|
||||
For example, this will **NOT** work as expected since there is no definition for `Test[str]`.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
import typing
|
||||
from sanic import Sanic, text
|
||||
|
||||
T = typing.TypeVar("T")
|
||||
|
||||
class Test(typing.Generic[T]):
|
||||
test: T
|
||||
|
||||
app = Sanic("testapp")
|
||||
app.ext.dependency(Test())
|
||||
|
||||
@app.get("/")
|
||||
def test(request, test: Test[str]):
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
To get this example to work, you will need to add an explicit definition for the type you intend to be injected.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
import typing
|
||||
from sanic import Sanic, text
|
||||
|
||||
T = typing.TypeVar("T")
|
||||
|
||||
class Test(typing.Generic[T]):
|
||||
test: T
|
||||
|
||||
app = Sanic("testapp")
|
||||
_singleton = Test()
|
||||
app.ext.add_dependency(Test[str], lambda: _singleton)
|
||||
|
||||
@app.get("/")
|
||||
def test(request, test: Test[str]):
|
||||
...
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
.. column::
|
||||
|
||||
By default, dependencies will be injected after the `http.routing.after` [signal](../../guide/advanced/signals.md#built-in-signals). Starting in v22.9, you can change this to the `http.handler.before` signal.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
app.config.INJECTION_SIGNAL = "http.handler.before"
|
||||
```
|
||||
|
||||
*Added in v22.9*
|
||||
30
guide/content/en/plugins/sanic-ext/logger.md
Normal file
30
guide/content/en/plugins/sanic-ext/logger.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Background logger
|
||||
|
||||
The background logger requires both `sanic>=22.9` and `sanic-ext>=22.9`.
|
||||
|
||||
You can setup Sanic Extensions to log all of your messages from a background process. This requires that you not be in [single process mode](../../guide/deployment/manager.md#single-process-mode).
|
||||
|
||||
Logging can sometimes be an expensive operation. By pushing all logging off to a background process, you can potentially gain some performance benefits.
|
||||
|
||||
## Setup
|
||||
|
||||
.. column::
|
||||
|
||||
Out of the box, the background logger is disabled. You will need to opt-in if you would like to use it.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
app.config.LOGGING = True
|
||||
```
|
||||
|
||||
## How does it work
|
||||
|
||||
When enabled, the extension will create a `multoprocessing.Queue`. It will remove all handlers on the [default Sanic loggers](../../guide/best-practices/logging.md) and replace them with a [`QueueHandler`](https://docs.python.org/3/library/logging.handlers.html#queuehandler). When a message is logged, it will be pushed into the queue by the handler, and read by the background process to the log handlers that were originally in place. This means you can still configure logging as normal and it should "just work."
|
||||
|
||||
## Configuration
|
||||
|
||||
| Key | Type | Default| Description |
|
||||
|--|--|--|--|
|
||||
| LOGGING | `bool` | `False` | Whether to enable this extension. |
|
||||
| LOGGING_QUEUE_MAX_SIZE | `int` | `4096` | The max size of the queue before messages are rejected. |
|
||||
7
guide/content/en/plugins/sanic-ext/openapi.md
Normal file
7
guide/content/en/plugins/sanic-ext/openapi.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Openapi
|
||||
|
||||
- Adding documentation with decorators
|
||||
- Documenting CBV
|
||||
- Using autodoc
|
||||
- Rendering docs with redoc/swagger
|
||||
- Validation
|
||||
9
guide/content/en/plugins/sanic-ext/openapi/advanced.md
Normal file
9
guide/content/en/plugins/sanic-ext/openapi/advanced.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Advanced
|
||||
|
||||
_Documentation coming EOQ1 2023_
|
||||
|
||||
## CBV
|
||||
|
||||
## Blueprints
|
||||
|
||||
## Components
|
||||
141
guide/content/en/plugins/sanic-ext/openapi/autodoc.md
Normal file
141
guide/content/en/plugins/sanic-ext/openapi/autodoc.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Auto-documentation
|
||||
|
||||
To make documenting endpoints easier, Sanic Extensions will use a function's docstring to populate your documentation.
|
||||
|
||||
## Summary and description
|
||||
|
||||
.. column::
|
||||
|
||||
A function's docstring will be used to create the summary and description. As you can see from this example here, the docstring has been parsed to use the first line as the summary, and the remainder of the string as the description.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@app.get("/foo")
|
||||
async def handler(request, something: str):
|
||||
"""This is a simple foo handler
|
||||
|
||||
It is helpful to know that you could also use **markdown** inside your
|
||||
docstrings.
|
||||
|
||||
- one
|
||||
- two
|
||||
- three"""
|
||||
return text(">>>")
|
||||
```
|
||||
```json
|
||||
"paths": {
|
||||
"/foo": {
|
||||
"get": {
|
||||
"summary": "This is a simple foo handler",
|
||||
"description": "It is helpful to know that you could also use **markdown** inside your<br>docstrings.<br><br>- one<br>- two<br>- three",
|
||||
"responses": {
|
||||
"default": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"operationId": "get_handler"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Operation level YAML
|
||||
|
||||
.. column::
|
||||
|
||||
You can expand upon this by adding valid OpenAPI YAML to the docstring. Simply add a line that contains `openapi:`, followed by your YAML.
|
||||
|
||||
The `---` shown in the example is *not* necessary. It is just there to help visually identify the YAML as a distinct section of the docstring.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@app.get("/foo")
|
||||
async def handler(request, something: str):
|
||||
"""This is a simple foo handler
|
||||
|
||||
Now we will add some more details
|
||||
|
||||
openapi:
|
||||
---
|
||||
operationId: fooDots
|
||||
tags:
|
||||
- one
|
||||
- two
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
description: How many items to return at one time (max 100)
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
responses:
|
||||
'200':
|
||||
description: Just some dots
|
||||
"""
|
||||
return text("...")
|
||||
```
|
||||
```json
|
||||
"paths": {
|
||||
"/foo": {
|
||||
"get": {
|
||||
"operationId": "fooDots",
|
||||
"summary": "This is a simple foo handler",
|
||||
"description": "Now we will add some more details",
|
||||
"tags": [
|
||||
"one",
|
||||
"two"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"description": "How many items to return at one time (max 100)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Just some dots"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
When both YAML documentation and decorators are used, it is the content from the decorators that will take priority when generating the documentation.
|
||||
|
||||
|
||||
|
||||
## Excluding docstrings
|
||||
|
||||
.. column::
|
||||
|
||||
Sometimes a function may contain a docstring that is not meant to be consumed inside the documentation.
|
||||
|
||||
**Option 1**: Globally turn off auto-documentation `app.config.OAS_AUTODOC = False`
|
||||
|
||||
**Option 2**: Disable it for the single handler with the `@openapi.no_autodoc` decorator
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@app.get("/foo")
|
||||
@openapi.no_autodoc
|
||||
async def handler(request, something: str):
|
||||
"""This is a docstring about internal info only. Do not parse it.
|
||||
"""
|
||||
return text("...")
|
||||
```
|
||||
|
||||
70
guide/content/en/plugins/sanic-ext/openapi/basics.md
Normal file
70
guide/content/en/plugins/sanic-ext/openapi/basics.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Basics
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
The OpenAPI implementation in Sanic Extensions is based upon the OAS3 implementation from [`sanic-openapi`](https://github.com/sanic-org/sanic-openapi). In fact, Sanic Extensions is in a large way the successor to that project, which entered maintenance mode upon the release of Sanic Extensions. If you were previously using OAS3 with `sanic-openapi` you should have an easy path to upgrading to Sanic Extensions. Unfortunately, this project does *NOT* support the OAS2 specification.
|
||||
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
Out of the box, Sanic Extensions provides automatically generated API documentation using the [v3.0 OpenAPI specification](https://swagger.io/specification/). There is nothing special that you need to do
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic("MyApp")
|
||||
|
||||
# Add all of your views
|
||||
```
|
||||
|
||||
After doing this, you will now have beautiful documentation already generated for you based upon your existing application:
|
||||
|
||||
- [http://localhost:8000/docs](http://localhost:8000/docs)
|
||||
- [http://localhost:8000/docs/redoc](http://localhost:8000/docs/redoc)
|
||||
- [http://localhost:8000/docs/swagger](http://localhost:8000/docs/swagger)
|
||||
|
||||
Checkout the [section on configuration](../configuration.md) to learn about changing the routes for the docs. You can also turn off one of the two UIs, and customize which UI will be available on the `/docs` route.
|
||||
|
||||
.. column::
|
||||
|
||||
Using [Redoc](https://github.com/Redocly/redoc)
|
||||
|
||||

|
||||
|
||||
.. column::
|
||||
|
||||
or [Swagger UI](https://github.com/swagger-api/swagger-ui)
|
||||
|
||||

|
||||
|
||||
## Changing specification metadata
|
||||
|
||||
.. column::
|
||||
|
||||
If you want to change any of the metada, you should use the `describe` method.
|
||||
|
||||
In this example `dedent` is being used with the `description` argument to make multi-line strings a little cleaner. This is not necessary, you can pass any string value here.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from textwrap import dedent
|
||||
|
||||
app.ext.openapi.describe(
|
||||
"Testing API",
|
||||
version="1.2.3",
|
||||
description=dedent(
|
||||
"""
|
||||
# Info
|
||||
This is a description. It is a good place to add some _extra_ doccumentation.
|
||||
|
||||
**MARKDOWN** is supported.
|
||||
"""
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
468
guide/content/en/plugins/sanic-ext/openapi/decorators.md
Normal file
468
guide/content/en/plugins/sanic-ext/openapi/decorators.md
Normal file
@@ -0,0 +1,468 @@
|
||||
# Decorators
|
||||
|
||||
The primary mechanism for adding content to your schema is by decorating your endpoints. If you have
|
||||
used `sanic-openapi` in the past, this should be familiar to you. The decorators and their arguments match closely
|
||||
the [OAS v3.0 specification](https://swagger.io/specification/).
|
||||
|
||||
.. column::
|
||||
|
||||
All of the examples show will wrap around a route definition. When you are creating these, you should make sure that
|
||||
your Sanic route decorator (`@app.route`, `@app.get`, etc) is the outermost decorator. That is to say that you should
|
||||
put that first and then one or more of the below decorators after.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import openapi
|
||||
|
||||
@app.get("/path/to/<something>")
|
||||
@openapi.summary("This is a summary")
|
||||
@openapi.description("This is a description")
|
||||
async def handler(request, something: str):
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
You will also see a lot of the below examples reference a model object. For the sake of simplicity, the examples will
|
||||
use `UserProfile` that will look like this. The point is that it can be any well-typed class. You could easily imagine
|
||||
this being a `dataclass` or some other kind of model object.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
class UserProfile:
|
||||
name: str
|
||||
age: int
|
||||
email: str
|
||||
```
|
||||
|
||||
## Definition decorator
|
||||
|
||||
### `@openapi.definition`
|
||||
|
||||
The `@openapi.definition` decorator allows you to define all parts of an operations on a path at once. It is an omnibums
|
||||
decorator in that it has the same capabilities to create operation definitions as the rest of the decorators. Using
|
||||
multiple field-specific decorators or a single decorator is a style choice for you the developer.
|
||||
|
||||
The fields are purposely permissive in accepting multiple types to make it easiest for you to define your operation.
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Field | Type |
|
||||
| ------------- | --------------------------------------------------------------------------|
|
||||
| `body` | **dict, RequestBody, *YourModel*** |
|
||||
| `deprecated` | **bool** |
|
||||
| `description` | **str** |
|
||||
| `document` | **str, ExternalDocumentation** |
|
||||
| `exclude` | **bool** |
|
||||
| `operation` | **str** |
|
||||
| `parameter` | **str, dict, Parameter, [str], [dict], [Parameter]** |
|
||||
| `response` | **dict, Response, *YourModel*, [dict], [Response]** |
|
||||
| `summary` | **str** |
|
||||
| `tag` | **str, Tag, [str], [Tag]** |
|
||||
| `secured` | **Dict[str, Any]** |
|
||||
|
||||
**Examples**
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.definition(
|
||||
body=RequestBody(UserProfile, required=True),
|
||||
summary="User profile update",
|
||||
tag="one",
|
||||
response=[Success, Response(Failure, status=400)],
|
||||
)
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
|
||||
|
||||
*See below examples for more examples. Any of the values for the below decorators can be used in the corresponding
|
||||
keyword argument.*
|
||||
|
||||
## Field-specific decorators
|
||||
|
||||
All the following decorators are based on `@openapi`
|
||||
|
||||
### body
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Field | Type |
|
||||
| ----------- | ---------------------------------- |
|
||||
| **content** | ***YourModel*, dict, RequestBody** |
|
||||
|
||||
**Examples**
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.body(UserProfile)
|
||||
```
|
||||
|
||||
```python
|
||||
@openapi.body({"application/json": UserProfile})
|
||||
```
|
||||
|
||||
```python
|
||||
@openapi.body(RequestBody({"application/json": UserProfile}))
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.body({"content": UserProfile})
|
||||
```
|
||||
|
||||
```python
|
||||
@openapi.body(RequestBody(UserProfile))
|
||||
```
|
||||
|
||||
```python
|
||||
@openapi.body({"application/json": {"description": ...}})
|
||||
```
|
||||
|
||||
### deprecated
|
||||
|
||||
**Arguments**
|
||||
|
||||
*None*
|
||||
|
||||
**Examples**
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.deprecated()
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.deprecated
|
||||
```
|
||||
|
||||
### description
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Field | Type |
|
||||
| ------ | ------- |
|
||||
| `text` | **str** |
|
||||
|
||||
**Examples**
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.description(
|
||||
"""This is a **description**.
|
||||
|
||||
## You can use `markdown`
|
||||
|
||||
- And
|
||||
- make
|
||||
- lists.
|
||||
"""
|
||||
)
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
|
||||
### document
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Field | Type |
|
||||
| ------------- | ------- |
|
||||
| `url` | **str** |
|
||||
| `description` | **str** |
|
||||
|
||||
**Examples**
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.document("http://example.com/docs")
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.document(ExternalDocumentation("http://example.com/more"))
|
||||
```
|
||||
|
||||
### exclude
|
||||
|
||||
Can be used on route definitions like all of the other decorators, or can be called on a Blueprint
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Field | Type | Default |
|
||||
| ------ | ------------- | -------- |
|
||||
| `flag` | **bool** | **True** |
|
||||
| `bp` | **Blueprint** | |
|
||||
|
||||
**Examples**
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.exclude()
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
openapi.exclude(bp=some_blueprint)
|
||||
```
|
||||
|
||||
### operation
|
||||
|
||||
Sets the operation ID.
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Field | Type |
|
||||
| ------ | ------- |
|
||||
| `name` | **str** |
|
||||
|
||||
**Examples**
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.operation("doNothing")
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
|
||||
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Field | Type | Default |
|
||||
| ---------- | ----------------------------------------- | ----------- |
|
||||
| `name` | **str** | |
|
||||
| `schema` | ***type*** | **str** |
|
||||
| `location` | **"query", "header", "path" or "cookie"** | **"query"** |
|
||||
|
||||
**Examples**
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.parameter("thing")
|
||||
```
|
||||
|
||||
```python
|
||||
@openapi.parameter(parameter=Parameter("foobar", deprecated=True))
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.parameter("Authorization", str, "header")
|
||||
```
|
||||
|
||||
```python
|
||||
@openapi.parameter("thing", required=True, allowEmptyValue=False)
|
||||
```
|
||||
|
||||
### response
|
||||
|
||||
**Arguments**
|
||||
|
||||
If using a `Response` object, you should not pass any other arguments.
|
||||
|
||||
| Field | Type |
|
||||
| ------------- | ----------------------------- |
|
||||
| `status` | **int** |
|
||||
| `content` | ***type*, *YourModel*, dict** |
|
||||
| `description` | **str** |
|
||||
| `response` | **Response** |
|
||||
|
||||
**Examples**
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.response(200, str, "This is endpoint returns a string")
|
||||
```
|
||||
|
||||
```python
|
||||
@openapi.response(200, {"text/plain": str}, "...")
|
||||
```
|
||||
|
||||
```python
|
||||
@openapi.response(response=Response(UserProfile, description="..."))
|
||||
```
|
||||
|
||||
```python
|
||||
@openapi.response(
|
||||
response=Response(
|
||||
{
|
||||
"application/json": UserProfile,
|
||||
},
|
||||
description="...",
|
||||
status=201,
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.response(200, UserProfile, "...")
|
||||
```
|
||||
|
||||
```python
|
||||
@openapi.response(
|
||||
200,
|
||||
{
|
||||
"application/json": UserProfile,
|
||||
},
|
||||
"Description...",
|
||||
)
|
||||
```
|
||||
|
||||
### summary
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Field | Type |
|
||||
| ------ | ------- |
|
||||
| `text` | **str** |
|
||||
|
||||
**Examples**
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.summary("This is an endpoint")
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
|
||||
### tag
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Field | Type |
|
||||
| ------- | ------------ |
|
||||
| `*args` | **str, Tag** |
|
||||
|
||||
**Examples**
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.tag("foo")
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.tag("foo", Tag("bar"))
|
||||
```
|
||||
|
||||
|
||||
### secured
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Field | Type |
|
||||
| ----------------- | ----------------------- |
|
||||
| `*args, **kwargs` | **str, Dict[str, Any]** |
|
||||
|
||||
**Examples**
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.secured()
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.secured("foo")
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.secured("token1", "token2")
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.secured({"my_api_key": []})
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@openapi.secured(my_api_key=[])
|
||||
```
|
||||
|
||||
Do not forget to use `add_security_scheme`. See [security](./security.md) for more details.
|
||||
``
|
||||
|
||||
## Integration with Pydantic
|
||||
|
||||
Pydantic models have the ability to [generate OpenAPI schema](https://pydantic-docs.helpmanual.io/usage/schema/).
|
||||
|
||||
.. column::
|
||||
|
||||
To take advantage of Pydantic model schema generation, pass the output in place of the schema.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic import Sanic, json
|
||||
from sanic_ext import validate, openapi
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@openapi.component
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
description: str = None
|
||||
price: float
|
||||
tax: float = None
|
||||
|
||||
class ItemList(BaseModel):
|
||||
items: List[Item]
|
||||
|
||||
app = Sanic("test")
|
||||
|
||||
@app.get("/")
|
||||
@openapi.definition(
|
||||
body={
|
||||
"application/json": ItemList.schema(
|
||||
ref_template="#/components/schemas/{model}"
|
||||
)
|
||||
},
|
||||
)
|
||||
async def get(request):
|
||||
return json({})
|
||||
```
|
||||
|
||||
.. note::
|
||||
|
||||
It is important to set that `ref_template`. By default Pydantic will select a template that is not standard OAS. This will cause the schema to not be found when generating the final document.
|
||||
|
||||
*Added in v22.9*
|
||||
96
guide/content/en/plugins/sanic-ext/openapi/security.md
Normal file
96
guide/content/en/plugins/sanic-ext/openapi/security.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Security Schemes
|
||||
|
||||
To document authentication schemes, there are two steps.
|
||||
|
||||
_Security is only available starting in v21.12.2_
|
||||
|
||||
## Document the scheme
|
||||
|
||||
.. column::
|
||||
|
||||
The first thing that you need to do is define one or more security schemes. The basic pattern will be to define it as:
|
||||
|
||||
```python
|
||||
add_security_scheme("<NAME>", "<TYPE>")
|
||||
```
|
||||
|
||||
The `type` should correspond to one of the allowed security schemes: `"apiKey"`, `"http"`, `"oauth2"`, `"openIdConnect"`. You can then pass appropriate keyword arguments as allowed by the specification.
|
||||
|
||||
You should consult the [OpenAPI Specification](https://swagger.io/specification/) for details on what values are appropriate.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
app.ext.openapi.add_security_scheme("api_key", "apiKey")
|
||||
app.ext.openapi.add_security_scheme(
|
||||
"token",
|
||||
"http",
|
||||
scheme="bearer",
|
||||
bearer_format="JWT",
|
||||
)
|
||||
app.ext.openapi.add_security_scheme("token2", "http")
|
||||
app.ext.openapi.add_security_scheme(
|
||||
"oldschool",
|
||||
"http",
|
||||
scheme="basic",
|
||||
)
|
||||
app.ext.openapi.add_security_scheme(
|
||||
"oa2",
|
||||
"oauth2",
|
||||
flows={
|
||||
"implicit": {
|
||||
"authorizationUrl": "http://example.com/auth",
|
||||
"scopes": {
|
||||
"on:two": "something",
|
||||
"three:four": "something else",
|
||||
"threefour": "something else...",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Document the endpoints
|
||||
|
||||
.. column::
|
||||
|
||||
There are two options, document _all_ endpoints.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
app.ext.openapi.secured()
|
||||
app.ext.openapi.secured("token")
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
Or, document only specific routes.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@app.route("/one")
|
||||
async def handler1(request):
|
||||
"""
|
||||
openapi:
|
||||
---
|
||||
security:
|
||||
- foo: []
|
||||
"""
|
||||
|
||||
@app.route("/two")
|
||||
@openapi.secured("foo")
|
||||
@openapi.secured({"bar": []})
|
||||
@openapi.secured(baz=[])
|
||||
async def handler2(request):
|
||||
...
|
||||
|
||||
@app.route("/three")
|
||||
@openapi.definition(secured="foo")
|
||||
@openapi.definition(secured={"bar": []})
|
||||
async def handler3(request):
|
||||
...
|
||||
```
|
||||
|
||||
26
guide/content/en/plugins/sanic-ext/openapi/ui.md
Normal file
26
guide/content/en/plugins/sanic-ext/openapi/ui.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# UI
|
||||
|
||||
Sanic Extensions comes with both Redoc and Swagger interfaces. You have a choice to use one, or both of them. Out of the box, the following endpoints are setup for you, with the bare `/docs` displaying Redoc.
|
||||
|
||||
- `/docs`
|
||||
- `/docs/openapi.json`
|
||||
- `/docs/redoc`
|
||||
- `/docs/swagger`
|
||||
- `/docs/openapi-config`
|
||||
|
||||
## Config options
|
||||
|
||||
| **Key** | **Type** | **Default** | **Desctiption** |
|
||||
| -------------------------- | --------------- | ------------------- | ------------------------------------------------------------ |
|
||||
| `OAS_IGNORE_HEAD` | `bool` | `True` | Whether to display `HEAD` endpoints. |
|
||||
| `OAS_IGNORE_OPTIONS` | `bool` | `True` | Whether to display `OPTIONS` endpoints. |
|
||||
| `OAS_PATH_TO_REDOC_HTML` | `Optional[str]` | `None` | Path to HTML to override the default Redoc HTML |
|
||||
| `OAS_PATH_TO_SWAGGER_HTML` | `Optional[str]` | `None` | Path to HTML to override the default Swagger HTML |
|
||||
| `OAS_UI_DEFAULT` | `Optional[str]` | `"redoc"` | Can be set to `redoc` or `swagger`. Controls which UI to display on the base route. If set to `None`, then the base route will not be setup. |
|
||||
| `OAS_UI_REDOC` | `bool` | `True` | Whether to enable Redoc UI. |
|
||||
| `OAS_UI_SWAGGER` | `bool` | `True` | Whether to enable Swagger UI. |
|
||||
| `OAS_URI_TO_CONFIG` | `str` | `"/openapi-config"` | URI path to the OpenAPI config used by Swagger |
|
||||
| `OAS_URI_TO_JSON` | `str` | `"/openapi.json"` | URI path to the JSON document. |
|
||||
| `OAS_URI_TO_REDOC` | `str` | `"/redoc"` | URI path to Redoc. |
|
||||
| `OAS_URI_TO_SWAGGER` | `str` | `"/swagger"` | URI path to Swagger. |
|
||||
| `OAS_URL_PREFIX` | `str` | `"/docs"` | URL prefix to use for the Blueprint for OpenAPI docs. |
|
||||
@@ -0,0 +1 @@
|
||||
# Coming soon
|
||||
153
guide/content/en/plugins/sanic-ext/templating/jinja.md
Normal file
153
guide/content/en/plugins/sanic-ext/templating/jinja.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Templating
|
||||
|
||||
Sanic Extensions can easily help you integrate templates into your route handlers.
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Currently, we only support [Jinja](https://github.com/pallets/jinja/).**
|
||||
|
||||
[Read the Jinja docs first](https://jinja.palletsprojects.com/en/3.1.x/) if you are unfamiliar with how to create templates.
|
||||
|
||||
Sanic Extensions will automatically setup and load Jinja for you if it is installed in your environment. Therefore, the only setup that you need to do is install Jinja:
|
||||
|
||||
```
|
||||
pip install Jinja2
|
||||
```
|
||||
|
||||
## Rendering a template from a file
|
||||
|
||||
There are three (3) ways for you:
|
||||
|
||||
1. Using a decorator to pre-load the template file
|
||||
1. Returning a rendered `HTTPResponse` object
|
||||
1. Hybrid pattern that creates a `LazyResponse`
|
||||
|
||||
Let's imagine you have a file called `./templates/foo.html`:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>My Webpage</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Hello, world!!!!</h1>
|
||||
<ul>
|
||||
{% for item in seq %}
|
||||
<li>{{ item }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
```
|
||||
|
||||
Let's see how you could render it with Sanic + Jinja.
|
||||
|
||||
### Option 1 - as a decorator
|
||||
|
||||
.. column::
|
||||
|
||||
The benefit of this approach is that the templates can be predefined at startup time. This will mean that less fetching needs to happen in the handler, and should therefore be the fastest option.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@app.get("/")
|
||||
@app.ext.template("foo.html")
|
||||
async def handler(request: Request):
|
||||
return {"seq": ["one", "two"]}
|
||||
```
|
||||
|
||||
### Option 2 - as a return object
|
||||
|
||||
.. column::
|
||||
|
||||
This is meant to mimic the `text`, `json`, `html`, `file`, etc pattern of core Sanic. It will allow the most customization to the response object since it has direct control of it. Just like in other `HTTPResponse` objects, you can control headers, cookies, etc.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import render
|
||||
|
||||
@app.get("/alt")
|
||||
async def handler(request: Request):
|
||||
return await render(
|
||||
"foo.html", context={"seq": ["three", "four"]}, status=400
|
||||
)
|
||||
```
|
||||
|
||||
### Option 3 - hybrid/lazy
|
||||
|
||||
.. column::
|
||||
|
||||
In this approach, the template is defined up front and not inside the handler (for performance). Then, the `render` function returns a `LazyResponse` that can be used to build a proper `HTTPResponse` inside the decorator.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import render
|
||||
|
||||
@app.get("/")
|
||||
@app.ext.template("foo.html")
|
||||
async def handler(request: Request):
|
||||
return await render(context={"seq": ["five", "six"]}, status=400)
|
||||
```
|
||||
|
||||
## Rendering a template from a string
|
||||
|
||||
.. column::
|
||||
|
||||
Sometimes you may want to write (or generate) your template inside of Python code and _not_ read it from an HTML file. In this case, you can still use the `render` function we saw above. Just use `template_source`.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import render
|
||||
from textwrap import dedent
|
||||
|
||||
@app.get("/")
|
||||
async def handler(request):
|
||||
template = dedent("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>My Webpage</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Hello, world!!!!</h1>
|
||||
<ul>
|
||||
{% for item in seq %}
|
||||
<li>{{ item }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
""")
|
||||
return await render(
|
||||
template_source=template,
|
||||
context={"seq": ["three", "four"]},
|
||||
app=app,
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
In this example, we use `textwrap.dedent` to remove the whitespace in the beginning of each line of the multi-line string. It is not necessary, but just a nice touch to keep both the code and the generated source clean.
|
||||
|
||||
|
||||
|
||||
## Development and auto-reload
|
||||
|
||||
If auto-reload is turned on, then changes to your template files should trigger a reload of the server.
|
||||
|
||||
## Configuration
|
||||
|
||||
See `templating_enable_async` and `templating_path_to_templates` in [settings](./configuration.md#settings).
|
||||
164
guide/content/en/plugins/sanic-ext/validation.md
Normal file
164
guide/content/en/plugins/sanic-ext/validation.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Validation
|
||||
|
||||
One of the most commonly implemented features of a web application is user-input validation. For obvious reasons, this is not only a security issue, but also just plain good practice. You want to make sure your data conforms to expectations, and throw a `400` response when it does not.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Validation with Dataclasses
|
||||
|
||||
With the introduction of [Data Classes](https://docs.python.org/3/library/dataclasses.html), Python made it super simple to create objects that meet a defined schema. However, the standard library only supports type checking validation, **not** runtime validation. Sanic Extensions adds the ability to do runtime validations on incoming requests using `dataclasses` out of the box. If you also have either `pydantic` or `attrs` installed, you can alternatively use one of those libraries.
|
||||
|
||||
.. column::
|
||||
|
||||
First, define a model.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class SearchParams:
|
||||
q: str
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
Then, attach it to your route
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import validate
|
||||
|
||||
@app.route("/search")
|
||||
@validate(query=SearchParams)
|
||||
async def handler(request, query: SearchParams):
|
||||
return json(asdict(query))
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
You should now have validation on the incoming request.
|
||||
|
||||
.. column::
|
||||
|
||||
```
|
||||
$ curl localhost:8000/search
|
||||
⚠️ 400 — Bad Request
|
||||
====================
|
||||
Invalid request body: SearchParams. Error: missing a required argument: 'q'
|
||||
```
|
||||
```
|
||||
$ curl localhost:8000/search\?q=python
|
||||
{"q":"python"}
|
||||
```
|
||||
|
||||
### Validation with Pydantic
|
||||
|
||||
You can use Pydantic models also.
|
||||
|
||||
.. column::
|
||||
|
||||
First, define a model.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
class Person(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
Then, attach it to your route
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import validate
|
||||
|
||||
@app.post("/person")
|
||||
@validate(json=Person)
|
||||
async def handler(request, body: Person):
|
||||
return json(body.dict())
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
You should now have validation on the incoming request.
|
||||
|
||||
.. column::
|
||||
|
||||
```
|
||||
$ curl localhost:8000/person -d '{"name": "Alice", "age": 21}' -X POST
|
||||
{"name":"Alice","age":21}
|
||||
```
|
||||
|
||||
### Validation with Attrs
|
||||
|
||||
You can use Attrs also.
|
||||
|
||||
.. column::
|
||||
|
||||
First, define a model.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@attrs.define
|
||||
class Person:
|
||||
name: str
|
||||
age: int
|
||||
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
Then, attach it to your route
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic_ext import validate
|
||||
|
||||
@app.post("/person")
|
||||
@validate(json=Person)
|
||||
async def handler(request, body: Person):
|
||||
return json(attrs.asdict(body))
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
You should now have validation on the incoming request.
|
||||
|
||||
.. column::
|
||||
|
||||
```
|
||||
$ curl localhost:8000/person -d '{"name": "Alice", "age": 21}' -X POST
|
||||
{"name":"Alice","age":21}
|
||||
```
|
||||
|
||||
## What can be validated?
|
||||
|
||||
The `validate` decorator can be used to validate incoming user data from three places: JSON body data (`request.json`), form body data (`request.form`), and query parameters (`request.args`).
|
||||
|
||||
.. column::
|
||||
|
||||
As you might expect, you can attach your model using the keyword arguments of the decorator.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@validate(
|
||||
json=ModelA,
|
||||
query=ModelB,
|
||||
form=ModelC,
|
||||
)
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user