Skip to content

REST Views

Pokie provides several helpers to implement REST endpoints; one of them is the RestView class. This class implements all the basic REST operations for CRUD operations on a given Record/Service. Service can either be the generic pokie.rest.RestService, or a custom service that implements pokie.rest.RestServiceInterface.

There are also advanced helpers that provide endpoint generation with a greater degree of automation, and based on pokie.rest.RestView. These helpers are described in detail in the Auto REST section.

Resource-based REST views - RestView

The pokie.rest.RestView provides the basic CRUD functionality for a RickDb Record class, effectively allowing automatic creation of CRUD endpoints for database objects, while maintaining a three-tier architectural design.

By default, RestView will attempt to create a pokie.rest.RestService instance, uniquely named by module and record class. However, if service_name is specified, the associated service will be used instead. Please note that services to be used with the RestView class need to extend from pokie.rest.RestServiceMixin.

RestView example:

from pokie.rest import RestView
from rick.form import RequestRecord, field
from rick_db import fieldmapper


# DB Record class
@fieldmapper(tablename="country", pk="country_id", schema="public")
class CountryRecord:
    id = "country_id"
    country = "country"


# RequestRecord class
class CountryRequest(RequestRecord):
    fields = {
        "id": field(validators="id|numeric", bind="id"),
        "country": field(validators="required", bind="country")
    }


# REST view class
class CountryView(RestView):
    # RequestRecord class for body operations
    request_class = CountryRequest

    # Database Record class
    record_class = CountryRecord

    # allowed search fields to be used with the search variable
    search_fields = [CountryRecord.country]

    # optional custom service name to be used; if no service name is specified, an instance of
    # pokie.rest.RestService is automatically created
    # service_name = "my-service-name"

    # optional limit for default listing operations
    # if list_limit > 0, the specified value will be used as default limit for unbounded listing requests
    # list_limit = -1    

Using RestView grid capabilities

By leveraging DbGrid capabilities through a DbGridRequest object, the RestView offers a set of relevant features for advanced scenarios, such as server-side pagination, sorting and search.

the following url variables are available:

Variable Format Example Description
- - /url List all records, see response type below
offset /url?offset=5 List records starting at specified zero-based offset
limit /url?limit=5 List limit amount of records
search /url?search=foo List search results for search expression
match field:value|other_field:other_value[...|field...] /url?match=age:22|gender:M List records with exact match on the set of conditions presented
sort field:asc,field:desc /url?sort=name:asc,age:desc Sort records by specified conditions

Using offset, limit

Offset and limit behave like their SQL counterparts - zero-based integer numbers specifying an absolute value. They can be combined to perform pagination:

# pagination with 10 records per page
/url?offset=10&limit=10

Search is performed in all fields specified in the view class search_fields attribute. The search is performed as case-insensitive %expression%, allowing for matches inside strings. If multiple fields are specified in search_fields, the search is performed with OR concatenation - the result record list is the combination of all individual matches on each field. Some of the internals of the search behaviour can be overridden by providing a custom Service class that extends the pokie.rest.RestServiceMixin mixin and overrides the list() method implementation.

Using sort

Sort can be performed on multiple fields at once, and sort order can either be 'asc' or 'desc' in case-insensitive form.

Default operation and mixing multiple options

By default, a naked GET request to the listing endpoint will return all records; However, this may not be desirable when the dataset size is bigger than a few hundred rows. It is possible to cap the default listing to a given number of rows by changing the list_limit class attribute.

All or part of the specified options can be combined in a single request, to perform a server-side sorting & filtering procedure.

Querying an endpoint with curl, using offset, limit, search and sort on the records:

$ curl -X GET -H 'Content-Type: application/json' http://127.0.0.1:5000/my_url?offset=10&limit=10&search=john&sort=name:desc

Response format

All RestView operations return a variable JSON dataset with the following structure:

{
  "total": 110,
  "items": [
    ...list
    of
    record
    objects...
  ]
}

Where total is the total amount of rows on the source dataset, ignoring offset and limit constraints, allowing the implementation of server-side pagination.

Registering routes

The traditional approach is to register the desired routes in the build() method of the Module class in module.py of your specific module, by using either traditional Flask routes or AutoRouter:

(...)


class Module(BaseModule):
    (...)

    def build(self, parent=None):
        # get Flask application
        app = parent.app
        # register desired routes
        app.add_url_rule('/v1/my-rest-endpoint', methods=['GET', 'POST'],
                         view_func=MyRestView.as_view('my-rest-endpoint'))
        (...)

These routes are created when the module is loaded, so these operations are performed during the initialization of the application. Please note that using Flask route decorators - while possible - it is not supported nor recommended.

RestView views can be added to the router using the traditional Flask approach; however, this process often envolves needless creation of similar code. To simplify this process, the pokie.http.AutoRouter class can be used to automate the registration of all endpoints at once:

from pokie.http import AutoRouter
from pokie.rest import RestView
from rick.form import RequestRecord, field
from rick_db import fieldmapper


# DB Record class
@fieldmapper(tablename="country", pk="country_id", schema="public")
class CountryRecord:
    id = "country_id"
    country = "country"


# RequestRecord class
class CountryRequest(RequestRecord):
    fields = {
        "id": field(validators="id|numeric", bind="id"),
        "country": field(validators="required", bind="country")
    }


# REST view class
class CountryView(RestView):
    pass


(...)

# in our module's module.py:
(...)


class Module(BaseModule):
    (...)

    def build(self, parent=None):
        # register all routes automatically
        AutoRouter.resource(parent.app, "country", CountryView)

The following routes will be registered:

Url Method Description
/country GET List records
/country POST Create record
/country/:id_record GET Get record by id
/country/:id_record PUT Update record
/country/:id_record PATCH Update record
/country/:id_record DELETE Delete record

Please note that AutoRouter doesn't verify if the binding method receives the appropriate arguments, so always make sure that the method signature is preserved when overriding it.

Classes may not implement all available methods for resource manipulation; AutoRouter will only define routes for existing methods whose name match the resource operation - get, post, put, delete, or - alternatively - the controller operation - list, show, create, update, delete.

AutoRouter id_record type

By default, AutoRouter defines id_record as an int value; This can, however, be changed to any Flask supported data type:

(...)


# RequestRecord class
class CountryRequest(RequestRecord):
    fields = {
        "id": field(validators="required|maxlen:4", bind="id"),
        "country": field(validators="required", bind="country")
    }


# REST view class
class CountryView(RestView):
    pass


(...)
# in our module's module.py:
(...)


class Module(BaseModule):
    (...)

    def build(self, parent=None):
        # register all routes automatically, but id_record is of type string
        AutoRouter.resource(parent.app, "country", CountryView, "string")

Controller-style REST views

Pokie also support Controller-style REST views - views where the handler of a given HTTP method can have a custom name. Pokie's base view, pokie.http.PokieView already provides out-of-the-box support for these view types:

# in our view file
from pokie.http import PokieView


# our Controller class:
class CustomerController(PokieView):

    # instead of get(self, id_customer:str=None), we can give it a custom name
    def view_customer(self, id_customer: str):
        """
        Get customer record
        :param id_customer: 
        :return: 
        """
        # attempt to fetch record from our existing customer service
        record = self.svc_customer().get_customer(id_customer)
        if not record:
            return self.not_found()

        # return record if exists
        return self.success(record)

    def svc_customer(self):
        return self.get_service(MY_CUSTOMER_SERVICE_CONSTANT)


(...)

# in our module's module.py:
(...)


class Module(BaseModule):
    (...)

    def build(self, parent=None):
        # register a custom route for the class method called "view_customer"
        app = parent.app
        app.add_url_rule(
            "/v1/customer/<string:id_customer>",
            methods=["GET"],
            view_func=CustomerController.view_method("view_customer"),
        )

Using AutoRouter with Controllers

pokie.http.AutoRouter also provides automatic route registration for controller classes, if they implement the appropriate methods:

Method name Method signature HTTP operation
list list(self) GET
show show(self, id_record) GET
create create(self) POST
update update(self, id_record) PUT, PATCH
delete delete(self, id_record) DELETE

The route registration will only map existing methods:

# in our view file
from pokie.http import PokieView


# our Controller class:
class CustomerController(PokieView):

    # GET handler for customer record
    def show(self, id_record: str):
        """
        Get customer record
        :param id_record: 
        :return: 
        """
        # attempt to fetch record from our existing customer service
        record = self.svc_customer().get_customer(id_record)
        if not record:
            return self.not_found()

        # return record if exists
        return self.success(record)

    # GET handler for customer listing operations 
    def list(self):
        pass

    def svc_customer(self):
        return self.get_service(MY_CUSTOMER_SERVICE_CONSTANT)


(...)

# in our module's module.py:
(...)

class Module(BaseModule):
    (...)

    def build(self, parent=None):

        # register all available controller routes for class CustomerController:
        # /customer [GET] -> CustomerController.list()
        # /customer/<string:id_record> [GET] -> CustomerController.show()        
        AutoRouter.controller(parent.app, "customer", CustomerController, "string")