twfy-api-client (empty) → 0.1.0.0
raw patch · 12 files changed
+591/−0 lines, 12 filesdep +aesondep +aeson-compatdep +basesetup-changed
Dependencies added: aeson, aeson-compat, base, base-compat, bytestring, either, exceptions, http-client, http-client-tls, http-media, mtl, servant, servant-client, servant-server, text, transformers, twfy-api-client
Files
- LICENSE +30/−0
- README.md +7/−0
- Setup.hs +2/−0
- app/Main.hs +42/−0
- src/Twfy/Api.hs +114/−0
- src/Twfy/Client.hs +102/−0
- src/Twfy/Data/Constituency.hs +50/−0
- src/Twfy/Data/JsonIso8859.hs +38/−0
- src/Twfy/Data/MP.hs +108/−0
- src/Twfy/Util/Json.hs +26/−0
- test/Spec.hs +2/−0
- twfy-api-client.cabal +70/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Nigel Rantor (c) 2016++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Author name here nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,7 @@+# twfy-api-client++They Work For You REST API Client Bindings++## Status++Under development
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Servant.Client (ServantError)+import System.Environment (lookupEnv)+import qualified Data.Text as T++import Twfy.Client++-- TODO: add applicative opt parsing for urls, api key etc and commands for different API calls++-- TODO: take from command line or env+readApiKey :: IO (Maybe ApiKey)+readApiKey = do+ envKey <- lookupEnv "TWFY_API_KEY"+ let textKey = fmap T.pack envKey+ return textKey++displayResult :: Show a => (Either ServantError a) -> IO ()+displayResult result = do+ case result of+ Left err -> putStrLn $ "Error: " ++ show err+ Right something -> do+ print something++run :: ApiKey -> IO ()+run apiKey = do+ c <- client apiKey Nothing Nothing+ putStrLn $ show c+ getConstituencies c >>= displayResult+ getConstituency c Nothing (Just "TW8 0QU") >>= displayResult+ getMP c Nothing (Just "Manchester, Gorton") Nothing (Just False) >>= displayResult+ getMPs c Nothing (Just "Labour") Nothing >>= displayResult++main :: IO ()+main = do+ apiKey <- readApiKey+ maybe (putStrLn "Can't run without API KEY") run apiKey
+ src/Twfy/Api.hs view
@@ -0,0 +1,114 @@+{-|+Module : Twfy.Api+Description : Client API definition++Servant client API for <https://www.theyworkforyou.com/ They Work For You>++Different calls are being implemented as needed for now so if you require any of the API calls that are not yet implemented please either submit a pull request via github or let me know you want it and I will add it myself.++TODO: getPerson, getMPInfo, getMPsInfo, getLord, getLords, getMLA, getMLAs, getMSP, getMSPs, getGeometry, getBoundary, getCommittee, getDebates, getWrans, getWMS, getHansard, getComments+-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Twfy.Api+ (+ TwfyAPI+ , ApiKey+ , getConstituency+ , getConstituencies+ , getMP+ , getMPs+ )+ where++import Data.Proxy+import Servant.API+import Servant.Client+import qualified Data.Text as T++import Twfy.Data.JsonIso8859+import Twfy.Data.Constituency+import Twfy.Data.MP++-- | API Key+type ApiKey = T.Text++-- | Servant API definition+type TwfyAPI = "getConstituency"+ :> QueryParam "key" ApiKey+ :> QueryParam "name" T.Text+ :> QueryParam "postcode" T.Text+ :> Get '[JsonIso8859] Constituency+ :<|>+ "getConstituencies"+ :> QueryParam "key" ApiKey+ :> Get '[JsonIso8859] [Constituency]+ :<|>+ "getMP"+ :> QueryParam "key" ApiKey+ :> QueryParam "id" Int+ :> QueryParam "constituency" T.Text+ :> QueryParam "postcode" T.Text+ :> QueryParam "always_return" Bool+ :> Get '[JsonIso8859] MP+ :<|>+ "getMPs"+ :> QueryParam "key" ApiKey+ :> QueryParam "search" T.Text+ :> QueryParam "party" T.Text+ :> QueryParam "date" T.Text -- TODO: change this to a date+ :> Get '[JsonIso8859] [MP]++twfyAPI :: Proxy TwfyAPI+twfyAPI = Proxy++-- TODO: convertURL++-- | The 'getConstituency' function retreives a constituency based on name or post code+getConstituency :: Maybe ApiKey -- ^ API key+ -> Maybe T.Text -- ^ Name+ -> Maybe T.Text -- ^ Post code+ -> ClientM Constituency++-- | The 'getConstituencies' function retreives all constituencies+getConstituencies :: Maybe ApiKey -- ^ API key+ -> ClientM [Constituency]++-- | The 'getMP' function retreives an MP+getMP :: Maybe ApiKey -- ^ API key+ -> Maybe Int -- ^ Id+ -> Maybe T.Text -- ^ Constituency name+ -> Maybe T.Text -- ^ Post code+ -> Maybe Bool -- ^ Always return+ -> ClientM MP++-- | The 'getMPs' function retreives a list of MPs+getMPs :: Maybe ApiKey -- ^ API key+ -> Maybe T.Text -- ^ Name search+ -> Maybe T.Text -- ^ party name+ -> Maybe T.Text -- ^ Date+ -> ClientM [MP]++-- TODO: getPerson+-- TODO: getMPInfo+-- TODO: getMPsInfo+-- TODO: getLord+-- TODO: getLords+-- TODO: getMLA+-- TODO: getMLAs+-- TODO: getMSP+-- TODO: getMSPs+-- TODO: getGeometry+-- TODO: getBoundary+-- TODO: getCommittee+-- TODO: getDebates+-- TODO: getWrans+-- TODO: getWMS+-- TODO: getHansard+-- TODO: getComments++(getConstituency :<|> getConstituencies :<|> getMP :<|> getMPs) = client twfyAPI
+ src/Twfy/Client.hs view
@@ -0,0 +1,102 @@+{-|+Module : Twfy.Client+Description : Client+-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Twfy.Client+ (+ A.ApiKey+ , Client(..)+ , client+ , getConstituency+ , getConstituencies+ , getMP+ , getMPs+ )+ where++import Servant.Client hiding(Client(..), client)+import Network.HTTP.Client (Manager, newManager)+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Servant.Common.BaseUrl (parseBaseUrl)++import qualified Data.Text as T++import qualified Twfy.Api as A+import Twfy.Data.Constituency+import Twfy.Data.MP++-- | Client - very subject to change, use the 'client' function to create+data Client = Client {+ clientApiKey :: A.ApiKey -- ^ API Key+ , clientApiUri :: String -- ^ API URL+ , clientManager :: Manager -- ^ HTTP client manager+ , clientEnv :: ClientEnv -- ^ Servant client env+ }++instance Show Client where+ show c = let k = show (clientApiKey c)+ u = show (clientApiUri c)+ in "twfy-client: " ++ k ++ " - " ++ u++-- | Create a client.+--+client :: A.ApiKey -- ^ API Key+ -> Maybe String -- ^ API URI - defaults to "https://www.theyworkforyou.com/api"+ -> Maybe Manager -- ^ HTTP Client manager - uses default TLS settings if not provided+ -> IO Client+client k u m = let defaultUri = "https://www.theyworkforyou.com/api"+ defaultManagerAction = newManager tlsManagerSettings+ key = k+ uri = maybe defaultUri id u+ in do+ baseUrl <- parseBaseUrl uri+ manager <- maybe defaultManagerAction return m+ let env = ClientEnv manager baseUrl+ return $ Client key uri manager env++-- | The 'getConstituency' function retreives a constituency based on name or post code+getConstituency :: Client+ -> Maybe T.Text -- ^ Name+ -> Maybe T.Text -- ^ Post code+ -> IO (Either ServantError Constituency)+getConstituency c = do+ let key = Just $ clientApiKey c+ env = clientEnv c+ (\name postCode -> (flip runClientM) env (A.getConstituency key name postCode))++-- | The 'getConstituencies' function retreives all constituencies+getConstituencies :: Client+ -> IO (Either ServantError [Constituency])+getConstituencies c = do+ let key = Just $ clientApiKey c+ env = clientEnv c+ (flip runClientM) env (A.getConstituencies key)++-- | The 'getMP' function retreives an MP+getMP :: Client+ -> Maybe Int -- ^ Id+ -> Maybe T.Text -- ^ Constituency name+ -> Maybe T.Text -- ^ Post code+ -> Maybe Bool -- ^ Always return+ -> IO (Either ServantError MP)+getMP c = do+ let key = Just $ clientApiKey c+ env = clientEnv c+ (\mpId constituencyName postCode alwaysReturn -> (flip runClientM) env (A.getMP key mpId constituencyName postCode alwaysReturn))++-- | The 'getMPs' function retreives a list of MPs+getMPs :: Client+ -> Maybe T.Text -- ^ Name search+ -> Maybe T.Text -- ^ Party name+ -> Maybe T.Text -- ^ Date+ -> IO (Either ServantError [MP])+getMPs c = do+ let key = Just $ clientApiKey c+ env = clientEnv c+ (\personName partyName date -> (flip runClientM) env (A.getMPs key personName partyName date))
+ src/Twfy/Data/Constituency.hs view
@@ -0,0 +1,50 @@+{-|+Module : Twfy.Data.Constituency+Description : Constituency data types++Constituency data types+-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Twfy.Data.Constituency+ (+ Constituency(..)+ )+ where++import GHC.Generics+import Data.Aeson.TH+import Data.Maybe (catMaybes)+import qualified Data.Text as T+import Twfy.Util.Json (recordNameToJsonName)++-- | Constituency Data+data Constituency = Constiutency {+ -- | Constituency name+ consituencyName :: T.Text+ -- | BBC ID for constituency. Arrives as text but appears to be integer+ , constituencyBbcConstituencyId :: Maybe T.Text+ -- | Guardian ID for constituency. Arrives as text but appears to be integer+ , constituencyGuardianId :: Maybe T.Text+ -- | Guardian name for constituency+ , constituencyGuardianName :: Maybe T.Text+ -- | Guardian election results URL+ , constituencyGuardianElectionResults :: Maybe T.Text+ -- | TOOD: find out what this is. Arrives as text but appears to be integer.+ , constituencyPaId :: Maybe T.Text+ }+ deriving (Generic)+$(deriveJSON defaultOptions { fieldLabelModifier = recordNameToJsonName } ''Constituency)++instance Show Constituency where+ -- | Convert a constituency to a String+ show c = let name = Just $ consituencyName c+ bbc = fmap (mappend "bbc_id: ") $ constituencyBbcConstituencyId c+ guardian_id = fmap (mappend "guardian_id: ") $ constituencyGuardianId c+ guardian_name = fmap (mappend "guardian_name: ") $ constituencyGuardianName c+ parts = catMaybes [ name, bbc, guardian_id, guardian_name ]+ in show $ T.intercalate " - " parts
+ src/Twfy/Data/JsonIso8859.hs view
@@ -0,0 +1,38 @@+{-|+Module : Twfy.Data.JsonIso8859+Description : Content-Type not provided by Servant++Instances for Accept and MimeUnrender to allow Servant client to read 'text/javascript; charset=iso-8859-1'+-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+module Twfy.Data.JsonIso8859+ (+ JsonIso8859+ )+ where++import Prelude ()+import Prelude.Compat++import Servant+import Data.Aeson++import qualified Data.ByteString.Lazy as LBS+import qualified Data.Text.Encoding as TE+import qualified Network.HTTP.Media as Media++-- | Type for 'text/plain; charset=iso8859-1'+data JsonIso8859++instance Accept JsonIso8859 where+ contentType _ = "text" Media.// "javascript" Media./: ("charset", "iso-8859-1")++instance FromJSON a => MimeUnrender JsonIso8859 a where+ mimeUnrender _ = eitherDecode . from8859ToUtf8++from8859ToUtf8 :: LBS.ByteString -> LBS.ByteString+from8859ToUtf8 iso = let isoText = TE.decodeLatin1 $ LBS.toStrict iso+ utf8 = TE.encodeUtf8 isoText+ in LBS.fromStrict utf8
+ src/Twfy/Data/MP.hs view
@@ -0,0 +1,108 @@+{-|+Module : Twfy.Data.MP+Description : MP data types++MP data types+-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Twfy.Data.MP+ (+ MP(..)+ )+ where++import GHC.Generics+import Data.Aeson.TH+import qualified Data.Text as T+import Twfy.Util.Json (recordNameToJsonName)++-- | MP Data+--+-- Does not yet include office data+data MP = MP {++ -- | Member ID (TODO:who allocates these?). Arrives as text but appears to be integer+ mpMemberId :: T.Text++ -- | House (TODO: semantics?). Arrives as text but appears to be integer+ , mpHouse :: T.Text++ -- | Constituency name+ , mpConstituency :: T.Text++ -- | Party name. (TODO: what does independant show?)+ , mpParty :: T.Text++ -- | Date entered house. (TODO: convert to date - format appears to be YYYY-MM-DD)+ , mpEnteredHouse :: T.Text++ -- | Date left house. (TODO: convert to date - format appears to be YYYY-MM-DD and 9999-12-31 for Still Here)+ , mpLeftHouse :: T.Text++ -- | Reason for entering house+ , mpEnteredReason :: T.Text++ -- | Reason for leaving house+ , mpLeftReason :: T.Text++ -- | Person ID (TODO:who allocates these?). Arrives as text but appears to be integer+ , mpPersonId :: T.Text++ -- | Last update time stamp. (TODO: convert to time - format appears to be ''YYYY-MM-DD HH:MM:SS')+ , mpLastupdate :: T.Text++ -- | Title+ , mpTitle :: T.Text++ -- | Given name+ , mpGivenName :: T.Text++ -- | Family name+ , mpFamilyName :: T.Text++ -- | Full name+ , mpFullName :: T.Text++ -- | URL (TODO: looks like a URL Path for the TWFY site, should maybe create a URL here somehow)+ , mpUrl :: T.Text++ -- | Image (TODO: looks like a URL Path for the TWFY site, should maybe create a URL here somehow)+ , mpImage :: T.Text++ -- | Image height+ , mpImageHeight :: Int++ -- | Image width+ , mpImageWidth :: Int++ -- TODO: , mpOffice :: [Office]+ }+ deriving (Show,Generic)+$(deriveJSON defaultOptions { fieldLabelModifier = recordNameToJsonName } ''MP)++-- SAMPLE output from API - remaining stuff not included above+-- {+-- "office":[+-- {+-- "moffice_id":"uk.parliament.data/Member/4389/OppositionPost/1177",+-- "dept":"",+-- "position":"Shadow Minister (Housing)",+-- "from_date":"2016-10-10",+-- "to_date":"9999-12-31",+-- "person":"25343","source":""+-- },+-- {+-- "moffice_id":"uk.parliament.data/Member/4389/Committee/328",+-- "dept":"Women and Equalities Committee",+-- "position":"Member",+-- "from_date":"2015-07-06",+-- "to_date":"9999-12-31",+-- "person":"25343","source":""+-- }+-- ]+-- }
+ src/Twfy/Util/Json.hs view
@@ -0,0 +1,26 @@+{-|+Module : Twfy.Util.Json+Description : JSON Utilities++Convert record field names between haskell/JSON versions+-}+module Twfy.Util.Json+ (+ recordNameToJsonName+ ) where++import Data.Char (isUpper, toLower)++-- | Convert a haskell record field name in camelCase to JSON snake_case.+recordNameToJsonName :: String -- ^ Record name+ -> String -- ^ JSON name+recordNameToJsonName xs = let suffix = dropWhile (not . isUpper) xs+ camel = (toLower (head suffix)) : tail suffix+ in camelToSnake camel++camelToSnake :: String -> String+camelToSnake [] = []+camelToSnake xs@(_:[]) = xs+camelToSnake (x:xs) = if isUpper x+ then '_' : ( (toLower x) : (camelToSnake xs) )+ else x : (camelToSnake xs)
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"
+ twfy-api-client.cabal view
@@ -0,0 +1,70 @@+name: twfy-api-client+version: 0.1.0.0+synopsis: They Work For You API Client Library+description: Please see README.md+homepage: https://github.com/wiggly/twfy-api-client#readme+license: BSD3+license-file: LICENSE+author: Nigel Rantor+maintainer: wiggly@wiggly.org+copyright: 2016 Nigel Rantor+category: API+build-type: Simple+extra-source-files: README.md+cabal-version: >=1.10+stability: alpha+bug-reports: https://github.com/wiggly/twfy-api-client/issues++library+ hs-source-dirs: src+ exposed-modules: Twfy.Client Twfy.Api Twfy.Data.Constituency Twfy.Data.MP Twfy.Data.JsonIso8859 Twfy.Util.Json+ build-depends: base >= 4.7 && < 5+ , base-compat+ , mtl+ , either+ , transformers+ , exceptions+ , bytestring+ , text+ , aeson+ , aeson-compat+ , servant >= 0.9.1+ , servant-client >= 0.9.1+ , servant-server >= 0.9.1+ , http-client+ , http-client-tls+ , http-media+ default-language: Haskell2010++executable twfy-api-client+ hs-source-dirs: app+ main-is: Main.hs+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends: base+ , twfy-api-client+ , http-client+ , base-compat+ , mtl+ , either+ , transformers+ , text+ , aeson+ , aeson-compat+ , servant >= 0.9.1+ , servant-client >= 0.9.1+ , http-client+ , http-client-tls+ default-language: Haskell2010++test-suite twfy-api-client-test+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ build-depends: base+ , twfy-api-client+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/wiggly/twfy-api-client