servant-py
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