Skip to main content

Instantly create an HTTP API with automatic type conversions, JSON RPC, and a Swagger UI. Just add methods!

Project description

instant_api

Build Status Coverage Status Supports Python versions 3.7+

Instantly create an HTTP API with automatic type conversions, JSON RPC, and a Swagger UI. Just add methods!

pip install instant-api

Or to also install the corresponding Python client:

pip install 'instant-api[client]'

Basic usage looks like this:

from dataclasses import dataclass
from flask import Flask
from instant_api import InstantAPI

app = Flask(__name__)

@dataclass
class Point:
    x: int
    y: int

@InstantAPI(app)
class Methods:
    def translate(self, p: Point, dx: int, dy: int) -> Point:
        """Move a point by dx and dy."""
        return Point(p.x + dx, p.y + dy)

    def scale(self, p: Point, factor: int) -> Point:
        """Scale a point away from the origin by factor."""
        return Point(p.x * factor, p.y * factor)

if __name__ == '__main__':
    app.run()

Visit http://127.0.0.1:5000/apidocs/ for a complete Swagger GUI to try out the API interactively:

Swagger overview

The API implements the standard JSON-RPC protocol, making it easy to use libraries in existing languages to communicate with minimal boilerplate.

If you need a Python client, I highly recommend the companion library instant_client. Basic usage looks like:

from server import Methods, Point  # the classes we defined above
from instant_client import InstantClient

# The type hint is a lie, but your linter/IDE doesn't know that!
methods: Methods = InstantClient("http://127.0.0.1:5000/api/", Methods()).methods

assert methods.scale(Point(1, 2), factor=3) == Point(3, 6)

That looks a lot like it just called Methods.scale() directly, which is the point (no pun intended), but under the hood it did in fact send an HTTP request to the server.

If a library doesn't suit your needs, or if you're wondering what the protocol looks like, it's very simple. Here's a the same call done 'manually':

import requests

response = requests.post(
    'http://127.0.0.1:5000/api/',
    json={
        'id': 0, 
        'jsonrpc': '2.0', 
        'method': 'scale', 
        'params': {
            'p': {'x': 1, 'y': 2}, 
            'factor': 3,
        },
    },
)

assert response.json()['result'] == {'x': 3, 'y': 6}

instant_api and instant_client use datafunctions under the hood (which in turn uses marshmallow) to transparently handle conversion between JSON and Python classes on both ends. All this means you can focus on writing 'normal' Python and worry less about the communication details. The Swagger UI is provided by Flasgger, and the protocol is handled by the json-rpc library.

Because other libraries do so much of the work, instant_api itself is a very small library, essentially contained in one little file. You can probably read the source code pretty easily and adapt it to your needs.

Configuration and other details

Class parameters

The InstantAPI class requires a Flask app and has the following optional keyword-only parameters:

  • path is a string (default '/api/') which is the endpoint that will be added to the app for the JSON RPC. This is where requests will be POSTed. There will also be a path for each method based on the function name, e.g. /api/scale and /api/translate, but these all behave identically (in particular the body must still specify a "method" key) and are only there to make the Swagger UI usable.
  • swagger_kwargs is a dictionary (default empty) of keyword arguments to pass to the flasgger.Swagger constructor that is called with the app.

Errors

The server will always (unless a request is not authenticated, see below) respond with HTTP code 200, even if there is an error in the RPC call. Instead the response body will contain an error code and other details. Complete information can be found in the protocol spec and the json-rpc library docs. Below are the essentials.

If a method is given invalid parameters, the details of the error (either a TypeError or a marshmallow ValidationError) will be included in the response. The error code will be -32602. The response JSON looks like this:

{
  "error": {
    "code": -32602,
    "data": {
      "p": {
        "y": [
          "Not a valid integer."
        ]
      }
    },
    "message": "marshmallow.exceptions.ValidationError: {'p': {'y': ['Not a valid integer.']}}"
  },
  "id": 0,
  "jsonrpc": "2.0"
}

If there's an error inside the method, the exception type and message will be in the response, e.g:

{
  "error": {
    "code": -32000,
    "data": {
      "args": [
        "division by zero"
      ],
      "message": "division by zero",
      "type": "ZeroDivisionError"
    },
    "message": "Server error"
  },
  "id": 0,
  "jsonrpc": "2.0"
}

If you'd like to control the error response directly, raise a JSONRPCDispatchException in your method, e.g:

from jsonrpc.exceptions import JSONRPCDispatchException
from instant_api import InstantAPI

@InstantAPI(app)
class Methods:
    def find_thing(self, thing_id: int) -> Thing:
        ...
        raise JSONRPCDispatchException(
            code=40404,
            message="Thing not found anywhere at all",
            data=["not here", "or here"],
        )

The response will then be:

{
  "error": {
    "code": 40404,
    "data": [
      "not here",
      "or here"
    ],
    "message": "Thing not found anywhere at all"
  },
  "id": 0,
  "jsonrpc": "2.0"
}

Attaching methods

Instances of InstantAPI can be called with functions, classes, or arbitrary objects to add methods to the API. For functions and classes, the InstantAPI can be used as a decorators to call it.

Decorating a single function adds it as an API method, as you'd expect. The function itself should not be a method of a class, since there is no way to provide the first argument self.

Calling InstantAPI with an object will search through all its attributes and add to the API all functions (including bound methods) whose name doesn't start with an underscore (_).

Decorating a class will construct an instance of the class without arguments and then call the resulting object as described above. This means it will add bound methods, so the self argument is ignored.

So given api = InstantAPI(app), all of these are equivalent:

@api
def foo(bar: Bar) -> Spam:
    ...

api(foo)

@api
class Methods:
    def foo(self, bar: Bar) -> Spam:
        ...

api(Methods)

api(Methods())

If a function is missing a type annotation for any of its parameters or for the return value, an exception will be raised. If you don't want a method to be added to the API, prefix its name with an underscore, e.g. def _foo(...).

If a function has a docstring, it's first line will be shown in the Swagger UI.

Intercepting requests

To directly control how requests are handled, create a subclass of InstantAPI and override one of these methods:

  • handle_request(self) is the entrypoint which converts a raw flask request to a response.
  • call_method(self, func, *args, **kwargs) calls the API method func with the given arguments. The arguments here are not yet deserialized according to the function type annotations.

Unless you're doing something very weird, remember to call the parent method with super() somewhere.

Authentication

To require authentication for requests:

  1. Create a subclass of InstantAPI.
  2. Override the method def is_authenticated(self):.
  3. Return a boolean: True if a user should have access, False if they should be denied.
  4. Use an instance of your subclass to decorate methods.

Unauthenticated requests will receive a 403 response with a non-JSON body.

Project details


Download files

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

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

instant_api-0.0.2-py3-none-any.whl (9.5 kB view details)

Uploaded Python 3

File details

Details for the file instant_api-0.0.2-py3-none-any.whl.

File metadata

  • Download URL: instant_api-0.0.2-py3-none-any.whl
  • Upload date:
  • Size: 9.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.12.1 pkginfo/1.4.2 requests/2.11.1 setuptools/36.2.7 requests-toolbelt/0.8.0 tqdm/4.28.1 CPython/3.5.1

File hashes

Hashes for instant_api-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 e90355d0a697856d25342012df1ab6191535f7bc1d42549e04b6b24dbba73a67
MD5 c1977ccdda71d7caa9e1cf2efcf6f810
BLAKE2b-256 3d1bd7fbae137eb3119688ada240d49abcaa6011d2ac535220e6e63ad44c0fc3

See more details on using hashes here.

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