Pico

Fergal Walsh

Hipo

github.com/fergalwalsh/pico.git@dev

Pico

Python HTTP APIs for Humans™

Fergal Walsh

Hipo

A minimalistic opinionated framework for writing HTTP APIs

A minimalistic opinionated framework for writing HTTP APIs

GET http://example.com/api/hello?who="world"

POST http://example.com/api/hello who="world"

POST http://example.com/api/hello {"who": "world"}


response:
{"message": "hello world!"}

A minimalistic opinionated framework for writing HTTP APIs

Features not included:

  • Customisable URL Routing
  • Templates
  • Models
  • Admin
  • Class based views
  • ...

A minimalistic opinionated framework for writing HTTP APIs

  • Automatic routing
  • Decorators
  • Automatic JSON serialisation
  • Automatic argument passing
  • Simple streaming responses
  • Development server
  • + Python Client
  • + Javascript Client

A minimalistic opinionated framework for writing HTTP APIs

  • Only supports JSON output
  • Does not support RESTful apis
  • Doesn't care about HTTP methods
  • Simple instead of Featureful
GET http://example.com/api/hello?who="world"
{"message": "hello world!"}
 
GET http://example.com/api/hello?who="world"
{"message": "hello world!"}
from flask import Flask
from flask import jsonify
from flask import request
app = Flask(__name__)

@app.route('/hello')
 def hello():
    who = request.args['who']
    s = "hello %s!" % who
    return jsonify(message=s)
GET http://example.com/api/hello?who="world"
{"message": "hello world!"}
from django.http import JsonResponse


def hello(request):
    who = request.GET['who']
    s = "hello %s!" % who
    return JsonResponse({'message': s})



from . import views

urlpatterns = [
    url(r'^api/hello$', views.hello),
]
GET http://example.com/api/hello?who="world"
{"message": "hello world!"}
import tornado.ioloop
import tornado.web

class HelloHandler(tornado.web.RequestHandler):
    def get(self):
        who = self.get_argument('who')
        s = "hello %s!" % who
        self.write(json.dumps(dict(message=s)))

def make_app():
    return tornado.web.Application([
        (r"/api/hello", HelloHandler),
    ])
GET http://example.com/api/hello?who="world"
{"message": "hello world!"}
 def hello(who):
    s = "hello %s!" % who
    return dict(message=s)
GET http://example.com/api/hello?who="world"
{"message": "hello world!"}
# api.py
from pico import PicoApp

app = PicoApp()


@app.expose()
def hello(who):
    s = "hello %s!" % who
    return dict(message=s)
python -m pico.server api
GET http://example.com/api/hello?who="world"&user="fergal"
{"message": "hello world from fergal!"}
from pico import PicoApp

app = PicoApp()


@app.expose()
def hello(who, user):
    s = "hello %s from %s!" % (who, user)
    return dict(message=s)
GET http://fergal:1234@example.com/api/hello?who="world"
{"message": "hello world from fergal!"}
from pico import PicoApp
from pico.decorators import request_arg
from werkzeug.exceptions import Unauthorized

app = PicoApp()


def current_user(request):
    auth = request.authorization
    if not check_password(auth.username, auth.password):
        raise Unauthorized("Incorrect username or password!")
    return auth.username


@app.expose()
@request_arg(user=current_user)
def hello(who, user):
    s = "hello %s from %s!" % (who, user)
    return dict(message=s)


POST http://fergal:1234@example.com/api/delete id=1
from pico import PicoApp
from pico.decorators import require_method
from myapp import user_is_admin

app = PicoApp()


@app.expose()
@require_method('POST')
@user_is_admin()
def delete(id):
    return posts.delete(id)
        
POST http://example.com/api/upload photo@me.jpg
from pico import PicoApp
from pico.decorators import require_method, request_arg

app = PicoApp()


@app.expose()
@require_method('POST')
@request_arg(username=current_user)
def upload(photo, username):
    with open('uploads/%s_profile.jpg') as f:
        f.write(photo.read())

        
GET http://example.com/api/profile?user="fergal"
from pico import PicoApp
from datetime import date

app = PicoApp()


users = {
    'fergal': User(username="fergal", dob=date(1986, 6, 28), password=md5("1234"))
}


@app.expose()
def profile(user):
    return users.get(user)
        
{
    "username": "fergal",
    "dob": "1986-06-28",
    "age": 29,
}

class User(object):
    def __init__(self, name, dob, password):
        self.name = name
        self.dob = dob
        self.password = password

    @property
    def age(self):
        return (date.today() - self.dob).days / 365

    def as_json(self):
        return {
            'name': self.name,
            'dob': self.dob,
            'age': self.age,
        }
{
    "username": "fergal",
    "dob": "1986-06-28",
    "age": 29,
}
github.com/fergalwalsh/pico.git@dev
github.com/fergalwalsh/pico.git@1.4.2
import pico

def hello(name="World"):
    return "Hello " + name

Literally add one line of code (import pico) to your Python module to turn it into a web service

readme

Why do I import pico but not use it?

How do I know which functions are public?

set(​pico_users) - [me]
F401'pico' imported but unused

flake8

github.com/fergalwalsh/pico.git@1.4.2
import pico

def hello(name="World"):
    request = pico.get_request()
    user = current_user(request)
    return "Hello %s from %s " % (name, user)
@app.expose()
@request_arg(user=current_user)
def hello(who, user):
    return "Hello %s from %s " % (name, user)
github.com/fergalwalsh/pico.git@dev
import api

api.hello("world", "fergal")
github.com/fergalwalsh/pico.git@1.4.2
        try:
            if '/pico/' in path:
                path = path.replace('/pico/', '/')
                try:
                    response = handle_api_v1(path, params, environ)
                except APIError:
                    try:
                        response = handle_pico_js(path, params)
                    except APIError:
                        try:
                            response = handle_api_v2(path, params, environ)
                        except APIError:
                            response = not_found_error(path)
            elif enable_static:
                try:
                    response = static_file_handler(path)
                except OSError, e:
                    response = not_found_error(path)
            else:
                response = not_found_error(path)
github.com/fergalwalsh/pico.git@1.4.2
def extract_params(environ):
    params = {}
    # if parameters are in the URL, we extract them first
    get_params = environ['QUERY_STRING']
    if get_params == '' and '/call/' in environ['PATH_INFO']:
        path = environ['PATH_INFO'].split('/')
        environ['PATH_INFO'] = '/'.join(path[:-1]) + '/'
        params.update(cgi.parse_qs(path[-1]))

    # now get GET and POST data
    fields = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ)
    for name in fields:
        if fields[name].filename:
            upload = fields[name]
            params[name] = upload.file
        elif type(fields[name]) == list and fields[name][0].file:
            params[name] = [v.file for v in fields[name]]
        else:
            params[name] = fields[name].value
    return params
github.com/fergalwalsh/pico.git@1.4.2
class Response(object):
    def __init__(self, **kwds):
        self.status = '200 OK'
        self._headers = {}
        self.content = ''
        self._type = "object"
        self.cacheable = False
        self.callback = None
        self.json_dumpers = {}
        self.__dict__.update(kwds)

    def __getattribute__(self, a):
        try:
            return object.__getattribute__(self, a)
        except AttributeError:
            return None

    def set_header(self, key, value):
        self._headers[key] = value

    @property
    def headers(self):
        headers = dict(self._headers)
        headers['Access-Control-Allow-Origin'] = '*'
        headers['Access-Control-Allow-Headers'] = 'Content-Type'
        headers['Access-Control-Expose-Headers'] = 'Transfer-Encoding'
        if self.cacheable:
            headers['Cache-Control'] = 'public, max-age=22222222'
        if self.type == 'stream':
            headers['Content-Type'] = 'text/event-stream'
        elif self.type == 'object':
            if self.callback:
                headers['Content-Type'] = 'application/javascript'
            else:
github.com/fergalwalsh/pico.git@1.4.2
class NotAuthorizedError(PicoError):
    def __init__(self, message=''):
        PicoError.__init__(self, message)
        self.response.status = "401 Not Authorized"
        self.response.set_header("WWW-Authenticate",  "Basic")


class InvalidSessionError(PicoError):
    def __init__(self, message=''):
        PicoError.__init__(self, message)
        self.response.status = "440 Invalid Session"
github.com/fergalwalsh/pico.git@dev
from werkzeug.wrappers import Request, Response
from werkzeug.exceptions import *

from werkzeug.serving import run_simple
from werkzeug.wsgi import SharedDataMiddleware
http://werkzeug.pocoo.org/

The Python WSGI Utility Library

client.js
<!DOCTYPE HTML>
<html>
<head>
  <title>Pico Example</title>
    <script src="client.js"></script>
    <script>
        pico.load("example");
    </script>
</head>
<body>
  <p id="message"></p>
  <script>
  example.hello("World", function(message){
    $("#message").innerHTML = message;  
  });
  </script>
</body>
</html>
// app.js

  api.event_stream("fergal", function(message){
    $("#message").innerHTML += message + "<br/>";  
  });
# api.py
from pico.decorators import stream

app = PicoApp()


@app.expose()
@stream()
def event_stream(user):
    pubsub = redis.pubsub()
    pubsub.subscribe([user, 'public'])
    while True:
        message = pubsub.get_message()
        if message and message['type'] == 'message':
            yield message['data']

<EventSource/> aka Server-Sent Events

import pico.client


api = pico.client.load('http://localhost:5000/api/')


s = api.hello("world")
print(s)

for message in api.event_stream('fergal'):
    print(message)
client.py
git clone github.com/fergalwalsh/pico.git@dev
Made with Slides.com