Instantly create an HTTP API with automatic type conversions, JSON RPC, and a Swagger UI. Just add methods!
Project description
instant_api
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:
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 theflasgger.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 methodfunc
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:
- Create a subclass of
InstantAPI
. - Override the method
def is_authenticated(self):
. - Return a boolean:
True
if a user should have access,False
if they should be denied. - 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
Built Distribution
Hashes for instant_api-0.0.2-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | e90355d0a697856d25342012df1ab6191535f7bc1d42549e04b6b24dbba73a67 |
|
MD5 | c1977ccdda71d7caa9e1cf2efcf6f810 |
|
BLAKE2b-256 | 3d1bd7fbae137eb3119688ada240d49abcaa6011d2ac535220e6e63ad44c0fc3 |