Skip to main content

A Lyte (light) Async Viewset.

Project description

LAViewSet

A Lyte (light) Asynchronous ViewSet.

A ViewSets package, a-la Django Rest Framework - ViewSets, built on top of aiohttp.web.

*********************************************************

Getting Started


Quick Start

# laviewset_intro.py

from aiohttp import web
from laviewset import Route, ViewSet, HttpMethods

app = web.Application()
base_route = Route.create_base(app.router)      # '/'


class ListingsViewSet(ViewSet):

    route = base_route.extend('listings')  # '/listings'

    @route('/', HttpMethods.GET)
    async def list(self, request):
        assert isinstance(request, web.Request)
        return web.Response(text='GET at '/listings')


web.run_app(app)

For a step-by-step walkthrough, continue reading below.

Or, skip ahead for a more thorough look at fully extending laviewset.ViewSet.


Intro

The first step is to create a base route by passing the aiohttp.web.UrlDispatcher of your current application into Route.create_base:

# laviewset_intro.py

from aiohttp import web
from laviewset import Route

app = web.Application()
base_route = Route.create_base(app.router)      # '/'

base_route can then be extended into resources that you want to include in your ViewSets:

listings_route = base_route.extend('listings')  # '/listings'
events_route = base_route.extend('/events')     # '/events'

# We can further extend a resource
sessions_route = listings.extend('sessions')    # '/listings/sessions'

Now that we have the resource we want a ViewSet to manage, we can create our ViewSet. This is done by subclassing laviewset.ViewSet, including your route as the route attribute, and overriding the ViewSet methods and/or including your custom views:

# laviewset_intro.py

from aiohttp import web
from laviewset import Route, ViewSet, HttpMethods

app = web.Application()
base_route = Route.create_base(app.router)      # '/'


class ListingsViewSet(ViewSet):

    route = base_route.extend('listings')  # '/listings'
    serializer = 'some_serializer'

    @route('/', HttpMethods.GET)
    async def list(self, request):
        ...
        return web.Response(text='GET at /listings with {self.serializer}')


web.run_app(app)

Note, the code above is similar to the following:

from aiohttp import web


serializer = 'some_serializer'

def handler(request):
    ...
    return web.Response(text='GET at /listings with {self.serializer}')


app = web.Application()
app.add_routes([web.get('/', handler)])
web.run_app(app)

ViewSet methods

@route decorator

In order to create a view on the ViewSet, the @route decorator is required. Since each view is essentially a wrapper over aiohttp.web.route, the arguments passed into the decorator correlate with the arguments for web.route: the first argument is the path, the second argument is the HTTP method for the view (method), any other keyword argument passed into the decorator will be included as kwargs to the web.route method, and finally, the view itself will be the handler.

    @route('/', HttpMethods.GET, z=20, f='abc')     # z=20 and f='abc' will be
    async def list(self, request):                  # passed into web.route
        ...
        return web.Response(text='GET at '/listings')

Since the idea behind laviewset is an asynchronous ViewSet a-la Django Rest Framework - ViewSets, the methods list, create, retrieve, update, partial_update, and delete are included on the base class laviewset.ViewSet. However, unlike Rest Framework, they are not complete: the user still needs to declare the view using the @route decorator. One reason for this design decision is to allow more flexibility to the user, e.g. to decide on what kwargs to pass into web.route. Trying to access any of the aforementioned methods without overriding and completing them will return a 404NotFound.

View method signatures

The signatures of the views are important. Each view signature requires at least the self and request arguments. The request is in fact a web.Request object, and can be accessed as such: request.query, request.rel_url, etc are all accessible. If the path declared in the @route decorator is a variable path, then the {identifier} should be included in the view signature as a KEYWORD_ONLY argument and have the same name as the identifier included in the path, otherwise an laviewset.ViewSignatureError will be raised:

# Correct

    @route(r'/{pk:\d+}', HttpMethods.GET)  # /listings/123
    async def retrieve(self, request, *, pk):   # `pk` is KEYWORD_ONLY and
        assert pk == 123                        # `pk` is same as identifier
        return web.Response(text=f'retrieved {pk}')

# Incorrect

    @route(r'/{pk:\d+}', HttpMethods.GET)
    async def retrieve(self, request, pk):      # `pk` is not KEYWORD_ONLY
        ...
        return web.Response(text=f'retrieved {pk}')

# Incorrect

    @route(r'/{fk:\d+}', HttpMethods.GET)
    async def retrieve(self, request, *, pk):  # `pk` != `fk`
        ...
        return web.Response(text=f'retrieved {pk}')


Custom views

Custom views can also be defined. Simply wrap a method with the @route decorator and follow the rules described above:

    # Custom GET view
    # '/listings/123/events/Coachella'
    @route(r'/{pk:\d+}/events/{name:\w+}', HttpMethods.GET)
    async def custom_get(self, request, *, pk, name):
        assert pk == 123
        assert name == 'Coachella'
        return web.Response(text=f'GET at /listings/{pk}/events/{name}')

    # Custom DELETE view
    # '/listings/custom_delete/123/Coachella'
    @route(r'/custom_delete/{pk:\d+}/{name:\w+}', HttpMethods.DELETE)
    async def custom_delete(self, request, *, pk, name):
        assert pk == 123
        assert name == 'Coachella'
        return web.Response(text=f'Deleting something to do with {pk} {name}')

A short note on errors:
All laviewset errors are raised "statically", i.e. before your server is up and running.


Project structure

Since ViewSets do not need to be initialized, it is important to let the module your app is running in know about each ViewSet. Therefore, for more complex project structures, the following structure is recommended:

proj/
├── package/
│   ├── __init__.py
│   ├── __main__.py
│   ├── server.py
│   ├── app1/
│   │   └── views.py
│   └── app2/
│       └── views.py
├── conf.py
└── README.md
# package/server.py

from aiohttp import web
from laviewset import Route

app = web.Application()
base_route = Route.create_base(app.router)


def run_server(app: web.Application) -> None:
    web.run_app(app, host='localhost', port=8000)

# package/app1/views.py

from laviewset import ViewSet
from ..server import base_route
...
# package/app2/views.py

from laviewset import ViewSet
from ..server import base_route
...
# package/__init__.py

# Now `server.app` knows about `ViewSet`s.
from .app1 import views
from .app2 import views
# package/__main__.py

from .server import app, run_server

run_server(app)

*********************************************************

Cheatsheet

# quicksetup.py

from aiohttp import web
from laviewset import Route, ViewSet, HttpMethods

app = web.Application()
base_route = Route.create_base(app.router)


class ListingsViewSet(ViewSet):

    route = base_route.extend('listings')  # '/listings'

    @route('/', HttpMethods.GET)
    async def list(self, request):
        # GET at '/listings'
        ...
        return web.Response(...)

    @route(r'/{pk:\d+}', HttpMethods.GET)
    async def retrieve(self, request, *, pk):
        # GET at '/listings/{pk}'
        # where the dynamic value {pk} can be accessed 
        # through `pk`.
        ...
        return web.Response(...)

    @route('/', HttpMethods.POST)
    async def create(self, request):
        # POST at '/listings'
        data = await request.json()
        return web.Response(text=f'Metrics Created data: {data}')

    @route(r'/{pk:\d+}', HttpMethods.DELETE)
    async def delete(self, request, *, pk):
        # DELETE at '/listings/{pk}'
        ...
        return web.Response(...)

    @route(r'/{pk:\d+}', HttpMethods.PUT)
    async def update(self, request, *, pk):
        # PUT at '/listings/{pk}'
        ...
        return web.Response(...)

    @route(r'/{pk:\d+}', HttpMethods.PATCH)
    async def partial_update(self, request, *, pk):
        # PATCH at '/listings/{pk}'
        ...
        return web.Response(...)

    @route(r'/{pk:\d+}/do_thing/{name:\w+}', HttpMethods.GET)
    async def custom_view(self, request, *, pk, name):
        # GET at '/listings/{pk}/do_thing/{name}'
        ...
        return web.Response(...)

*********************************************************

Requirements

  • Python >= 3.8
  • aiohttp==3.6.2

*********************************************************

Installation

This package does not exist on PyPI yet, so the only way to install it is through LAViewSet/setup.py.

*********************************************************

License

laviewset is offered under the MIT license.

This package uses the aiohttp package, which is distributed under the Apache 2 license.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

LAViewSet-0.0.1.tar.gz (9.5 kB view hashes)

Uploaded Source

Built Distribution

LAViewSet-0.0.1-py3-none-any.whl (10.1 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page