Skip to content

erewok/servant-py

Repository files navigation

servant-py

Build Status

This library lets you derive automatically Python functions that let you query each endpoint of a servant webservice.

Currently, the only supported method for generating requests is via the requests library, which is the recommended way to generate HTTP requests in the Python world (even among Python core devs).

Inspiration

This library is largely inspired by servant-js and by the fantastic work of the Servant team in general. Any good ideas you find in here are from their work (any mistakes are almost entirely mine, however).

Example

There are two different styles of function-return supported here: DangerMode and RawResponse.

The latter returns the raw response from issuing the request and the former calls raise_for_status and then attempts to return resp.json(). You can switch which style you'd like to use by creating a proper CommonGeneratorOptions object.

The default options just chucks it all to the wind and goes for DangerMode (because, seriously, we're using Haskell to generate Python here...).

Following is an example of using the Servant DSL to describe endpoints and then using servant-py to create Python clients for those endpoints.

Servant DSL API Description

{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeOperators              #-}

module Main where

import           Data.Aeson
import qualified Data.ByteString.Char8 as B
import           Data.Proxy
import qualified Data.Text             as T
import           GHC.Generics
import           Servant
import           System.FilePath

import           Servant.PY

-- * A simple Counter data type
newtype Counter = Counter { value :: Int }
  deriving (Generic, Show, Num)
instance ToJSON Counter

data LoginForm = LoginForm
 { username :: !T.Text
 , password :: !T.Text
 , otherMissing :: Maybe T.Text
 } deriving (Eq, Show, Generic)
instance ToJSON LoginForm

-- * Our API type
type TestApi = "counter-req-header" :> Post '[JSON] Counter
          :<|> "counter-queryparam"
            :> QueryParam "sortby" T.Text
            :> Header "Some-Header" T.Text :> Get '[JSON] Counter
          :<|> "login-queryflag" :> QueryFlag "published" :> Get '[JSON] LoginForm
          :<|> "login-params-authors-with-reqBody"
            :> QueryParams "authors" T.Text
            :> ReqBody '[JSON] LoginForm :> Post '[JSON] LoginForm
          :<|> "login-with-path-var-and-header"
            :> Capture "id" Int
            :> Capture "Name" T.Text
            :> Capture "hungrig" Bool
            :> ReqBody '[JSON] LoginForm
            :> Post '[JSON] (Headers '[Header "test-head" B.ByteString] LoginForm)

testApi :: Proxy TestApi
testApi = Proxy

-- where our static files reside
result :: FilePath
result = "examples"

main :: IO ()
main = writePythonForAPI testApi requests (result </> "api.py")

Generated Python Code

If you build the above and run it, you will get some output that looks like the following:

from urllib import parse

import requests

def post_counterreqheader():
    """
    POST "counter-req-header"

    """
    url = "http://localhost:8000/counter-req-header"

    resp = requests.post(url)
    resp.raise_for_status()
    return resp.json()


def get_counterqueryparam(sortby, headerSomeHeader):
    """
    GET "counter-queryparam"

    """
    url = "http://localhost:8000/counter-queryparam"

    headers = {"Some-Header": headerSomeHeader}
    params = {"sortby": sortby}
    resp = requests.get(url,
                        headers=headers,
                        params=params)

    resp.raise_for_status()
    return resp.json()


def get_loginqueryflag(published):
    """
    GET "login-queryflag"

    """
    url = "http://localhost:8000/login-queryflag"

    params = {"published": published}
    resp = requests.get(url,
                        params=params)

    resp.raise_for_status()
    return resp.json()


def post_loginparamsauthorswithreqBody(authors, data):
    """
    POST "login-params-authors-with-reqBody"

    """
    url = "http://localhost:8000/login-params-authors-with-reqBody"

    params = {"authors": authors}
    resp = requests.post(url,
                         params=params,
                         json=data)

    resp.raise_for_status()
    return resp.json()


def post_loginwithpathvarandheader_by_id_by_Name_by_hungrig(id, Name, hungrig, data):
    """
    POST "login-with-path-var-and-header/{id}/{Name}/{hungrig}"
    Args:
        id
        Name
        hungrig
    """
    url = "http://localhost:8000/login-with-path-var-and-header/{id}/{Name}/{hungrig}".format(
        id=parse.quote(id),
        Name=parse.quote(Name),
        hungrig=parse.quote(hungrig))

    resp = requests.post(url,
                         json=data)

    resp.raise_for_status()
    return resp.json()

If you would like to compile and run this example yourself, you can do that like so:

$ stack build --flag servant-py:example
$ stack exec servant-py-exe
$ cat examples/api.py