• Stars
    star
    126
  • Rank 284,543 (Top 6 %)
  • Language
    Python
  • License
    MIT License
  • Created over 4 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

logo

Build Status Coverage Status Supports Python versions 3.7+

instant_api

Instantly create an HTTP API with automatic type conversions, JSON RPC, and a Swagger UI. All the boring stuff is done for you, so you can focus on the interesting logic while having an awesome API. Just add methods!

Installation

pip install instant-api

Or to also install the corresponding Python client:

pip install 'instant-api[client]'

Basic usage

Just write some Python functions or methods and decorate them. Parameters and the return value need type annotations so that they can be converted to and from JSON for you. You can use dataclasses for complex values.

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

Talking to the API with instant_client

If you need a Python client, I highly recommend the companion library instant_client. It handles data conversion on the client side and works well with developer tools. 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.

Using method paths instead of JSON-RPC

The API is automatically available in two flavours, and clients can choose which way they prefer to communicate:

  1. The central JSON-RPC endpoint, which follows the JSON-RPC protocol spec exactly, and is easiest to use with standard client libraries.
  2. Method paths, which make it slightly easier for humans to write requests manually (especially in the Swagger GUI) and use the features of HTTP more.

To make a request to a method path, include the method name at the end of the URL, and just send the parameters object in the JSON body. Here's what such a call looks like:

import requests

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

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

The response will be a complete JSON-RPC response as if you had made a full JSON-RPC request. In particular it will either have a result or an error key.

HTTP status codes

The central JSON-RPC endpoint will always (unless a request is not authenticated, see below) return the code HTTP status code 200 (OK), even if there's an error, as standard clients expect that.

Since the method paths are not quite JSON-RPC, they may return a different code in case of errors. In particular an invalid request will lead to a 400 and an unhandled error inside a method will cause a 500.

If you raise an InstantError inside a method, you can give it an http_code, e.g. raise InstantError(..., http_code=404). This will become the HTTP status code only if the method was called by the method path, not the JSON-RPC endpoint.

Global API configuration

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. There will also be a path for each method based on the function name, e.g. /api/scale and /api/translate - see Using method paths instead of JSON-RPC. Specify a different string to change all of these paths.
  • swagger_kwargs is a dictionary (default empty) of keyword arguments to pass to the flasgger.Swagger constructor that is called with the app. For example, you can customise the Swagger UI by passing a dictionary to config:
api = InstantAPI(app, swagger_kwargs={"config": {"specs_route": "/my_apidocs/", ...}})

Handling errors

When the server encounters an error, the response will contain an error key (instead of a result) with an object containing code, data, and message. For example, 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"
}

You can find more details, including the standard error codes for some typical errors, in the JSON-RPC protocol spec.

To return your own custom error information, raise an InstantError in your method, e.g:

from instant_api import InstantAPI, InstantError

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

The response will then be:

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

The HTTP status code depends on which flavour of the API you use - see this section.

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 instance can be used as a decorator 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(...).

Customising method paths in the Swagger UI

Setting attributes directly

For each method, a flasgger.SwaggerView will be created. You can customise the view by passing a dictionary of class attributes in the argument swagger_view_attrs of the decorator. For example:

@api(swagger_view_attrs={"tags": ["Stuff"]})
def foo(...)

This will put foo in the Stuff section of the Swagger UI.

Note that the below is invalid syntax before Python 3.9:

@InstantAPI(app)(swagger_view_attrs={"tags": ["Stuff"]})
def foo(...)

Setting summary and description via the docstring

If a method has a docstring, its first line will be the summary in the OpenAPI spec of the method path, visible in the overview in the Swagger UI. The remaining lines will become the description, visible when the path is expanded in the UI.

Customising global request and method handling

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

  • handle_request(self, method) is the entrypoint which converts a raw flask request to a response. If method is None, the request was made to the generic JSON-RPC path. Otherwise method is a string with the method name at the end of the request path.
  • 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 (based on the global Flask request object), 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.

Dependencies

  • datafunctions (which in turn uses marshmallow) is used by both instant_api and instant_client to transparently handle conversion between JSON and Python classes on both ends.
  • Flasgger provides the Swagger UI.
  • json-rpc handles the protocol.

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.

Why use this library?

This library takes obvious inspiration from FastAPI. So why did I write this, and why might you want to use it?

  • It's really great with instant_client, which lets you feel like you're calling methods locally (and your IDE helps you as if you are) even though they're executed remotely.

  • It's easier to set up, as you don't have to specify paths or HTTP methods. If you group everything into a class, you just have to decorate the whole thing once. It's almost the minimum amount of boilerplate possible.

  • JSON-RPC is pretty cool.

    • It's a popular, standard protocol that has client libraries written in many languages.
    • It lets you do bulk requests: send an array of requests, get an array of responses back.
    • It supports notifications for when you don't care about the result.
  • It's great when you want to work with Flask (e.g. to use other Flask libraries), or more generally if you want a WSGI application without having to embed it inside FastAPI.

    When my use case for this popped up, I considered FastAPI, but being able to use Flask (specifically Plotly Dash) was a hard requirement. The API was only a small part of a larger project, so I didn't want FastAPI to be 'in charge'.

    I tried looking through the source code of FastAPI to extract the bits I needed, like generating the Swagger spec from type annotations, but the code is very complicated and this wasn't worth it. So I wrote my own version where the dependencies do the hard work like that in a nice modular manner. What's left is a small, readable library that largely just wires other stuff together. This way if someone else is in the same situation as me where they have slightly different needs, it's now feasible for them to adapt the source code.

More Repositories

1

heartrate

Simple real time visualisation of the execution of a Python program.
Python
1,624
star
2

birdseye

Graphical Python debugger which lets you easily view the values of all evaluated expressions
JavaScript
1,570
star
3

futurecoder

100% free and interactive Python course for beginners
Python
1,292
star
4

snoop

A powerful set of Python debugging tools, based on PySnooper
Python
927
star
5

sorcery

Dark magic delights in Python
Python
353
star
6

executing

Get information about what a Python frame is currently doing, particularly the AST node being executed
Python
324
star
7

s3-stream-upload

Manages streaming of data to AWS S3 without knowing the size beforehand and without keeping it all in memory or writing to disk.
Java
200
star
8

funcfinder

A tool for automatically solving problems of the form "I need a python function that does X."
Python
165
star
9

stack_data

Python
32
star
10

birdseye-pycharm

IntelliJ IDE plugin for the Python debugger birdseye
Java
31
star
11

pure_eval

Safely evaluate AST nodes without side effects
Python
26
star
12

outdated

Check if a version of a PyPI package is outdated
Python
22
star
13

cheap_repr

Better version of repr/reprlib for short, cheap string representations in Python
Python
21
star
14

friendly_states

Declarative, explicit, tool-friendly finite state machines in Python
Python
19
star
15

nameof

Python function to get the name of a variable or attribute, as in C#
Python
13
star
16

boxes

A library that adds object oriented power to fields, letting you do better than traditional getters and setters.
Java
12
star
17

sunhours

Sketchup plugin for analysing the amount of sunlight hitting points on a surface over the year:
HTML
10
star
18

pyodide-worker-runner

TypeScript
9
star
19

instant_client

Type safe JSON RPC client with automatic (de)serialization. Best paired with instant_api.
Python
7
star
20

jsonfinder

Python library to easily handle JSON contained within strings.
Python
7
star
21

oeis-explorer

Explore related sequences in the OEIS
Python
6
star
22

sync-message

TypeScript
5
star
23

comsync

TypeScript
4
star
24

python_runner

Helper for running python code indirectly
Python
4
star
25

dryenv

Simple DRY configuration with environment variables and pydantic
Python
4
star
26

littleutils

Small personal collection of python utility functions, partly just for fun.
Python
3
star
27

askso

AskSO - StackOverflow Python Question Assistant
Python
2
star
28

datafunctions

Automatic (de)serialization of arguments and return values for Python functions
Python
2
star
29

quiggles

Android app for drawing symmetrical patterns
Kotlin
2
star
30

alexmojaki

2
star
31

dependent_types

Python
1
star
32

case-classes

A framework to refactor computing a result from an aggregate object
Java
1
star
33

trace_augmentation

Python
1
star