py-rs 1.0 Concept

This framework is designed to provide lightweight, robust and rapid RESTful service development in Python.

Readers are assumed to be familiar with Python and REST.


Terminology

Target

Python function defining HTTP resource and responsible for producing the response for HTTP Request.

Targets specified by decorating Python functions with py-rs api decorators:

@rs.get
@rs.path('foo')
def foo(): pass
# foo is responsible for processing "GET /foo" request.

Target has following requirements for parameters:

  • Target can specify any number of required parameters. Suitable HTTP Request values will be selected by py-rs to pass into the function parameters:
  • @rs.get
    @rs.path('/foo/{id}')
    def foo(id): pass
    # value of {id} will be passed into *foo* function as *id* value.
    
    
    @rs.get
    @rs.path('/bar')
    def bar(skip, take): pass
    # due to path has no parameters the values of skip and take parameters
    # will be looked up in HTTP Request URI query part:
    #   1. "GET /bar?skip=10&take=100" will be normally served by bar function.
    #   2. If any of the parameters will not be found than _py-rs_ will raise 400 Bad Request error.
    #   3. Unless default value for parameters will be provided within function signature e.g. (skip=0,take=0).
    
    @rs.post
    @rs.path('/baz')
    @rs.consumes('application/x-www-form-urlencoded')
    def baz(firstname, lastname, email, sex): pass
    # the values for baz parameters are expected to be the form values of HTTP Request "POST /baz".
  • If Target is a method of Type class then the first(self) parameter will be an instance of Type class created by py-rs as singleton object of that type and passed into during method invocation:
  • @rs.get
    class Foo
    
      @rs.path('/foo')
      def foo(self): pass
    
      @rs.path('/bar')
      def bar(self): pass
    
    # requests "GET /foo" and "GET /bar" will invoke foo and bar methods with the same Foo instance.
  • Special Context properties can be used as default values of Target's parameters to specify context depended values such as:
    1. rs.context.entity defines parameter that will get HTTP Request Entity:
    2. @rs.post
      def foo(entity=rs.context.entity): pass
      # "POST /" entity will be passed into foo function as entity parameter.
    3. rs.context.request defines parameter that will get HTTP Request instance of rs.request.Request itself:
    4. @rs.post
      def foo(request=rs.context.request): pass
      # "POST /" request will be passed into foo function as request parameter.
    5. rs.context.uri defines parameter that will get HTTP Request URI object:
    6. @rs.post
      def foo(request=rs.context.ui): pass
      # "POST /" / URI(instance of rs.message.uri) will be passed into foo function as uri parameter.
    7. rs.context.alias defines parameter's alias that will be associated with suitable value from available parameters of the request:
    8. @rs.get
      @rs.path('/{class}')
      def foo(klass=rs.context.alias('class')): pass
      # "GET /{class}" value of rs.path class param will be associated with klass through 'class' alias.
      
      @rs.get
      @rs.path('/bar')
      def bar(from_=rs.context.alias('from'), length): pass
      # value of 'from' alias will be looked up in query params of "GET /bar" request, if not found 400 Bad Request
      # is returned.
      
      @rs.get
      @rs.path('/baz')
      def baz(from_=rs.context.alias('from', default='0'), length): pass
      # if default value for alias is specified than in case of not found 'from' parameter it will be used and
      # no error will be raised.

Target has following requirements for return values:

  • Basic Target return value is a tuple with three elements: entity, status, headers where:
    1. entity value is response entity produced by Target or the value for entity Producer if Target has specified one.
    2. status value is status code returned in HTTP Response by Target. status is optional and can be omitted.
    3. headers value is dictionary with custom HTTP Headers to be appended to the response. headers are optional and can be omitted.
  • Some examples of return values are:
  • return entity                                  # returns entity, 200 OK will be chosen as default status code.
    
    return entity, 200                             # returns 200 OK and entity.
    
    return entity, 201, {'Location':'/entity'}     # returns 201 Created, entity and Location header.
  • Due to the return value for user's Producer can be a tuple itself it is required to explicitly put comma in the return statement:
  • return (entity, 200), # tuple (entity, 200) is considered as single response entity to return.
  • If Target returns None as entity the 204 No Content status code will be returned to HTTP Client.

If Target needs to produce HTTP error code as HTTP Response rs.error instance with required status code should be raised:

@rs.get
def foo():
  raise rs.error(400) # will return 400 Bad Request response.

Type

Python class representing the container of Target methods.

Types specified by decorating Python classes with py-rs api decorators:

@rs.path('foo')
class Foo:

  @rs.get
  def foo(self): pass

  @rs.post
  def bar(self): pass

  @rs.put
  def baz(self): pass

  @rs.delete
  def qux(self): pass

All py-rs api decorators of Type are inherited by its methods.

Instances of Type for single HTTP Resource are considered to be singletons during the resource's lifetime.

Type __init__ method signature should have been able to be invoked without parameters:

@rs.path('foo')
class Foo:
  def __init__(self): pass

# or

@rs.path('bar')
class Bar:
  def __init__(self, logger=some_logger): pass

# both classes can instantiate objects as Foo() and Bar()

Inner Type classes supported:

@rs.path('foo')
class Foo:

  @rs.path('bar')
  class Bar:

    @rs.get
    def bar(self): pass

# "GET /foo/bar" request will be processed by Foo.Bar.bar method.

Path

Path value used to identify Target upon which to apply HTTP Request.

rs.path api decorator specifies the target's Path:

@rs.get
@rs.path('foo')
def foo(): pass
# associate foo function with /foo HTTP Request URI.

To specify Path parameters use {name of parameter}-syntax:

@rs.get
@rs.path('foo/{id}'
def foo(id): pass
# {id} will be substituted by the part following foo/ of "GET /foo/*" requests.

Regular expression syntax supported:

@rs.post
@rs.path('re(set|start|load)')
def reset(): pass
# HTTP Requests with URI like '/restart', '/reset', '/reload' all will be processed by reset function.

Due to there can be some conflict situations with Path resolution the current implementation is using length of the Path value as a partial key to resolve ambiguous situations and applies the longest Path as the priority one unless Path value will contain zero number of capturing groups(considered as exact match):

@rs.post
@rs.path('re(set|start|load)')
def reset(): pass

@rs.post
@rs.path('restart')
def restart(): pass

# "POST /restart" will be processed by restart function.

If py-rs will not find Target with suitable Path for HTTP Request 404 Not Found will be returned.

Method

Method indicates the HTTP Method to be performed on Target identified by Path.

Following api decorators define the target's Method:

  • rs.get - defines HTTP GET method of the target.
  • rs.post - defines HTTP POST method.
  • rs.put - defines HTTP PUT method.
  • rs.delete - defines HTTP DELETE method.
  • rs.method(name) - defines any other HTTP Method specified by name parameter.
@rs.get
def get(): pass
# serves "GET /" requests.

@rs.post
def post(): pass
# serves "POST /" requests.

@rs.put
def put(): pass
# serves "PUT /" requests.

@rs.delete
def delete(): pass
# serves "DELETE /" requests.

@rs.method('HEAD')
def head(): pass
# serves "HEAD /" requests.

If py-rs will not find Target with suitable Method for HTTP Request 405 Method Not Allowed will be returned.

Consumer

Consumer specifies a media type which Target should consume and an optional HTTP Request Entity processor.

rs.consumes(media, consumer=None) api decorator is used to define Consumer.

REQUIRED parameter media defines expected HTTP Request Content-Type header value.

OPTIONAL parameter consumer specifies custom processor which has to be apply to the request's content before passing into Target:

@rs.post
@rs.consumes('application/json', json.loads)
def foo(entity=rs.context.entity): pass
# foo function will accept "POST /" request only if its Content-Type header matches consume media value -
# 'application/json'. The value of entity parameter will be set to json.loads(request.entity) result.

If the target does not define Consumer than it is assumed that Target consume any Content-Type.

If no suitable Consumer found for Target the 415 Unsupported Media Type status code will be returned to HTTP Client.

Consumer has following requirements for consumer parameter:

  • It should be a callable that accepts single explicit parameter.
  • ValueError should be raised if consumer is unable to process an entity.
  • If ValueError will be raised the 400 Bad Request status code wile be returned to HTTP Client.
  • If other Exception types will be raised in consumer the framework will respond with 500 Internal Server Error immediately.

Producer

Producer specifies a media type which Target should produce and an optional HTTP Response Entity processor.

rs.produces(media, producer=None) api decorator is used to define Producer.

REQUIRED parameter media defines HTTP Response Content-Type header value.

OPTIONAL parameter producer specifies custom processor which has to be apply to Target return entity before writing into HTTP Response:

@rs.get
@rs.produces('application/json', json.dumps)
def foo():
  return {'id':1, 'name':'Entity', 'zip':None}
# foo function will respond to "GET /" request with json.dumps({'id':1, 'name':'Entity', 'zip':None}) result
# as HTTP Response Entity.

If the target does not define Producer but returns not None entity, the application\octet-stream mime-type will be used as default value for HTTP Response Content-Type header.

Producer media value is being matched to media types in the request's Accept header during HTTP Request dispatching; if no suitable media will be found for Accept header the 406 Not Acceptable status code will be returned to HTTP Client.

Producer has following requirements for producer parameter:

  • It should be a callable that accepts single explicit parameter.
  • If any Exception types will be raised in producer the framework will respond with 500 Internal Server Error rather than response returned by Target.

Application

WSGI Application that serves HTTP Request and provides HTTP Response to HTTP Client of REST service.

py-rs provides pep-0333 compatible Application.

rs.application(resources=None) is called to create Application instance.

By default all Targets decorated with py-rs api will be included in rs.application() instance as HTTP Resources.

To specify exact Targets to serve by Application pass them in resources parameter value, note if targets are specified under the Type, the class should be passed as a resource instead of class methods:

import rs

@rs.get
def foo(): pass

@rs.path('/bar')
class Bar:

  @rs.get
  def bar(self): pass

  @rs.post
  def baz(self): pass

application = rs.application()
# create an application with foo, bar, baz targets.

application = rs.application([Bar])
# create an application with bar and baz targets of Bar type.

It is recommended to use an external WSGI Server to run Application in production environment.

However for testing purposes Application can be launched by run method:

application.run('localhost', 8001)
# host an application at http://localhost:8001; wsgiref.simple_server.WSGIServer will serve an application.

Acknowledgements

Inspired by JAX-RS.