Conversion of User Guide to the SHH stack (#2781)

This commit is contained in:
Adam Hopkins
2023-09-06 15:44:00 +03:00
committed by GitHub
parent 47215d4635
commit d255d1aae1
332 changed files with 51495 additions and 2013 deletions

View 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

View 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).
![](https://user-images.githubusercontent.com/166269/190922460-43bd2cfc-f81a-443b-b84f-07b6ce475cbf.png)

View 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*

View 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).

View 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. |

View 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.")
```

View 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()
```

View 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*

View 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. |

View File

@@ -0,0 +1,7 @@
# Openapi
- Adding documentation with decorators
- Documenting CBV
- Using autodoc
- Rendering docs with redoc/swagger
- Validation

View File

@@ -0,0 +1,9 @@
# Advanced
_Documentation coming EOQ1 2023_
## CBV
## Blueprints
## Components

View 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("...")
```

View 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)
![Redoc](/assets/images/sanic-ext-redoc.png)
.. column::
or [Swagger UI](https://github.com/swagger-api/swagger-ui)
![Swagger UI](/assets/images/sanic-ext-swagger.png)
## 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.
"""
),
)
```

View 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*

View 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):
...
```

View 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. |

View File

@@ -0,0 +1 @@
# Coming soon

View 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).

View 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,
)
```