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.

Cookies#

Cookies are exposed as a regular dictionary interface through cookies:

session = request.cookies.get('session', '')

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:

str

(default) accepts any text without a slash

int

accepts positive integers

float

accepts positive floating point values

path

like string but also accepts slashes

uuid

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:

  1. If a result is response Response it’s directly returned from the view.

  2. If it’s a string, a response ResponseHTML is created with that data and the default parameters.

  3. If it’s a dict/list/bool/None, a response ResponseJSON is created

  4. If 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). The status:int value will override the status code and headers:dict[str, str] a list or dictionary of additional header values.

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

  1. 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)
    
  2. 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)
    
  3. 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).