Usage#
ASGI Application#
ASGI-Tools is designed to be used as an ASGI Toolkit. You can use any of its
components independently. Dispite this ASGI-Tools contains
asgi_tools.App
helper to quickly build ASGI applications.
Note
If you are looking more for a toolkit for your ASGI apps go straight to the API Reference docs.
from asgi_tools import App
app = App()
@app.route("/")
async def hello_world(request):
return "<p>Hello, World!</p>"
Save it as hello.py
or something similar.
ASGI-Tools belongs to the category of ASGI web frameworks, so it requires an ASGI HTTP server to run, such as uvicorn, daphne, or hypercorn.
To run the application, run the command:
uvicorn hello:app
This launches a very simple builtin server, which is good enough for testing but probably not what you want to use in production.
Now head over to http://127.0.0.1:8000/, and you should see your hello world greeting.
The Request Object#
Every callback should accept a request object. The request object is
documented in the API section and we will not cover it here in detail (see
Request
). Here is a broad overview of some of the most
common operations.
The current request method is available by using the
method
attribute. To access form data (data
transmitted in a POST
or PUT
request) you can use the
form
attribute. Here is a full example of the two
attributes mentioned above:
@app.route('/login', methods=['POST', 'PUT'])
async def login(request):
error = None
if request.method == 'POST':
formdata = await request.form()
if valid_login(formdata['username'], formdata['password']):
return log_the_user_in(formdata['username'])
error = 'Invalid username/password'
return render_template('login.html', error=error)
To access parameters submitted in the URL (?key=value
) you can use the
query
attribute:
search = request.query.get('search', '')
We recommend accessing URL parameters with get or by catching the
KeyError
because users might change the URL and presenting them a 400
bad request page in that case is not user friendly.
For a full list of methods and attributes of the request object, head over
to the Request
documentation.
File Uploads#
Request files are normally sent as multipart form data (multipart/form-data).
The uploaded files are available in form()
:
formdata = await request.form()
Routing#
Modern web applications use meaningful URLs to help users. Users are more likely to like a page and come back if the page uses a meaningful URL they can remember and use to directly visit a page.
Use the route()
decorator to bind a function to a URL.
@app.route('/')
def index():
return 'Index Page'
@app.route('/hello', '/hello/world')
async def hello():
return 'Hello, World'
@app.route('/only-post', methods=['POST'])
async def only_post():
return request.method
You can do more! You can make parts of the URL dynamic. The every routed
callback should be callable and accepts a Request
.
See also: HTTPView
.
Dynamic URLs#
All the URLs support regexp. You can use any regular expression to customize your URLs:
import re
@app.route(re.compile(r'/reg/(a|b|c)/?'))
async def regexp(request):
return request.path
Variable Rules#
You can add variable sections to a URL by marking sections with
{variable_name}
. Your function then receives the {variable_name}
from
request.path_params
.
@app.route('/user/{username}')
async def show_user_profile(request):
username = request.path_params['username']
return f'User {username}'
By default this will capture characters up to the end of the path or the next /.
Optionally, you can use a converter to specify the type of the argument like
{variable_name:converter}
.
Converter types:
|
(default) accepts any text without a slash |
|
accepts positive integers |
|
accepts positive floating point values |
|
like string but also accepts slashes |
|
accepts UUID strings |
Convertors are used by prefixing them with a colon, like so:
@app.route('/post/{post_id:int}')
async def show_post(request):
post_id = request.path_params['post_id']
return f'Post # {post_id}'
Any unknown convertor will be parsed as a regex:
@app.route('/orders/{order_id:\d{3}}')
async def orders(request):
order_id = request.path_params['order_id']
return f'Order # {order_id}'
Static Files#
Set static url prefix and directories when initializing your app:
from asgi_tools import App
app = App(static_url_prefix='/assets', static_folders=['static'])
And your static files will be available at url /static/{file}
.
Redirects and Errors#
To redirect a user to another endpoint, use the ResponseRedirect
class; to abort a request early with an error code, use the
ResponseError()
class:
from asgi_tools import ResponseRedirect, ResponseError
@app.route('/')
async def index(request):
return ResponseRedirect('/login')
@app.route('/login')
async def login(request):
raise ResponseError(status_code=401)
this_is_never_executed()
This is a rather pointless example because a user will be redirected from the index to a page they cannot access (401 means access denied) but it shows how that works.
By default only description is shown for each error code. If you want to
customize the error page, you can use the on_error()
decorator:
@app.on_error(ResponseError)
async def process_http_errors(request, error):
if error.status_code == 404:
return render_template('page_not_found.html'), 404
return error
It’s possible to bind the handlers not only for status codes, but for the exceptions themself:
@app.on_error(TimeoutError)
async def timeout(request, error):
return 'Something bad happens'
About Responses#
The return value from a view function is automatically converted into a
response object for you. If the return value is a string it’s converted into a
response object with the string as response body, a 200 OK
status code and
a text/html mimetype. If the return value is a dict or list,
json.dumps()
is called to produce a response. The logic that ASGI-Tools
applies to converting return values into response objects is as follows:
If a result is response
Response
it’s directly returned from the view.If it’s a string, a response
ResponseHTML
is created with that data and the default parameters.If it’s a dict/list/bool/None, a response
ResponseJSON
is createdIf a tuple is returned the items in the tuple can provide extra information. Such tuples have to be in the form
(status, response content)
,(status, response content, headers)
. Thestatus
:int
value will override the status code andheaders
:dict[str, str]
a list or dictionary of additional header values.If none of that works, ASGI-Tools will convert the return value to a string and return as html.
@app.route('/html')
async def html(request):
return '<b>HTML is here</b>'
@app.route('/json')
async def json(request):
return {'json': 'here'}
@app.route('/text')
async def text(request):
res = ResponseText('response is here')
res.headers['x-custom'] = 'value'
res.cookies['x-custom'] = 'value'
return res
@app.route('/short-form')
async def short_form(request):
return 418, 'Im a teapot'
Middlewares#
asgi_tools.App
supports middlewares, which provide a flexible way
to define a chain of functions that handles every web requests.
As an ASGI application asgi_tools.App can be proxied with any ASGI middlewares:
from asgi_tools import App from sentry_asgi import SentryMiddleware app = App() app = SentryMiddleware(app)
Alternatively you can decorate any ASGI middleware to connect it to an app:
from asgi_tools import App from sentry_asgi import SentryMiddleware app = App() app.middleware(SentryMiddleware)
Internal middlewares. For middlewares it’s possible to use simpler interface which one accepts a request and can return responses.
from asgi_tools import App app = App() @app.middleware async def simple_md(app, request, receive, send): try: response = await app(request, receive, send) response.headers['x-simple-md'] = 'passed' return response except RuntimeError: return ResponseHTML('Middleware Exception')
Nested applications#
Sub applications are designed for solving the problem of the big monolithic code base.
from asgi_tools import App
# Main application
app = App()
@app.route('/')
def index(request):
return 'OK'
# Sub application
subapp = App()
@subapp.route('/route')
def subpage(request):
return 'OK from subapp'
# Connect the subapplication with an URL prefix
app.route('/sub')(subapp)
# await client.get('/sub/route').text() == 'OK from subapp'
Middlewares from app and subapp are chained (only internal middlewares are supported for nested apps).