Compare commits
9 Commits
ruff
...
priority-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dd1948fd2 | ||
|
|
86a414fd58 | ||
|
|
5a7ed4d4fe | ||
|
|
81d986a413 | ||
|
|
f613818263 | ||
|
|
36ea283b42 | ||
|
|
a7766de797 | ||
|
|
b67e31efe8 | ||
|
|
7ac4933386 |
@@ -19,7 +19,7 @@ import sys
|
||||
root_directory = os.path.dirname(os.getcwd())
|
||||
sys.path.insert(0, root_directory)
|
||||
|
||||
import sanic # noqa: E402
|
||||
import sanic
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
@@ -25,6 +25,5 @@ def key_exist_handler(request):
|
||||
|
||||
return text("num does not exist in request")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
@@ -69,6 +69,5 @@ async def runner(app: Sanic, app_server: AsyncioServer):
|
||||
app.is_running = False
|
||||
app.is_stopping = True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
https.run(port=HTTPS_PORT, debug=True)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import requests
|
||||
|
||||
|
||||
# Warning: This is a heavy process.
|
||||
|
||||
data = ""
|
||||
|
||||
@@ -35,7 +35,6 @@ async def after_server_stop(app, loop):
|
||||
async def test(request):
|
||||
return response.json({"answer": "42"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
serv_coro = app.create_server(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Sanic Application
|
||||
|
||||
See API docs: [sanic.app](/api/sanic.app)
|
||||
|
||||
## Instance
|
||||
|
||||
.. column::
|
||||
|
||||
The most basic building block is the `Sanic()` instance. It is not required, but the custom is to instantiate this in a file called `server.py`.
|
||||
The most basic building block is the :class:`sanic.app.Sanic` instance. It is not required, but the custom is to instantiate this in a file called `server.py`.
|
||||
|
||||
.. column::
|
||||
|
||||
@@ -18,33 +20,36 @@
|
||||
|
||||
## Application context
|
||||
|
||||
Most applications will have the need to share/reuse data or objects across different parts of the code base. The most common example is DB connections.
|
||||
Most applications will have the need to share/reuse data or objects across different parts of the code base. Sanic helps be providing the `ctx` object on application instances. It is a free space for the developer to attach any objects or data that should existe throughout the lifetime of the application.
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
In versions of Sanic prior to v21.3, this was commonly done by attaching an attribute to the application instance
|
||||
The most common pattern is to attach a database instance to the application.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
# Raises a warning as deprecated feature in 21.3
|
||||
app = Sanic("MyApp")
|
||||
app.db = Database()
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
Because this can create potential problems with name conflicts, and to be consistent with [request context](./request.md#context) objects, v21.3 introduces application level context object.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
# Correct way to attach objects to the application
|
||||
app = Sanic("MyApp")
|
||||
app.ctx.db = Database()
|
||||
```
|
||||
|
||||
|
||||
.. column::
|
||||
|
||||
While the previous example will work and is illustrative, it is typically considered best practice to attach objects in one of the two application startup [listeners](./listeners).
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
app = Sanic("MyApp")
|
||||
|
||||
@app.before_server_start
|
||||
async def attach_db(app, loop):
|
||||
app.ctx.db = Database()
|
||||
```
|
||||
|
||||
|
||||
## App Registry
|
||||
|
||||
.. column::
|
||||
@@ -68,7 +73,7 @@ Most applications will have the need to share/reuse data or objects across diffe
|
||||
|
||||
.. column::
|
||||
|
||||
If you call `Sanic.get_app("non-existing")` on an app that does not exist, it will raise `SanicException` by default. You can, instead, force the method to return a new instance of Sanic with that name.
|
||||
If you call `Sanic.get_app("non-existing")` on an app that does not exist, it will raise :class:`sanic.exceptions.SanicException` by default. You can, instead, force the method to return a new instance of Sanic with that name.
|
||||
|
||||
.. column::
|
||||
|
||||
@@ -119,17 +124,54 @@ Most applications will have the need to share/reuse data or objects across diffe
|
||||
.. note:: Heads up
|
||||
|
||||
Config keys _should_ be uppercase. But, this is mainly by convention, and lowercase will work most of the time.
|
||||
```
|
||||
```python
|
||||
app.config.GOOD = "yay!"
|
||||
app.config.bad = "boo"
|
||||
```
|
||||
|
||||
|
||||
There is much [more detail about configuration](/guide/deployment/configuration.md) later on.
|
||||
There is much [more detail about configuration](../running/configuration.md) later on.
|
||||
|
||||
## Factory pattern
|
||||
|
||||
Many of the examples in these docs will show the instantiation of the :class:`sanic.app.Sanic` instance in a file called `server.py` in the "global scope" (i.e. not inside a function). This is a common pattern for very simple "hello world" style applications, but it is often beneficial to use a factory pattern instead.
|
||||
|
||||
A "factory" is just a function that returns an instance of the object you want to use. This allows you to abstract the instantiation of the object, but also may make it easier to isolate the application instance.
|
||||
|
||||
.. column::
|
||||
|
||||
A super simple factory pattern could look like this:
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
# ./path/to/server.py
|
||||
from sanic import Sanic
|
||||
from .path.to.config import MyConfig
|
||||
from .path.to.some.blueprint import bp
|
||||
|
||||
|
||||
def create_app(config=MyConfig) -> Sanic:
|
||||
app = Sanic("MyApp", config=config)
|
||||
app.blueprint(bp)
|
||||
return app
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
When we get to running Sanic later, you will learn that the Sanic CLI can detect this pattern and use it to run your application.
|
||||
|
||||
.. column::
|
||||
|
||||
```sh
|
||||
sanic path.to.server:create_app
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
The Sanic application instance can be customized for your application needs in a variety of ways at instantiation.
|
||||
The Sanic application instance can be customized for your application needs in a variety of ways at instantiation.
|
||||
|
||||
For complete details, see the [API docs](/api/sanic.app).
|
||||
|
||||
### Custom configuration
|
||||
|
||||
@@ -137,7 +179,7 @@ The Sanic application instance can be customized for your application needs in a
|
||||
|
||||
This simplest form of custom configuration would be to pass your own object directly into that Sanic application instance
|
||||
|
||||
If you create a custom configuration object, it is *highly* recommended that you subclass the Sanic `Config` option to inherit its behavior. You could use this option for adding properties, or your own set of custom logic.
|
||||
If you create a custom configuration object, it is *highly* recommended that you subclass the :class:`sanic.config.Config` option to inherit its behavior. You could use this option for adding properties, or your own set of custom logic.
|
||||
|
||||
*Added in v21.6*
|
||||
|
||||
@@ -293,7 +335,7 @@ The Sanic application instance can be customized for your application needs in a
|
||||
```python
|
||||
from orjson import dumps
|
||||
|
||||
app = Sanic(__name__, dumps=dumps)
|
||||
app = Sanic("MyApp", dumps=dumps)
|
||||
```
|
||||
|
||||
### Custom loads function
|
||||
@@ -309,7 +351,7 @@ The Sanic application instance can be customized for your application needs in a
|
||||
```python
|
||||
from orjson import loads
|
||||
|
||||
app = Sanic(__name__, loads=loads)
|
||||
app = Sanic("MyApp", loads=loads)
|
||||
```
|
||||
|
||||
|
||||
@@ -318,7 +360,7 @@ The Sanic application instance can be customized for your application needs in a
|
||||
|
||||
### Custom typed application
|
||||
|
||||
The correct, default type of a Sanic application instance is:
|
||||
Beginnint in v23.6, the correct type annotation of a default Sanic application instance is:
|
||||
|
||||
```python
|
||||
sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]
|
||||
@@ -326,14 +368,14 @@ sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]
|
||||
|
||||
It refers to two generic types:
|
||||
|
||||
1. The first is the type of the configuration object. It defaults to `sanic.config.Config`, but can be any subclass of that.
|
||||
2. The second is the type of the application context. It defaults to `types.SimpleNamespace`, but can be **any object** as show above.
|
||||
1. The first is the type of the configuration object. It defaults to :class:`sanic.config.Config`, but can be any subclass of that.
|
||||
2. The second is the type of the application context. It defaults to [`SimpleNamespace()`](https://docs.python.org/3/library/types.html#types.SimpleNamespace), but can be **any object** as show above.
|
||||
|
||||
Let's look at some examples of how the type will change.
|
||||
|
||||
.. column::
|
||||
|
||||
Consider this example where we pass a custom subclass of `Config` and a custom context object.
|
||||
Consider this example where we pass a custom subclass of :class:`sanic.config.Config` and a custom context object.
|
||||
|
||||
.. column::
|
||||
|
||||
@@ -431,7 +473,9 @@ add_listeners(app)
|
||||
|
||||
*Added in v23.6*
|
||||
|
||||
### Custom typed request
|
||||
.. new:: NEW in v23.6
|
||||
|
||||
### Custom typed request
|
||||
|
||||
Sanic also allows you to customize the type of the request object. This is useful if you want to add custom properties to the request object, or be able to access your custom properties of a typed application instance.
|
||||
|
||||
@@ -511,7 +555,7 @@ Let's look at some examples of how the type will change.
|
||||
pass
|
||||
```
|
||||
|
||||
See more information in the [custom request context](./request.md#custom-request-context) section.
|
||||
See more information in the [custom request context](./request#custom-request-context) section.
|
||||
|
||||
*Added in v23.6*
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
The next important building block are your _handlers_. These are also sometimes called "views".
|
||||
|
||||
In Sanic, a handler is any callable that takes at least a `Request` instance as an argument, and returns either an `HTTPResponse` instance, or a coroutine that does the same.
|
||||
|
||||
|
||||
In Sanic, a handler is any callable that takes at least a :class:`sanic.request.Request` instance as an argument, and returns either an :class:`sanic.response.HTTPResponse` instance, or a coroutine that does the same.
|
||||
|
||||
.. column::
|
||||
|
||||
@@ -24,26 +22,39 @@ In Sanic, a handler is any callable that takes at least a `Request` instance as
|
||||
return HTTPResponse()
|
||||
```
|
||||
|
||||
Two more important items to note:
|
||||
|
||||
1. You almost *never* will want to use :class:`sanic.response.HTTPresponse` directly. It is much simpler to use one of the [convenience methods](./response#methods).
|
||||
|
||||
- `from sanic import json`
|
||||
- `from sanic import html`
|
||||
- `from sanic import redirect`
|
||||
- *etc*
|
||||
|
||||
1. As we will see in [the streaming section](../advanced/streaming#response-streaming), you do not always need to return an object. If you use this lower-level API, you can control the flow of the response from within the handler, and a return object is not used.
|
||||
|
||||
.. tip:: Heads up
|
||||
|
||||
If you want to learn more about encapsulating your logic, checkout [class based views](/guide/advanced/class-based-views.md).
|
||||
If you want to learn more about encapsulating your logic, checkout [class based views](../advanced/class-based-views.md). For now, we will continue forward with just function-based views.
|
||||
|
||||
|
||||
### A simple function-based handler
|
||||
|
||||
The most common way to create a route handler is to decorate the function. It creates a visually simple identification of a route definition. We'll learn more about [routing soon](./routing.md).
|
||||
|
||||
.. column::
|
||||
|
||||
Then, all you need to do is wire it up to an endpoint. We'll learn more about [routing soon](./routing.md).
|
||||
|
||||
Let's look at a practical example.
|
||||
|
||||
- We use a convenience decorator on our app instance: `@app.get()`
|
||||
- And a handy convenience method for generating out response object: `text()`
|
||||
|
||||
Mission accomplished :muscle:
|
||||
Mission accomplished 💪
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
from sanic import text
|
||||
|
||||
@app.get("/foo")
|
||||
async def foo_handler(request):
|
||||
@@ -85,16 +96,10 @@ In Sanic, a handler is any callable that takes at least a `Request` instance as
|
||||
- Or, about **3,843.17** requests/second
|
||||
|
||||
.. attrs::
|
||||
:class: is-size-3
|
||||
:class: is-size-2
|
||||
|
||||
🤯
|
||||
|
||||
Okay... this is a ridiculously overdramatic result. And any benchmark you see is inherently very biased. This example is meant to over-the-top show the benefit of `async/await` in the web world. Results will certainly vary. Tools like Sanic and other async Python libraries are not magic bullets that make things faster. They make them _more efficient_.
|
||||
|
||||
In our example, the asynchronous version is so much better because while one request is sleeping, it is able to start another one, and another one, and another one, and another one...
|
||||
|
||||
But, this is the point! Sanic is fast because it takes the available resources and squeezes performance out of them. It can handle many requests concurrently, which means more requests per second.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@@ -105,14 +110,20 @@ In Sanic, a handler is any callable that takes at least a `Request` instance as
|
||||
```
|
||||
|
||||
|
||||
Okay... this is a ridiculously overdramatic result. And any benchmark you see is inherently very biased. This example is meant to over-the-top show the benefit of `async/await` in the web world. Results will certainly vary. Tools like Sanic and other async Python libraries are not magic bullets that make things faster. They make them _more efficient_.
|
||||
|
||||
.. warning:: A common mistake!
|
||||
In our example, the asynchronous version is so much better because while one request is sleeping, it is able to start another one, and another one, and another one, and another one...
|
||||
|
||||
But, this is the point! Sanic is fast because it takes the available resources and squeezes performance out of them. It can handle many requests concurrently, which means more requests per second.
|
||||
|
||||
|
||||
.. tip:: A common mistake!
|
||||
|
||||
Don't do this! You need to ping a website. What do you use? `pip install your-fav-request-library` 🙈
|
||||
|
||||
Instead, try using a client that is `async/await` capable. Your server will thank you. Avoid using blocking tools, and favor those that play well in the asynchronous ecosystem. If you need recommendations, check out [Awesome Sanic](https://github.com/mekicha/awesome-sanic).
|
||||
|
||||
Sanic uses [httpx](https://www.python-httpx.org/) inside of its testing package (sanic-testing) :wink:.
|
||||
Sanic uses [httpx](https://www.python-httpx.org/) inside of its testing package (sanic-testing) 😉.
|
||||
|
||||
|
||||
---
|
||||
@@ -129,3 +140,52 @@ from sanic.request import Request
|
||||
async def typed_handler(request: Request) -> HTTPResponse:
|
||||
return text("Done.")
|
||||
```
|
||||
|
||||
## Naming your handlers
|
||||
|
||||
All handlers are named automatically. This is useful for debugging, and for generating URLs in templates. When not specified, the name that will be used is the name of the function.
|
||||
|
||||
.. column::
|
||||
|
||||
For example, this handler will be named `foo_handler`.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
# Handler name will be "foo_handler"
|
||||
@app.get("/foo")
|
||||
async def foo_handler(request):
|
||||
return text("I said foo!")
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
However, you can override this by passing the `name` argument to the decorator.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
# Handler name will be "foo"
|
||||
@app.get("/foo", name="foo")
|
||||
async def foo_handler(request):
|
||||
return text("I said foo!")
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
In fact, as you will, there may be times when you **MUST** supply a name. For example, if you use two decorators on the same function, you will need to supply a name for at least one of them.
|
||||
|
||||
If you do not, you will get an error and your app will not start. Names **must** be unique within your app.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
# Two handlers, same function,
|
||||
# different names:
|
||||
# - "foo"
|
||||
# - "foo_handler"
|
||||
@app.get("/foo", name="foo")
|
||||
@app.get("/bar")
|
||||
async def foo_handler(request):
|
||||
return text("I said foo!")
|
||||
```
|
||||
|
||||
@@ -1,6 +1,49 @@
|
||||
# Request
|
||||
|
||||
The `Request` instance contains **a lot** of helpful information available on its parameters. Refer to the [API documentation](https://sanic.readthedocs.io/) for full details.
|
||||
See API docs: [sanic.request](/api/sanic.request)
|
||||
|
||||
The :class:`sanic.request.Request` instance contains **a lot** of helpful information available on its parameters. Refer to the [API documentation](https://sanic.readthedocs.io/) for full details.
|
||||
|
||||
As we saw in the section on [handlers](./handlers), the first argument in a route handler is usually the :class:`sanic.request.Request` object. Because Sanic is an async framework, the handler will run inside of a [`asyncio.Task`](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task) and will be scheduled by the event loop. This means that the handler will be executed in an isolated context and the request object will be unique to that handler's task.
|
||||
|
||||
.. column::
|
||||
|
||||
By convention, the argument is named `request`, but you can name it whatever you want. The name of the argument is not important. Both of the following handlers are valid.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@app.get("/foo")
|
||||
async def typical_use_case(request):
|
||||
return text("I said foo!")
|
||||
```
|
||||
|
||||
```python
|
||||
@app.get("/foo")
|
||||
async def atypical_use_case(req):
|
||||
return text("I said foo!")
|
||||
```
|
||||
|
||||
.. column::
|
||||
|
||||
Annotating a request object is super simple.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
from sanic.request import Request
|
||||
from sanic.response import text
|
||||
|
||||
@app.get("/typed")
|
||||
async def typed_handler(request: Request):
|
||||
return text("Done.")
|
||||
```
|
||||
|
||||
.. tip::
|
||||
|
||||
For your convenience, assuming you are using a modern IDE, you should leverage type annotations to help with code completion and documentation. This is especially helpful when using the `request` object as it has **MANY** properties and methods.
|
||||
|
||||
To see the full list of available properties and methods, refer to the [API documentation](/api/sanic.request).
|
||||
|
||||
## Body
|
||||
|
||||
@@ -112,7 +155,15 @@ The `Request` object allows you to access the content of the request body in a f
|
||||
|
||||
### Request context
|
||||
|
||||
The `request.ctx` object is your playground to store whatever information you need to about the request.
|
||||
The `request.ctx` object is your playground to store whatever information you need to about the request. This lives only for the duration of the request and is unique to the request.
|
||||
|
||||
This can be constrasted with the `app.ctx` object which is shared across all requests. Be careful not to confuse them!
|
||||
|
||||
The `request.ctx` object by default is a `SimpleNamespace` object allowing you to set arbitrary attributes on it. Sanic will not use this object for anything, so you are free to use it however you want without worrying about name clashes.
|
||||
|
||||
```python
|
||||
|
||||
### Typical use case
|
||||
|
||||
This is often used to store items like authenticated user details. We will get more into [middleware](./middleware.md) later, but here is a simple example.
|
||||
|
||||
@@ -123,12 +174,12 @@ async def run_before_handler(request):
|
||||
|
||||
@app.route('/hi')
|
||||
async def hi_my_name_is(request):
|
||||
return text("Hi, my name is {}".format(request.ctx.user.name))
|
||||
if not request.ctx.user:
|
||||
return text("Hmm... I don't know you)
|
||||
return text(f"Hi, my name is {request.ctx.user.name}")
|
||||
```
|
||||
|
||||
A typical use case would be to store the user object acquired from database in an authentication middleware. Keys added are accessible to all later middleware as well as the handler over the duration of the request.
|
||||
|
||||
Custom context is reserved for applications and extensions. Sanic itself makes no use of it.
|
||||
As you can see, the `request.ctx` object is a great place to store information that you need to access in multiple handlers making your code more DRY and easier to maintain. But, as we will learn in the [middleware section](./middleware.md), you can also use it to store information from one middleware that will be used in another.
|
||||
|
||||
### Connection context
|
||||
|
||||
@@ -161,9 +212,15 @@ Custom context is reserved for applications and extensions. Sanic itself makes n
|
||||
request.conn_info.ctx.foo=3
|
||||
```
|
||||
|
||||
.. warning::
|
||||
|
||||
While this looks like a convenient place to store information between requests by a single HTTP connection, do not assume that all requests on a single connection came from a single end user. This is because HTTP proxies and load balancers can multiplex multiple connections into a single connection to your server.
|
||||
|
||||
**DO NOT** use this to store information about a single user. Use the `request.ctx` object for that.
|
||||
|
||||
### Custom Request Objects
|
||||
|
||||
As dicussed in [application customization](./app.md#custom-requests), you can create a subclass of `sanic.Request` to add additional functionality to the request object. This is useful for adding additional attributes or methods that are specific to your application.
|
||||
As dicussed in [application customization](./app.md#custom-requests), you can create a subclass of :class:`sanic.request.Request` to add additional functionality to the request object. This is useful for adding additional attributes or methods that are specific to your application.
|
||||
|
||||
.. column::
|
||||
|
||||
@@ -201,13 +258,13 @@ As dicussed in [application customization](./app.md#custom-requests), you can cr
|
||||
|
||||
### Custom Request Context
|
||||
|
||||
By default, the request context (`request.ctx`) is a `SimpleNamespace` object allowing you to set arbitrary attributes on it. While this is super helpful to reuse logic across your application, it can be difficult in the development experience since the IDE will not know what attributes are available.
|
||||
By default, the request context (`request.ctx`) is a [`Simplenamespace`](https://docs.python.org/3/library/types.html#types.SimpleNamespace) object allowing you to set arbitrary attributes on it. While this is super helpful to reuse logic across your application, it can be difficult in the development experience since the IDE will not know what attributes are available.
|
||||
|
||||
To help with this, you can create a custom request context object that will be used instead of the default `SimpleNamespace`. This allows you to add type hints to the context object and have them be available in your IDE.
|
||||
|
||||
.. column::
|
||||
|
||||
Start by subclassing the `sanic.Request` class to create a custom request type. Then, you will need to add a `make_context()` method that returns an instance of your custom context object. *NOTE: the `make_context` method should be a static method.*
|
||||
Start by subclassing the :class:`sanic.request.Request` class to create a custom request type. Then, you will need to add a `make_context()` method that returns an instance of your custom context object. *NOTE: the `make_context` method should be a static method.*
|
||||
|
||||
.. column::
|
||||
|
||||
@@ -229,6 +286,10 @@ To help with this, you can create a custom request context object that will be u
|
||||
user_id: str = None
|
||||
```
|
||||
|
||||
.. note::
|
||||
|
||||
This is a Sanic poweruser feature that makes it super convenient in large codebases to have typed request context objects. It is of course not required, but can be very helpful.
|
||||
|
||||
*Added in v23.6*
|
||||
|
||||
|
||||
@@ -236,7 +297,7 @@ To help with this, you can create a custom request context object that will be u
|
||||
|
||||
.. column::
|
||||
|
||||
Values that are extracted from the path are injected into the handler as parameters, or more specifically as keyword arguments. There is much more detail about this in the [Routing section](./routing.md).
|
||||
Values that are extracted from the path parameters are injected into the handler as argumets, or more specifically as keyword arguments. There is much more detail about this in the [Routing section](./routing.md).
|
||||
|
||||
.. column::
|
||||
|
||||
@@ -244,6 +305,11 @@ To help with this, you can create a custom request context object that will be u
|
||||
@app.route('/tag/<tag>')
|
||||
async def tag_handler(request, tag):
|
||||
return text("Tag - {}".format(tag))
|
||||
|
||||
# or, explicitly as keyword arguments
|
||||
@app.route('/tag/<tag>')
|
||||
async def tag_handler(request, *, tag):
|
||||
return text("Tag - {}".format(tag))
|
||||
```
|
||||
|
||||
|
||||
@@ -254,8 +320,43 @@ There are two attributes on the `request` instance to get query parameters:
|
||||
- `request.args`
|
||||
- `request.query_args`
|
||||
|
||||
```bash
|
||||
$ curl http://localhost:8000\?key1\=val1\&key2\=val2\&key1\=val3
|
||||
These allow you to access the query parameters from the request path (the part after the `?` in the URL).
|
||||
|
||||
### Typical use case
|
||||
|
||||
In most use cases, you will want to use the `request.args` object to access the query parameters. This will be the parsed query string as a dictionary.
|
||||
|
||||
This is by far the most common pattern.
|
||||
|
||||
.. column::
|
||||
|
||||
Consider the example where we have a `/search` endpoint with a `q` parameter that we want to use to search for something.
|
||||
|
||||
.. column::
|
||||
|
||||
```python
|
||||
@app.get("/search")
|
||||
async def search(request):
|
||||
query = request.args.get("q")
|
||||
if not query:
|
||||
return text("No query string provided")
|
||||
return text(f"Searching for: {query}")
|
||||
```
|
||||
|
||||
### Parsing the query string
|
||||
|
||||
Sometimes, however, you may want to access the query string as a raw string or as a list of tuples. For this, you can use the `request.query_string` and `request.query_args` attributes.
|
||||
|
||||
It also should be noted that HTTP allows multiple values for a single key. Although `request.args` may seem like a regular dictionary, it is actually a special type that allows for multiple values for a single key. You can access this by using the `request.args.getlist()` method.
|
||||
|
||||
- `request.query_string` - The raw query string
|
||||
- `request.query_args` - The parsed query string as a list of tuples
|
||||
- `request.args` - The parsed query string as a *special* dictionary
|
||||
- `request.args.get()` - Get the first value for a key (behaves like a regular dictionary)
|
||||
- `request.args.getlist()` - Get all values for a key
|
||||
|
||||
```sh
|
||||
curl "http://localhost:8000?key1=val1&key2=val2&key1=val3"
|
||||
```
|
||||
|
||||
```python
|
||||
@@ -283,11 +384,12 @@ key1=val1&key2=val2&key1=val3
|
||||
|
||||
Most of the time you will want to use the `.get()` method to access the first element and not a list. If you do want a list of all items, you can use `.getlist()`.
|
||||
|
||||
|
||||
## Current request getter
|
||||
|
||||
Sometimes you may find that you need access to the current request in your application in a location where it is not accessible. A typical example might be in a `logging` format. You can use `Request.get_current()` to fetch the current request (if any).
|
||||
|
||||
Remember, the request object is confined to the single [`asyncio.Task`](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task) that is running the handler. If you are not in that task, there is no request object.
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
@@ -317,8 +419,8 @@ def record_factory(*args, **kwargs):
|
||||
|
||||
logging.setLogRecordFactory(record_factory)
|
||||
|
||||
LOGGING_CONFIG_DEFAULTS["formatters"]["access"]["format"] = LOGGING_FORMAT
|
||||
|
||||
LOGGING_CONFIG_DEFAULTS["formatters"]["access"]["format"] = LOGGING_FORMAT
|
||||
app = Sanic("Example", log_config=LOGGING_CONFIG_DEFAULTS)
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
from textwrap import indent
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ let burger;
|
||||
let menu;
|
||||
let menuLinks;
|
||||
let menuGroups;
|
||||
let anchors;
|
||||
let lastUpdated = 0;
|
||||
let updateFrequency = 300;
|
||||
function trigger(el, eventType) {
|
||||
if (typeof eventType === "string" && typeof el[eventType] === "function") {
|
||||
el[eventType]();
|
||||
@@ -44,6 +47,30 @@ function hasActiveLink(element) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function scrollHandler(e) {
|
||||
let now = Date.now();
|
||||
if (now - lastUpdated < updateFrequency) return;
|
||||
|
||||
let closestAnchor = null;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
if (!anchors) { return; }
|
||||
|
||||
anchors.forEach(anchor => {
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const distance = Math.abs(rect.top);
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestAnchor = anchor;
|
||||
}
|
||||
});
|
||||
|
||||
if (closestAnchor) {
|
||||
history.replaceState(null, null, "#" + closestAnchor.id);
|
||||
lastUpdated = now;
|
||||
}
|
||||
}
|
||||
function initBurger() {
|
||||
if (!burger || !menu) {
|
||||
return;
|
||||
@@ -110,6 +137,10 @@ function initSearch() {
|
||||
);
|
||||
});
|
||||
}
|
||||
function refreshAnchors() {
|
||||
anchors = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]");
|
||||
};
|
||||
|
||||
function setMenuLinkActive(href) {
|
||||
burger.classList.remove("is-active");
|
||||
menu.classList.remove("is-active");
|
||||
@@ -149,6 +180,7 @@ function init() {
|
||||
refreshMenu();
|
||||
refreshMenuLinks();
|
||||
refreshMenuGroups();
|
||||
refreshAnchors();
|
||||
initBurger();
|
||||
initMenuGroups();
|
||||
initDetails();
|
||||
@@ -162,3 +194,4 @@ function afterSwap(e) {
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
document.body.addEventListener("htmx:afterSwap", afterSwap);
|
||||
document.addEventListener("scroll", scrollHandler);
|
||||
|
||||
@@ -272,7 +272,7 @@ th {
|
||||
|
||||
html {
|
||||
background-color: white;
|
||||
font-size: 18px;
|
||||
font-size: 24px;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-width: 300px;
|
||||
@@ -12652,37 +12652,39 @@ a.has-text-danger-dark:hover, a.has-text-danger-dark:focus {
|
||||
.menu hr {
|
||||
background-color: var(--menu-divider); }
|
||||
.menu .is-anchor {
|
||||
font-size: 0.85em; }
|
||||
font-size: 0.75em; }
|
||||
.menu .is-anchor a::before {
|
||||
content: '# ';
|
||||
color: var(--menu-contrast); }
|
||||
.menu .menu-label {
|
||||
margin-bottom: 1rem; }
|
||||
.menu li.is-group > a::after {
|
||||
content: '';
|
||||
position: relative;
|
||||
top: -2px;
|
||||
left: 8px;
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid var(--menu-contrast);
|
||||
border-top: 5.33333px solid transparent;
|
||||
border-bottom: 5.33333px solid transparent;
|
||||
transform: rotate(90deg); }
|
||||
.menu li.is-group > a ~ .menu-list {
|
||||
transition: all .15s ease-in-out;
|
||||
transform: scaleY(1);
|
||||
transform-origin: top;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
margin: 0; }
|
||||
.menu li.is-group > a:not(.is-open)::after {
|
||||
transform: rotate(0deg); }
|
||||
.menu li.is-group > a:not(.is-open) ~ .menu-list {
|
||||
transform: scaleY(0);
|
||||
opacity: 0;
|
||||
font-size: 0; }
|
||||
.menu li.is-group > a {
|
||||
font-size: 0.85rem; }
|
||||
.menu li.is-group > a::after {
|
||||
content: '';
|
||||
position: relative;
|
||||
top: -2px;
|
||||
left: 8px;
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid var(--menu-contrast);
|
||||
border-top: 5.33333px solid transparent;
|
||||
border-bottom: 5.33333px solid transparent;
|
||||
transform: rotate(90deg); }
|
||||
.menu li.is-group > a ~ .menu-list {
|
||||
transition: all .15s ease-in-out;
|
||||
transform: scaleY(1);
|
||||
transform-origin: top;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
margin: 0; }
|
||||
.menu li.is-group > a:not(.is-open)::after {
|
||||
transform: rotate(0deg); }
|
||||
.menu li.is-group > a:not(.is-open) ~ .menu-list {
|
||||
transform: scaleY(0);
|
||||
opacity: 0;
|
||||
font-size: 0; }
|
||||
.menu ~ main {
|
||||
margin-left: 360px; }
|
||||
.menu .anchor-list {
|
||||
@@ -12702,6 +12704,16 @@ a.has-text-danger-dark:hover, a.has-text-danger-dark:focus {
|
||||
.burger {
|
||||
display: block; } }
|
||||
|
||||
.menu-list li ul {
|
||||
margin: 0;
|
||||
padding-left: 0; }
|
||||
|
||||
.menu-list ul li:not(.is-anchor) {
|
||||
margin-left: 0.75em; }
|
||||
|
||||
.menu-list .menu-list li a {
|
||||
font-size: 0.85em; }
|
||||
|
||||
code {
|
||||
color: #4a4a4a; }
|
||||
|
||||
@@ -12718,6 +12730,12 @@ a[target=_blank]::after {
|
||||
content: "⇗";
|
||||
margin-left: 0.25em; }
|
||||
|
||||
a > code {
|
||||
border-bottom: 1px solid #ff0d68; }
|
||||
a > code:hover {
|
||||
background-color: #ff0d68;
|
||||
color: white; }
|
||||
|
||||
h1 a.anchor,
|
||||
h2 a.anchor,
|
||||
h3 a.anchor,
|
||||
@@ -12768,6 +12786,10 @@ p + p {
|
||||
article {
|
||||
margin-left: 0; } }
|
||||
|
||||
@media screen and (min-width: 1216px) {
|
||||
.section {
|
||||
padding: 3rem 0rem; } }
|
||||
|
||||
.footer {
|
||||
margin-bottom: 4rem; }
|
||||
.footer a[target=_blank]::after {
|
||||
@@ -12998,6 +13020,51 @@ h3 + .code-block {
|
||||
.tabs .tab-content {
|
||||
display: none; }
|
||||
|
||||
.table-of-contents {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
max-width: 500px;
|
||||
padding: 1rem 2rem;
|
||||
background-color: #fafafa;
|
||||
box-shadow: 0 0 2px rgba(63, 63, 68, 0.5); }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.table-of-contents {
|
||||
background-color: #0a0a0a;
|
||||
box-shadow: 0 0 2px rgba(191, 191, 191, 0.5); } }
|
||||
.table-of-contents .table-of-contents-item {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
text-decoration: none; }
|
||||
.table-of-contents .table-of-contents-item:hover {
|
||||
text-decoration: underline;
|
||||
color: #ff0d68; }
|
||||
.table-of-contents .table-of-contents-item:hover strong, .table-of-contents .table-of-contents-item:hover small {
|
||||
color: #ff0d68; }
|
||||
.table-of-contents .table-of-contents-item strong {
|
||||
color: #121212;
|
||||
font-size: 1.15em;
|
||||
display: block;
|
||||
line-height: 1rem;
|
||||
margin-top: 0.75rem; }
|
||||
.table-of-contents .table-of-contents-item small {
|
||||
color: #7a7a7a;
|
||||
font-size: 0.85em; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.table-of-contents .table-of-contents-item strong {
|
||||
color: #dbdbdb; } }
|
||||
@media (max-width: 768px) {
|
||||
.table-of-contents {
|
||||
position: static;
|
||||
max-width: calc(100vw - 2rem); }
|
||||
.table-of-contents .table-of-contents-item {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: start; }
|
||||
.table-of-contents .table-of-contents-item strong {
|
||||
display: inline;
|
||||
margin: 0 0 0 0.75rem; } }
|
||||
footer .level,
|
||||
footer .level .level-right,
|
||||
footer .level .level-left {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[tool.ruff]
|
||||
extend = "../pyproject.toml"
|
||||
|
||||
[tool.ruff.isort]
|
||||
known-first-party = ["webapp"]
|
||||
lines-after-imports = 1
|
||||
lines-between-types = 1
|
||||
@@ -1,3 +1,8 @@
|
||||
sanic>=23.6.*
|
||||
sanic-ext>=23.6.*
|
||||
msgspec
|
||||
python-frontmatter
|
||||
pygments
|
||||
docstring-parser
|
||||
libsass
|
||||
mistune
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
"""Sanic User Guide
|
||||
|
||||
https://sanic.dev
|
||||
|
||||
Built using the SHH stack:
|
||||
- Sanic
|
||||
- html5tagger
|
||||
- HTMX"""
|
||||
from pathlib import Path
|
||||
|
||||
from webapp.worker.factory import create_app
|
||||
|
||||
@@ -290,3 +290,68 @@ h3 + .code-block { margin-top: 1rem; }
|
||||
.tabs {
|
||||
.tab-content { display: none; }
|
||||
}
|
||||
|
||||
.table-of-contents {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
max-width: 500px;
|
||||
padding: 1rem 2rem;
|
||||
background-color: $white-bis;
|
||||
box-shadow: 0 0 2px rgba(63, 63, 68, 0.5);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: $black;
|
||||
box-shadow: 0 0 2px rgba(191, 191, 191, 0.5);
|
||||
|
||||
}
|
||||
|
||||
.table-of-contents-item {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: $primary;
|
||||
|
||||
strong, small {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $black-bis;
|
||||
font-size: 1.15em;
|
||||
display: block;
|
||||
line-height: 1rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
small {
|
||||
color: $grey;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
strong { color: $grey-lighter; }
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
position: static;
|
||||
max-width: calc(100vw - 2rem);
|
||||
|
||||
.table-of-contents-item {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: start;
|
||||
|
||||
strong {
|
||||
display: inline;
|
||||
margin: 0 0 0 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,18 @@ code { color: #{$grey-dark}; }
|
||||
}
|
||||
}
|
||||
|
||||
a[target=_blank]::after {
|
||||
content: "⇗";
|
||||
margin-left: 0.25em;
|
||||
a{
|
||||
&[target=_blank]::after {
|
||||
content: "⇗";
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
& > code {
|
||||
border-bottom: 1px solid $primary;
|
||||
&:hover {
|
||||
background-color: $primary;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
h1 a.anchor,
|
||||
h2 a.anchor,
|
||||
@@ -49,4 +58,8 @@ p + p { margin-top: 1rem; }
|
||||
h2 { margin-left: 0; }
|
||||
article { margin-left: 0; }
|
||||
}
|
||||
|
||||
@media screen and (min-width: $widescreen) {
|
||||
.section {
|
||||
padding: 3rem 0rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$body-size: 18px;
|
||||
$body-size: 24px;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ $menu-width: 360px;
|
||||
hr { background-color: var(--menu-divider); }
|
||||
|
||||
.is-anchor {
|
||||
font-size: 0.85em;
|
||||
font-size: 0.75em;
|
||||
& a::before {
|
||||
content: '# ';
|
||||
color: var(--menu-contrast);
|
||||
@@ -68,6 +68,7 @@ $menu-width: 360px;
|
||||
}
|
||||
.menu-label { margin-bottom: 1rem; }
|
||||
li.is-group > a {
|
||||
font-size: 0.85rem;
|
||||
&::after {
|
||||
content: '';
|
||||
position: relative;
|
||||
@@ -119,3 +120,14 @@ $menu-width: 360px;
|
||||
}
|
||||
.burger { display: block; }
|
||||
}
|
||||
|
||||
.menu-list li ul {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
.menu-list ul li:not(.is-anchor) {
|
||||
margin-left: 0.75em;
|
||||
}
|
||||
.menu-list .menu-list li a {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from html5tagger import Builder, Document # type: ignore
|
||||
|
||||
|
||||
class BaseRenderer:
|
||||
def __init__(self, base_title: str):
|
||||
self.base_title = base_title
|
||||
|
||||
@@ -7,6 +7,7 @@ from pygments.token import ( # Error,; Generic,; Number,; Operator,
|
||||
Token,
|
||||
)
|
||||
|
||||
|
||||
class SanicCodeStyle(Style):
|
||||
styles = {
|
||||
Token: "#777",
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Generator
|
||||
from html5tagger import Builder
|
||||
from sanic import Request
|
||||
|
||||
|
||||
class BaseLayout:
|
||||
def __init__(self, builder: Builder):
|
||||
self.builder = builder
|
||||
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
from html5tagger import Builder, E # type: ignore
|
||||
from sanic import Request
|
||||
|
||||
|
||||
def do_footer(builder: Builder, request: Request) -> None:
|
||||
builder.footer(
|
||||
_pagination(request),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from webapp.display.layouts.models import MenuItem
|
||||
|
||||
from html5tagger import Builder, E # type: ignore
|
||||
from sanic import Request
|
||||
|
||||
from webapp.display.layouts.models import MenuItem
|
||||
|
||||
def do_navbar(builder: Builder, request: Request) -> None:
|
||||
navbar_items = [
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from webapp.display.layouts.models import MenuItem
|
||||
from webapp.display.text import slugify
|
||||
|
||||
from html5tagger import Builder, E # type: ignore
|
||||
from sanic import Request
|
||||
|
||||
from webapp.display.layouts.models import MenuItem
|
||||
from webapp.display.text import slugify
|
||||
|
||||
def do_sidebar(builder: Builder, request: Request) -> None:
|
||||
builder.a(class_="burger")(E.span().span().span().span())
|
||||
|
||||
@@ -8,6 +8,7 @@ from sanic import Request
|
||||
|
||||
from .base import BaseLayout
|
||||
|
||||
|
||||
class HomeLayout(BaseLayout):
|
||||
@contextmanager
|
||||
def layout(
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator
|
||||
|
||||
from sanic import Request
|
||||
|
||||
from webapp.display.layouts.elements.footer import do_footer
|
||||
from webapp.display.layouts.elements.navbar import do_navbar
|
||||
from webapp.display.layouts.elements.sidebar import do_sidebar
|
||||
|
||||
from sanic import Request
|
||||
|
||||
from .base import BaseLayout
|
||||
|
||||
|
||||
class MainLayout(BaseLayout):
|
||||
@contextmanager
|
||||
def layout(
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from msgspec import Struct, field
|
||||
|
||||
|
||||
class MenuItem(Struct, kw_only=False, omit_defaults=True):
|
||||
label: str
|
||||
path: str | None = None
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import re
|
||||
|
||||
from textwrap import dedent
|
||||
|
||||
from html5tagger import HTML, Builder, E # type: ignore
|
||||
from mistune import HTMLRenderer, create_markdown, escape
|
||||
from mistune.directives import RSTDirective, TableOfContents
|
||||
from mistune.util import safe_entity
|
||||
@@ -10,6 +8,8 @@ from pygments import highlight
|
||||
from pygments.formatters import html
|
||||
from pygments.lexers import get_lexer_by_name
|
||||
|
||||
from html5tagger import HTML, Builder, E # type: ignore
|
||||
|
||||
from .code_style import SanicCodeStyle
|
||||
from .plugins.attrs import Attributes
|
||||
from .plugins.columns import Column
|
||||
@@ -17,9 +17,11 @@ from .plugins.hook import Hook
|
||||
from .plugins.mermaid import Mermaid
|
||||
from .plugins.notification import Notification
|
||||
from .plugins.span import span
|
||||
from .plugins.inline_directive import inline_directive
|
||||
from .plugins.tabs import Tabs
|
||||
from .text import slugify
|
||||
|
||||
|
||||
class DocsRenderer(HTMLRenderer):
|
||||
def block_code(self, code: str, info: str | None = None):
|
||||
builder = Builder("Block")
|
||||
@@ -54,10 +56,12 @@ class DocsRenderer(HTMLRenderer):
|
||||
)
|
||||
|
||||
def link(self, text: str, url: str, title: str | None = None) -> str:
|
||||
url = self.safe_url(url).removesuffix(".md")
|
||||
if not url.endswith("/"):
|
||||
url = self.safe_url(url).replace(".md", ".html")
|
||||
url, anchor = url.split("#", 1) if "#" in url else (url, None)
|
||||
if not url.endswith("/") and not url.endswith(".html"):
|
||||
url += ".html"
|
||||
|
||||
if anchor:
|
||||
url += f"#{anchor}"
|
||||
attributes: dict[str, str] = {"href": url}
|
||||
if title:
|
||||
attributes["title"] = safe_entity(title)
|
||||
@@ -89,6 +93,22 @@ class DocsRenderer(HTMLRenderer):
|
||||
attrs["class"] = "table is-fullwidth is-bordered"
|
||||
return self._make_tag("table", attrs, text)
|
||||
|
||||
def inline_directive(self, text: str, **attrs) -> str:
|
||||
num_dots = text.count(".")
|
||||
display = self.codespan(text)
|
||||
|
||||
if num_dots <= 1:
|
||||
return display
|
||||
|
||||
module, *_ = text.rsplit(".", num_dots - 1)
|
||||
href = f"/api/{module}.html"
|
||||
return self._make_tag(
|
||||
"a",
|
||||
{"href": href, "class": "inline-directive"},
|
||||
display,
|
||||
)
|
||||
|
||||
|
||||
def _make_tag(
|
||||
self, tag: str, attributes: dict[str, str], text: str | None = None
|
||||
) -> str:
|
||||
@@ -125,6 +145,7 @@ _render_markdown = create_markdown(
|
||||
"mark",
|
||||
"table",
|
||||
span,
|
||||
inline_directive,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from html import escape
|
||||
@@ -11,10 +10,12 @@ from html import escape
|
||||
from docstring_parser import Docstring, DocstringParam, DocstringRaises
|
||||
from docstring_parser import parse as parse_docstring
|
||||
from docstring_parser.common import DocstringExample
|
||||
|
||||
from html5tagger import HTML, Builder, E # type: ignore
|
||||
|
||||
from ..markdown import render_markdown, slugify
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocObject:
|
||||
name: str
|
||||
@@ -107,13 +108,31 @@ def _get_object_type(obj) -> str:
|
||||
def organize_docobjects(package_name: str) -> dict[str, str]:
|
||||
page_content: defaultdict[str, str] = defaultdict(str)
|
||||
docobjects = _extract_docobjects(package_name)
|
||||
page_registry: defaultdict[str, list[str]] = defaultdict(list)
|
||||
for module, docobject in docobjects.items():
|
||||
print(f"{module=}")
|
||||
builder = Builder(name="Partial")
|
||||
_docobject_to_html(docobject, builder)
|
||||
ref = module.rsplit(".", module.count(".") - 1)[0]
|
||||
page_registry[ref].append(module)
|
||||
page_content[f"/api/{ref}.md"] += str(builder)
|
||||
for ref, objects in page_registry.items():
|
||||
page_content[f"/api/{ref}.md"] = _table_of_contents(objects) + page_content[f"/api/{ref}.md"]
|
||||
return page_content
|
||||
|
||||
def _table_of_contents(objects: list[str]) -> str:
|
||||
builder = Builder(name="Partial")
|
||||
with builder.div(class_="table-of-contents"):
|
||||
builder.h3("Table of Contents", class_="is-size-4")
|
||||
for obj in objects:
|
||||
module, name = obj.rsplit(".", 1)
|
||||
builder.a(
|
||||
E.strong(name), E.small(module),
|
||||
href=f"#{slugify(obj.replace('.', '-'))}",
|
||||
class_="table-of-contents-item",
|
||||
)
|
||||
return str(builder)
|
||||
|
||||
|
||||
def _extract_docobjects(package_name: str) -> dict[str, DocObject]:
|
||||
docstrings = {}
|
||||
@@ -309,7 +328,10 @@ def _render_params(builder: Builder, params: list[DocstringParam]) -> None:
|
||||
E.br(),
|
||||
E.span(
|
||||
param.type_name,
|
||||
class_="has-text-weight-normal has-text-purple ml-2",
|
||||
class_=(
|
||||
"has-text-weight-normal has-text-purple "
|
||||
"is-size-7 ml-2"
|
||||
),
|
||||
),
|
||||
]
|
||||
dt_args.extend(parts)
|
||||
|
||||
@@ -3,14 +3,15 @@ from __future__ import annotations
|
||||
from contextlib import contextmanager
|
||||
from typing import Type
|
||||
|
||||
from webapp.display.base import BaseRenderer
|
||||
|
||||
from html5tagger import HTML, Builder # type: ignore
|
||||
from sanic import Request
|
||||
|
||||
from webapp.display.base import BaseRenderer
|
||||
|
||||
from ..layouts.base import BaseLayout
|
||||
from .page import Page
|
||||
|
||||
|
||||
class PageRenderer(BaseRenderer):
|
||||
def render(self, request: Request, language: str, path: str) -> Builder:
|
||||
builder = self.get_builder(
|
||||
|
||||
@@ -2,11 +2,13 @@ from re import Match
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
from html5tagger import HTML, E
|
||||
from mistune.block_parser import BlockParser
|
||||
from mistune.core import BlockState
|
||||
from mistune.directives import DirectivePlugin
|
||||
|
||||
from html5tagger import HTML, E
|
||||
|
||||
|
||||
class Attributes(DirectivePlugin):
|
||||
def __call__(self, directive, md):
|
||||
directive.register("attrs", self.parse)
|
||||
|
||||
@@ -8,6 +8,7 @@ from mistune.core import BlockState
|
||||
from mistune.directives import DirectivePlugin, RSTDirective
|
||||
from mistune.markdown import Markdown
|
||||
|
||||
|
||||
class Column(DirectivePlugin):
|
||||
def parse(
|
||||
self, block: BlockParser, m: Match, state: BlockState
|
||||
|
||||
@@ -2,6 +2,7 @@ from mistune.core import BlockState
|
||||
from mistune.directives import DirectivePlugin, RSTDirective
|
||||
from mistune.markdown import Markdown
|
||||
|
||||
|
||||
class Hook(DirectivePlugin):
|
||||
def __call__( # type: ignore
|
||||
self, directive: RSTDirective, md: Markdown
|
||||
|
||||
20
guide/webapp/display/plugins/inline_directive.py
Normal file
20
guide/webapp/display/plugins/inline_directive.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import re
|
||||
|
||||
from mistune.markdown import Markdown
|
||||
|
||||
DIRECTIVE_PATTERN = r":(?:class|func|meth|attr|exc|mod|data|const|obj|keyword|option|cmdoption|envvar):`(?P<ref>sanic\.[^`]+)`" # noqa: E501
|
||||
|
||||
def _parse_inline_directive(inline, m: re.Match, state):
|
||||
print("inline_directive.py: _parse_inline_directive", m.group("ref"))
|
||||
state.append_token(
|
||||
{
|
||||
"type": "inline_directive",
|
||||
"attrs": {},
|
||||
"raw": m.group("ref"),
|
||||
}
|
||||
)
|
||||
return m.end()
|
||||
|
||||
def inline_directive(md: Markdown):
|
||||
print("Registering inline_directive")
|
||||
md.inline.register("inline_directive", DIRECTIVE_PATTERN, _parse_inline_directive, before="escape",)
|
||||
@@ -3,13 +3,15 @@ from re import Match
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
from html5tagger import HTML, E
|
||||
from mistune import HTMLRenderer
|
||||
from mistune.block_parser import BlockParser
|
||||
from mistune.core import BlockState
|
||||
from mistune.directives import DirectivePlugin, RSTDirective
|
||||
from mistune.markdown import Markdown
|
||||
|
||||
from html5tagger import HTML, E
|
||||
|
||||
|
||||
class Mermaid(DirectivePlugin):
|
||||
def parse(
|
||||
self, block: BlockParser, m: Match, state: BlockState
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from html5tagger import HTML, E
|
||||
from mistune.directives import Admonition
|
||||
|
||||
from html5tagger import HTML, E
|
||||
|
||||
|
||||
class Notification(Admonition):
|
||||
SUPPORTED_NAMES = {
|
||||
"success",
|
||||
|
||||
@@ -2,6 +2,7 @@ import re
|
||||
|
||||
from mistune.markdown import Markdown
|
||||
|
||||
|
||||
def parse_inline_span(inline, m: re.Match, state):
|
||||
state.append_token(
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ from mistune.core import BlockState
|
||||
from mistune.directives import DirectivePlugin, RSTDirective
|
||||
from mistune.markdown import Markdown
|
||||
|
||||
|
||||
class Tabs(DirectivePlugin):
|
||||
def parse(
|
||||
self, block: BlockParser, m: Match, state: BlockState
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
from contextlib import contextmanager
|
||||
from urllib.parse import unquote
|
||||
|
||||
from webapp.display.search.search import Searcher
|
||||
|
||||
from html5tagger import Builder, E # type: ignore
|
||||
from sanic import Request
|
||||
|
||||
from webapp.display.search.search import Searcher
|
||||
|
||||
from ..base import BaseRenderer
|
||||
from ..layouts.main import MainLayout
|
||||
|
||||
|
||||
class SearchRenderer(BaseRenderer):
|
||||
def render(
|
||||
self, request: Request, language: str, searcher: Searcher, full: bool
|
||||
|
||||
@@ -5,9 +5,9 @@ from pathlib import Path
|
||||
from typing import ClassVar
|
||||
|
||||
from msgspec import Struct
|
||||
|
||||
from webapp.display.page import Page
|
||||
|
||||
|
||||
class Stemmer:
|
||||
STOP_WORDS: ClassVar[set[str]] = set(
|
||||
"a about above after again against all am an and any are aren't as at be because been before being below between both but by can't cannot could couldn't did didn't do does doesn't doing don't down during each few for from further had hadn't has hasn't have haven't having he he'd he'll he's her here here's hers herself him himself his how how's i i'd i'll i'm i've if in into is isn't it it's its itself let's me more most mustn't my myself no nor not of off on once only or other ought our ours ourselves out over own same shan't she she'd she'll she's should shouldn't so some such than that that's the their theirs them themselves then there there's these they they'd they'll they're they've this those through to too under until up very was wasn't we we'd we'll we're we've were weren't what what's when when's where where's which while who who's whom why why's with won't would wouldn't you you'd you'll you're you've your yours yourself yourselves".split() # noqa: E501
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# from urllib.parse import unquote
|
||||
|
||||
from sanic import Blueprint, Request, Sanic, html
|
||||
|
||||
from webapp.display.page import Page
|
||||
from webapp.display.search.renderer import SearchRenderer
|
||||
from webapp.display.search.search import Document, Searcher, Stemmer
|
||||
|
||||
from sanic import Blueprint, Request, Sanic, html
|
||||
|
||||
bp = Blueprint("search", url_prefix="/<language>/search")
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from pathlib import Path
|
||||
|
||||
from msgspec import yaml
|
||||
|
||||
from webapp.display.layouts.models import GeneralConfig, MenuItem
|
||||
|
||||
|
||||
def load_menu(path: Path) -> list[MenuItem]:
|
||||
loaded = yaml.decode(path.read_bytes(), type=dict[str, list[MenuItem]])
|
||||
return loaded["root"]
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
from sanic import Request, Sanic, html, redirect
|
||||
|
||||
from webapp.display.layouts.models import MenuItem
|
||||
from webapp.display.page import Page, PageRenderer
|
||||
from webapp.endpoint.view import bp
|
||||
@@ -9,6 +7,9 @@ from webapp.worker.config import load_config, load_menu
|
||||
from webapp.worker.reload import setup_livereload
|
||||
from webapp.worker.style import setup_style
|
||||
|
||||
from sanic import Request, Sanic, html, redirect
|
||||
|
||||
|
||||
def _compile_sidebar_order(items: list[MenuItem]) -> list[str]:
|
||||
order = []
|
||||
for item in items:
|
||||
|
||||
@@ -8,6 +8,7 @@ import ujson
|
||||
|
||||
from sanic import Request, Sanic, Websocket
|
||||
|
||||
|
||||
def setup_livereload(app: Sanic) -> None:
|
||||
@app.main_process_start
|
||||
async def main_process_start(app: Sanic):
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# from scss.compiler import compile_string
|
||||
|
||||
from pygments.formatters import html
|
||||
from sanic import Sanic
|
||||
from sass import compile as compile_scss
|
||||
|
||||
from webapp.display.code_style import SanicCodeStyle
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
def setup_style(app: Sanic) -> None:
|
||||
index = app.config.STYLE_DIR / "index.scss"
|
||||
style_output = app.config.PUBLIC_DIR / "assets" / "style.css"
|
||||
|
||||
@@ -2,19 +2,6 @@
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.ruff]
|
||||
extend-select = ["I"]
|
||||
ignore = ["D100", "D101", "D102", "D103", "E402", "E741", "F811", "F821"]
|
||||
line-length = 79
|
||||
show-source = true
|
||||
show-fixes = true
|
||||
|
||||
[tool.ruff.isort]
|
||||
known-first-party = ["sanic"]
|
||||
known-third-party = ["pytest"]
|
||||
lines-after-imports = 2
|
||||
lines-between-types = 1
|
||||
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
|
||||
|
||||
75
sanic/app.py
75
sanic/app.py
@@ -119,8 +119,59 @@ class Sanic(
|
||||
StartupMixin,
|
||||
metaclass=TouchUpMeta,
|
||||
):
|
||||
"""
|
||||
The main application instance
|
||||
"""The main application instance
|
||||
|
||||
You will create an instance of this class and use it to register
|
||||
routes, listeners, middleware, blueprints, error handlers, etc.
|
||||
|
||||
By convention, it is often called `app`. It must be named using
|
||||
the `name` parameter and is roughly constrained to the same
|
||||
restrictions as a Python module name, however, it can contain
|
||||
hyphens (`-`).
|
||||
|
||||
```python
|
||||
# will cause an error because it contains spaces
|
||||
Sanic("This is not legal")
|
||||
```
|
||||
|
||||
```python
|
||||
# this is legal
|
||||
Sanic("Hyphens-are-legal_or_also_underscores")
|
||||
```
|
||||
|
||||
Args:
|
||||
name (str): The name of the application. Must be a valid
|
||||
Python module name (including hyphens).
|
||||
config (Optional[config_type]): The configuration to use for
|
||||
the application. Defaults to `None`.
|
||||
ctx (Optional[ctx_type]): The context to use for the
|
||||
application. Defaults to `None`.
|
||||
router (Optional[Router]): The router to use for the
|
||||
application. Defaults to `None`.
|
||||
signal_router (Optional[SignalRouter]): The signal router to
|
||||
use for the application. Defaults to `None`.
|
||||
error_handler (Optional[ErrorHandler]): The error handler to
|
||||
use for the application. Defaults to `None`.
|
||||
env_prefix (Optional[str]): The prefix to use for environment
|
||||
variables. Defaults to `SANIC_`.
|
||||
request_class (Optional[Type[Request]]): The request class to
|
||||
use for the application. Defaults to `Request`.
|
||||
strict_slashes (bool): Whether to enforce strict slashes.
|
||||
Defaults to `False`.
|
||||
log_config (Optional[Dict[str, Any]]): The logging configuration
|
||||
to use for the application. Defaults to `None`.
|
||||
configure_logging (bool): Whether to configure logging.
|
||||
Defaults to `True`.
|
||||
dumps (Optional[Callable[..., AnyStr]]): The function to use
|
||||
for serializing JSON. Defaults to `None`.
|
||||
loads (Optional[Callable[..., Any]]): The function to use
|
||||
for deserializing JSON. Defaults to `None`.
|
||||
inspector (bool): Whether to enable the inspector. Defaults
|
||||
to `False`.
|
||||
inspector_class (Optional[Type[Inspector]]): The inspector
|
||||
class to use for the application. Defaults to `None`.
|
||||
certloader_class (Optional[Type[CertLoader]]): The certloader
|
||||
class to use for the application. Defaults to `None`.
|
||||
"""
|
||||
|
||||
__touchup__ = (
|
||||
@@ -381,7 +432,7 @@ class Sanic(
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def register_listener(
|
||||
self, listener: ListenerType[SanicVar], event: str
|
||||
self, listener: ListenerType[SanicVar], event: str, *, priority: int = 0
|
||||
) -> ListenerType[SanicVar]:
|
||||
"""Register the listener for a given event.
|
||||
|
||||
@@ -402,10 +453,14 @@ class Sanic(
|
||||
raise BadRequest(f"Invalid event: {event}. Use one of: {valid}")
|
||||
|
||||
if "." in _event:
|
||||
self.signal(_event.value)(
|
||||
self.signal(_event.value, priority=priority)(
|
||||
partial(self._listener, listener=listener)
|
||||
)
|
||||
else:
|
||||
if priority:
|
||||
error_logger.warning(
|
||||
f"Priority is not supported for {_event.value}"
|
||||
)
|
||||
self.listeners[_event.value].append(listener)
|
||||
|
||||
return listener
|
||||
@@ -529,7 +584,7 @@ class Sanic(
|
||||
return handler.handler
|
||||
|
||||
def _apply_listener(self, listener: FutureListener):
|
||||
return self.register_listener(listener.listener, listener.event)
|
||||
return self.register_listener(listener.listener, listener.event, priority=listener.priority)
|
||||
|
||||
def _apply_route(
|
||||
self, route: FutureRoute, overwrite: bool = False
|
||||
@@ -581,7 +636,13 @@ class Sanic(
|
||||
|
||||
def _apply_signal(self, signal: FutureSignal) -> Signal:
|
||||
with self.amend():
|
||||
return self.signal_router.add(*signal)
|
||||
return self.signal_router.add(
|
||||
handler=signal.handler,
|
||||
event=signal.event,
|
||||
condition=signal.condition,
|
||||
exclusive=signal.exclusive,
|
||||
priority=signal.priority,
|
||||
)
|
||||
|
||||
@overload
|
||||
def dispatch(
|
||||
@@ -1348,7 +1409,7 @@ class Sanic(
|
||||
if not hasattr(handler, "is_websocket"):
|
||||
raise ServerError(
|
||||
f"Invalid response type {response!r} "
|
||||
"(need HTTPResponse)"
|
||||
"(need HTTPResponse)"
|
||||
)
|
||||
|
||||
except CancelledError: # type: ignore
|
||||
|
||||
@@ -349,7 +349,8 @@ def parse_content_header(value: str) -> Tuple[str, Options]:
|
||||
options: Dict[str, Union[int, str]] = {}
|
||||
else:
|
||||
options = {
|
||||
m.group(1).lower(): (m.group(2) or m.group(3))
|
||||
m.group(1)
|
||||
.lower(): (m.group(2) or m.group(3))
|
||||
.replace("%22", '"')
|
||||
.replace("%0D%0A", "\n")
|
||||
for m in _param.finditer(value[pos:])
|
||||
|
||||
@@ -14,7 +14,7 @@ class ExceptionMixin(metaclass=SanicMeta):
|
||||
def exception(
|
||||
self,
|
||||
*exceptions: Union[Type[Exception], List[Type[Exception]]],
|
||||
apply: bool = True,
|
||||
apply: bool = True
|
||||
) -> Callable:
|
||||
"""Decorator used to register an exception handler for the current application or blueprint instance.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum, auto
|
||||
from functools import partial
|
||||
from typing import Callable, List, Optional, Union, overload
|
||||
|
||||
from operator import attrgetter
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.exceptions import BadRequest
|
||||
from sanic.models.futures import FutureListener
|
||||
@@ -25,7 +25,7 @@ class ListenerEvent(str, Enum):
|
||||
AFTER_RELOAD_TRIGGER = auto()
|
||||
|
||||
|
||||
class ListenerMixin(metaclass=SanicMeta):
|
||||
class ListenerMixin(metaclass=SanicMeta):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self._future_listeners: List[FutureListener] = []
|
||||
|
||||
@@ -38,6 +38,8 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||
listener_or_event: ListenerType[Sanic],
|
||||
event_or_none: str,
|
||||
apply: bool = ...,
|
||||
*,
|
||||
priority: int = 0,
|
||||
) -> ListenerType[Sanic]:
|
||||
...
|
||||
|
||||
@@ -47,6 +49,8 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||
listener_or_event: str,
|
||||
event_or_none: None = ...,
|
||||
apply: bool = ...,
|
||||
*,
|
||||
priority: int = 0,
|
||||
) -> Callable[[ListenerType[Sanic]], ListenerType[Sanic]]:
|
||||
...
|
||||
|
||||
@@ -55,6 +59,8 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||
listener_or_event: Union[ListenerType[Sanic], str],
|
||||
event_or_none: Optional[str] = None,
|
||||
apply: bool = True,
|
||||
*,
|
||||
priority: int = 0,
|
||||
) -> Union[
|
||||
ListenerType[Sanic],
|
||||
Callable[[ListenerType[Sanic]], ListenerType[Sanic]],
|
||||
@@ -81,6 +87,7 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||
listener_or_event (Union[ListenerType[Sanic], str]): A listener function or an event name.
|
||||
event_or_none (Optional[str]): The event name to listen for if `listener_or_event` is a function. Defaults to `None`.
|
||||
apply (bool): Whether to apply the listener immediately. Defaults to `True`.
|
||||
priority (int): The priority of the listener. Defaults to `0`.
|
||||
|
||||
Returns:
|
||||
Union[ListenerType[Sanic], Callable[[ListenerType[Sanic]], ListenerType[Sanic]]]: The listener or a callable that takes a listener.
|
||||
@@ -96,7 +103,7 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||
""" # noqa: E501
|
||||
|
||||
def register_listener(
|
||||
listener: ListenerType[Sanic], event: str
|
||||
listener: ListenerType[Sanic], event: str, priority: int = 0
|
||||
) -> ListenerType[Sanic]:
|
||||
"""A helper function to register a listener for an event.
|
||||
|
||||
@@ -112,7 +119,7 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||
"""
|
||||
nonlocal apply
|
||||
|
||||
future_listener = FutureListener(listener, event)
|
||||
future_listener = FutureListener(listener, event, priority)
|
||||
self._future_listeners.append(future_listener)
|
||||
if apply:
|
||||
self._apply_listener(future_listener)
|
||||
@@ -123,9 +130,9 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||
raise BadRequest(
|
||||
"Invalid event registration: Missing event name."
|
||||
)
|
||||
return register_listener(listener_or_event, event_or_none)
|
||||
return register_listener(listener_or_event, event_or_none, priority)
|
||||
else:
|
||||
return partial(register_listener, event=listener_or_event)
|
||||
return partial(register_listener, event=listener_or_event, priority=priority)
|
||||
|
||||
def main_process_start(
|
||||
self, listener: ListenerType[Sanic]
|
||||
|
||||
@@ -25,7 +25,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
attach_to: str = "request",
|
||||
apply: bool = True,
|
||||
*,
|
||||
priority: int = 0,
|
||||
priority: int = 0
|
||||
) -> MiddlewareType:
|
||||
...
|
||||
|
||||
@@ -36,7 +36,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
attach_to: str = "request",
|
||||
apply: bool = True,
|
||||
*,
|
||||
priority: int = 0,
|
||||
priority: int = 0
|
||||
) -> Callable[[MiddlewareType], MiddlewareType]:
|
||||
...
|
||||
|
||||
@@ -46,7 +46,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
attach_to: str = "request",
|
||||
apply: bool = True,
|
||||
*,
|
||||
priority: int = 0,
|
||||
priority: int = 0
|
||||
) -> Union[MiddlewareType, Callable[[MiddlewareType], MiddlewareType]]:
|
||||
"""Decorator for registering middleware.
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ class SignalMixin(metaclass=SanicMeta):
|
||||
apply: bool = True,
|
||||
condition: Optional[Dict[str, Any]] = None,
|
||||
exclusive: bool = True,
|
||||
priority: int = 0,
|
||||
) -> Callable[[SignalHandler], SignalHandler]:
|
||||
"""
|
||||
For creating a signal handler, used similar to a route handler:
|
||||
@@ -51,7 +52,7 @@ class SignalMixin(metaclass=SanicMeta):
|
||||
|
||||
def decorator(handler: SignalHandler):
|
||||
future_signal = FutureSignal(
|
||||
handler, event_value, HashableDict(condition or {}), exclusive
|
||||
handler, event_value, HashableDict(condition or {}), exclusive, priority
|
||||
)
|
||||
self._future_signals.add(future_signal)
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class FutureRoute(NamedTuple):
|
||||
class FutureListener(NamedTuple):
|
||||
listener: ListenerType
|
||||
event: str
|
||||
priority: int
|
||||
|
||||
|
||||
class FutureMiddleware(NamedTuple):
|
||||
@@ -65,6 +66,7 @@ class FutureSignal(NamedTuple):
|
||||
event: str
|
||||
condition: Optional[Dict[str, str]]
|
||||
exclusive: bool
|
||||
priority: int
|
||||
|
||||
|
||||
class FutureRegistry(set):
|
||||
|
||||
@@ -44,8 +44,12 @@ class ConnInfo:
|
||||
self.server_name = ""
|
||||
self.cert: Dict[str, Any] = {}
|
||||
self.network_paths: List[Any] = []
|
||||
sslobj: Optional[SSLObject] = transport.get_extra_info("ssl_object") # type: ignore
|
||||
sslctx: Optional[SSLContext] = transport.get_extra_info("ssl_context") # type: ignore
|
||||
sslobj: Optional[SSLObject] = transport.get_extra_info(
|
||||
"ssl_object"
|
||||
) # type: ignore
|
||||
sslctx: Optional[SSLContext] = transport.get_extra_info(
|
||||
"ssl_context"
|
||||
) # type: ignore
|
||||
if sslobj:
|
||||
self.ssl = True
|
||||
self.server_name = getattr(sslobj, "sanic_server_name", None) or ""
|
||||
|
||||
@@ -57,7 +57,9 @@ class WebsocketFrameAssembler:
|
||||
self.read_mutex = asyncio.Lock()
|
||||
self.write_mutex = asyncio.Lock()
|
||||
|
||||
self.completed_queue = asyncio.Queue(maxsize=1) # type: asyncio.Queue[Data]
|
||||
self.completed_queue = asyncio.Queue(
|
||||
maxsize=1
|
||||
) # type: asyncio.Queue[Data]
|
||||
|
||||
# put() sets this event to tell get() that a message can be fetched.
|
||||
self.message_complete = asyncio.Event()
|
||||
|
||||
@@ -250,6 +250,8 @@ class SignalRouter(BaseRouter):
|
||||
event: str,
|
||||
condition: Optional[Dict[str, Any]] = None,
|
||||
exclusive: bool = True,
|
||||
*,
|
||||
priority: int = 0,
|
||||
) -> Signal:
|
||||
event_definition = event
|
||||
parts = self._build_event_parts(event)
|
||||
@@ -268,6 +270,7 @@ class SignalRouter(BaseRouter):
|
||||
handler,
|
||||
name=name,
|
||||
append=True,
|
||||
priority=priority,
|
||||
) # type: ignore
|
||||
|
||||
signal.ctx.exclusive = exclusive
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Dict, ItemsView, Iterator, KeysView, List, ValuesView
|
||||
from typing import Any, Dict, ItemsView, Iterator, KeysView, List
|
||||
from typing import Mapping as MappingType
|
||||
from typing import ValuesView
|
||||
|
||||
|
||||
dict
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
|
||||
from os import path
|
||||
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import click
|
||||
import towncrier
|
||||
import click
|
||||
except ImportError:
|
||||
print(
|
||||
"Please make sure you have a installed towncrier and click before using this tool"
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from collections import OrderedDict
|
||||
from configparser import RawConfigParser
|
||||
from datetime import datetime
|
||||
from json import dumps
|
||||
from os import chdir, path
|
||||
from subprocess import PIPE, Popen
|
||||
from os import path, chdir
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import towncrier
|
||||
|
||||
from jinja2 import BaseLoader, Environment
|
||||
from jinja2 import Environment, BaseLoader
|
||||
from requests import patch
|
||||
|
||||
import sys
|
||||
import towncrier
|
||||
|
||||
GIT_COMMANDS = {
|
||||
"get_tag": ["git describe --tags --abbrev=0"],
|
||||
@@ -81,7 +78,7 @@ def _run_shell_command(command: list):
|
||||
output, error = process.communicate()
|
||||
return_code = process.returncode
|
||||
return output.decode("utf-8"), error, return_code
|
||||
except Exception:
|
||||
except:
|
||||
return None, None, -1
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import bottle
|
||||
import ujson
|
||||
|
||||
from bottle import route
|
||||
from bottle import route, run
|
||||
|
||||
|
||||
@route("/")
|
||||
|
||||
@@ -3,6 +3,8 @@ import os
|
||||
import sys
|
||||
import timeit
|
||||
|
||||
import asyncpg
|
||||
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import ujson
|
||||
|
||||
from wheezy.http import HTTPResponse, WSGIApplication
|
||||
from wheezy.http.response import json_response
|
||||
from wheezy.routing import url
|
||||
from wheezy.web.handlers import BaseHandler
|
||||
from wheezy.web.middleware import (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from collections import deque, namedtuple
|
||||
@@ -11,7 +12,7 @@ from pytest import MonkeyPatch
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.application.state import Mode
|
||||
from sanic.asgi import Lifespan, MockTransport
|
||||
from sanic.asgi import ASGIApp, Lifespan, MockTransport
|
||||
from sanic.exceptions import BadRequest, Forbidden, ServiceUnavailable
|
||||
from sanic.request import Request
|
||||
from sanic.response import json, text
|
||||
|
||||
@@ -22,7 +22,7 @@ def test_bp_group_indexing(app: Sanic):
|
||||
group = Blueprint.group(blueprint_1, blueprint_2)
|
||||
assert group[0] == blueprint_1
|
||||
|
||||
with raises(expected_exception=IndexError):
|
||||
with raises(expected_exception=IndexError) as e:
|
||||
_ = group[3]
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import asyncio
|
||||
|
||||
from asyncio import CancelledError
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Request, Sanic, json
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from unittest.mock import Mock
|
||||
import pytest
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from pytest import LogCaptureFixture, MonkeyPatch
|
||||
from pytest import LogCaptureFixture, MonkeyPatch, WarningsRecorder
|
||||
|
||||
from sanic import Sanic, handlers
|
||||
from sanic.exceptions import BadRequest, Forbidden, NotFound, ServerError
|
||||
@@ -169,7 +169,7 @@ def test_exception_handler_lookup(exception_handler_app: Sanic):
|
||||
pass
|
||||
|
||||
try:
|
||||
ModuleNotFoundError # noqa: F823
|
||||
ModuleNotFoundError
|
||||
except Exception:
|
||||
|
||||
class ModuleNotFoundError(ImportError):
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import sys
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
try:
|
||||
import sanic_ext # noqa: F401
|
||||
import sanic_ext
|
||||
|
||||
SANIC_EXT_IN_ENV = True
|
||||
except ImportError:
|
||||
|
||||
@@ -57,7 +57,7 @@ def raised_ceiling():
|
||||
# Chrome, Firefox:
|
||||
# Content-Disposition: form-data; name="foo%22;bar\"; filename="😀"
|
||||
'form-data; name="foo%22;bar\\"; filename="😀"',
|
||||
("form-data", {"name": 'foo";bar\\', "filename": "😀"}),
|
||||
("form-data", {"name": 'foo";bar\\', "filename": "😀"})
|
||||
# cgi: ('form-data', {'name': 'foo%22;bar"; filename="😀'})
|
||||
# werkzeug (pre 2.3.0): ('form-data', {'name': 'foo%22;bar"; filename='})
|
||||
),
|
||||
|
||||
@@ -14,6 +14,7 @@ from sanic_testing.testing import HOST, PORT
|
||||
from sanic import Blueprint, text
|
||||
from sanic.compat import use_context
|
||||
from sanic.log import logger
|
||||
from sanic.server.socket import configure_socket
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
|
||||
@@ -573,7 +573,7 @@ def test_streaming_echo():
|
||||
|
||||
async def client(app, reader, writer):
|
||||
# Unfortunately httpx does not support 2-way streaming, so do it by hand.
|
||||
host = "host: localhost:8000\r\n".encode()
|
||||
host = f"host: localhost:8000\r\n".encode()
|
||||
writer.write(
|
||||
b"POST /echo HTTP/1.1\r\n" + host + b"content-length: 2\r\n"
|
||||
b"content-type: text/plain; charset=utf-8\r\n"
|
||||
@@ -581,7 +581,7 @@ def test_streaming_echo():
|
||||
)
|
||||
# Read response
|
||||
res = b""
|
||||
while b"\r\n\r\n" not in res:
|
||||
while not b"\r\n\r\n" in res:
|
||||
res += await reader.read(4096)
|
||||
assert res.startswith(b"HTTP/1.1 200 OK\r\n")
|
||||
assert res.endswith(b"\r\n\r\n")
|
||||
@@ -589,7 +589,7 @@ def test_streaming_echo():
|
||||
|
||||
async def read_chunk():
|
||||
nonlocal buffer
|
||||
while b"\r\n" not in buffer:
|
||||
while not b"\r\n" in buffer:
|
||||
data = await reader.read(4096)
|
||||
assert data
|
||||
buffer += data
|
||||
@@ -618,6 +618,6 @@ def test_streaming_echo():
|
||||
assert res == b"-"
|
||||
|
||||
res = await read_chunk()
|
||||
assert res is None
|
||||
assert res == None
|
||||
|
||||
app.run(access_log=False, single_process=True)
|
||||
|
||||
@@ -2011,11 +2011,11 @@ def test_server_name_and_url_for(app):
|
||||
app.config.SERVER_NAME = "my-server" # This means default port
|
||||
assert app.url_for("handler", _external=True) == "http://my-server/foo"
|
||||
request, response = app.test_client.get("/foo")
|
||||
assert request.url_for("handler") == "http://my-server/foo"
|
||||
assert request.url_for("handler") == f"http://my-server/foo"
|
||||
|
||||
app.config.SERVER_NAME = "https://my-server/path"
|
||||
request, response = app.test_client.get("/foo")
|
||||
url = "https://my-server/path/foo"
|
||||
url = f"https://my-server/path/foo"
|
||||
assert app.url_for("handler", _external=True) == url
|
||||
assert request.url_for("handler") == url
|
||||
|
||||
@@ -2180,7 +2180,7 @@ def test_safe_method_with_body_ignored(app):
|
||||
)
|
||||
|
||||
assert request.body == b""
|
||||
assert request.json is None
|
||||
assert request.json == None
|
||||
assert response.body == b"OK"
|
||||
|
||||
|
||||
|
||||
@@ -610,7 +610,7 @@ def test_multiple_responses(
|
||||
|
||||
@app.get("/4")
|
||||
async def handler4(request: Request):
|
||||
await request.respond(headers={"one": "one"})
|
||||
response = await request.respond(headers={"one": "one"})
|
||||
return json({"foo": "bar"}, headers={"one": "two"})
|
||||
|
||||
@app.get("/5")
|
||||
@@ -641,6 +641,10 @@ def test_multiple_responses(
|
||||
"been responded to."
|
||||
)
|
||||
|
||||
error_msg3 = (
|
||||
"Response stream was ended, no more "
|
||||
"response data is allowed to be sent."
|
||||
)
|
||||
|
||||
with caplog.at_level(ERROR):
|
||||
_, response = app.test_client.get("/1")
|
||||
@@ -765,7 +769,7 @@ def test_file_response_headers(
|
||||
assert (
|
||||
"cache-control" in headers
|
||||
and f"max-age={test_max_age}" in headers.get("cache-control")
|
||||
and "public" in headers.get("cache-control")
|
||||
and f"public" in headers.get("cache-control")
|
||||
)
|
||||
assert (
|
||||
"expires" in headers
|
||||
@@ -796,14 +800,14 @@ def test_file_response_headers(
|
||||
|
||||
_, response = app.test_client.get(f"/files/no_cache/{file_name}")
|
||||
headers = response.headers
|
||||
assert "cache-control" in headers and "no-cache" == headers.get(
|
||||
assert "cache-control" in headers and f"no-cache" == headers.get(
|
||||
"cache-control"
|
||||
)
|
||||
assert response.status == 200
|
||||
|
||||
_, response = app.test_client.get(f"/files/no_store/{file_name}")
|
||||
headers = response.headers
|
||||
assert "cache-control" in headers and "no-store" == headers.get(
|
||||
assert "cache-control" in headers and f"no-store" == headers.get(
|
||||
"cache-control"
|
||||
)
|
||||
assert response.status == 200
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# flake8: noqa: E501
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
Reference in New Issue
Block a user