module Data.Quandl (
getTable,
defaultOptions,
getTableWith,
search,
Options(..),
Frequency(..),
Transformation(..),
Dataset(..),
Metadata(..),
SearchSource(..),
SearchDoc(..),
SearchPage(..),
downloadJSON,
createUrl
) where
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Char8 as BC
import qualified Data.Text as T
import qualified Data.HashMap.Strict as H
import Data.Char (toLower, toUpper)
import Data.Time (UTCTime, Day, readTime)
import Data.Monoid ((<>))
import Data.Generics (Data, Typeable)
import Data.List (intercalate)
import Data.Aeson (decode', Value(..), (.:), (.:?), FromJSON(..))
import Data.Aeson.Types (Parser)
import Control.Monad (mzero)
import Control.Applicative ((<$>), (<*>), pure)
import Data.Time.Locale.Compat (defaultTimeLocale)
import Network.HTTP.Conduit (simpleHttp)
import Network.HTTP.Types.URI (encodePathSegments, renderQueryBuilder)
import Blaze.ByteString.Builder (toByteString, fromByteString)
data Options = Options {
opAuthToken :: Maybe String,
opSortAscending :: Bool,
opNumRows :: Maybe Int,
opStartDate :: Maybe Day,
opEndDate :: Maybe Day,
opFrequency :: Maybe Frequency,
opTransformation :: Maybe Transformation,
opMetadataOnly :: Bool
} deriving (Eq, Ord, Show, Data, Typeable)
data Frequency
= Daily
| Weekly
| Monthly
| Quarterly
| Annual
deriving (Eq, Ord, Show, Data, Typeable)
data Transformation
= Diff
| RDiff
| Cumul
| Normalize
deriving (Eq, Ord, Show, Data, Typeable)
data Metadata = Metadata {
meId :: Int,
meSourceCode :: String,
meTableCode :: String,
meSourceName :: T.Text,
meTableName :: T.Text,
meUrlName :: T.Text,
meDescription :: T.Text,
meSourceUrl :: T.Text,
meUpdatedAt :: UTCTime,
mePrivate :: Bool
} deriving (Eq, Ord, Show, Data, Typeable)
data Dataset = Dataset {
daTable :: Maybe Metadata,
daColumnNames :: [T.Text],
daData :: [[T.Text]],
daFromDate :: Day,
daToDate :: Day,
daFrequency :: T.Text
} deriving (Eq, Show, Data, Typeable)
data SearchSource = SearchSource {
ssName :: T.Text,
ssId :: Int,
ssDescription :: T.Text,
ssHost :: T.Text,
ssDatasetsCount :: Int,
ssCode :: T.Text
} deriving (Eq, Show, Data, Typeable)
data SearchDoc = SearchDoc {
sdSourceCode :: T.Text,
sdDisplayUrl :: Maybe T.Text,
sdPrivate :: Bool,
sdUrlizeName :: T.Text,
sdName :: T.Text,
sdFromDate :: Day,
sdDescription :: T.Text,
sdColumnNames :: [T.Text],
sdFrequency :: T.Text,
sdSourceName :: T.Text,
sdUpdatedAt :: UTCTime,
sdToDate :: Day,
sdCode :: T.Text
} deriving (Show, Eq, Data, Typeable)
data SearchPage = SearchPage {
spTotalCount :: Int,
spCurrentPage :: Int,
spPerPage :: Int,
spSources :: [SearchSource],
spDocs :: [SearchDoc]
} deriving (Eq, Show, Data, Typeable)
asRows :: Maybe [[Value]] -> [[T.Text]]
asRows Nothing = [[]]
asRows (Just x) = map (map convert) x
where
convert (Object o) = T.pack $ show o
convert (Array a) = T.pack $ show a
convert (String s) = s
convert (Number n) = T.pack $ show n
convert (Bool b) = T.pack $ show b
convert (Null) = T.empty
asUTCTime :: String -> UTCTime
asUTCTime = readTime defaultTimeLocale "%FT%T%QZ"
asDay :: String -> Day
asDay = readTime defaultTimeLocale "%F"
parseMetadata :: Value -> Parser (Maybe Metadata)
parseMetadata o@(Object obj) = case H.lookup "id" obj of
Nothing -> pure Nothing
Just _ -> parseJSON o
parseMetadata _ = pure Nothing
instance FromJSON Metadata where
parseJSON (Object v) = Metadata <$>
v .: "id" <*>
v .: "source_code" <*>
v .: "code" <*>
v .: "source_name" <*>
v .: "name" <*>
v .: "urlize_name" <*>
v .: "description" <*>
v .: "display_url" <*>
(asUTCTime <$> v .: "updated_at") <*>
v .: "private"
parseJSON _ = mzero
instance FromJSON Dataset where
parseJSON o@(Object v) = Dataset <$>
parseMetadata o <*>
v .: "column_names" <*>
(asRows <$> v .:? "data") <*>
(asDay <$> v .: "from_date") <*>
(asDay <$> v .: "to_date") <*>
v .: "frequency"
parseJSON _ = mzero
instance FromJSON SearchSource where
parseJSON (Object v) = SearchSource <$>
v .: "name" <*>
v .: "id" <*>
v .: "description" <*>
v .: "host" <*>
v .: "datasets_count" <*>
v .: "code"
parseJSON _ = mzero
instance FromJSON SearchDoc where
parseJSON (Object v) = SearchDoc <$>
v .: "source_code" <*>
v .: "display_url" <*>
v .: "private" <*>
v .: "urlize_name" <*>
v .: "name" <*>
(asDay <$> v .:"from_date") <*>
v .: "description" <*>
v .: "column_names" <*>
v .: "frequency" <*>
v .: "source_name" <*>
(asUTCTime <$> v .: "updated_at" ) <*>
(asDay <$> v .: "to_date") <*>
v .: "code"
parseJSON _ = mzero
instance FromJSON SearchPage where
parseJSON (Object v) = SearchPage <$>
v .: "total_count" <*>
v .: "current_page" <*>
v .: "per_page" <*>
v .: "sources" <*>
v .: "docs"
parseJSON _ = mzero
defaultOptions :: Options
defaultOptions = Options {
opAuthToken = Nothing,
opSortAscending = False,
opNumRows = Nothing,
opStartDate = Nothing,
opEndDate = Nothing,
opFrequency = Nothing,
opTransformation = Nothing,
opMetadataOnly = False
}
getTable :: String
-> String
-> Maybe String
-> IO (Maybe Dataset)
getTable source table auth = decode' <$> downloadJSON (defaultOptions { opAuthToken = auth }) [(source, table, Nothing)]
getTableWith :: Options
-> [(String, String, Maybe Int)]
-> IO (Maybe Dataset)
getTableWith options items = decode' <$> downloadJSON options items
downloadJSON :: Options -> [(String, String, Maybe Int)] -> IO L.ByteString
downloadJSON options items = simpleHttp $ createUrl options items
createUrl :: Options -> [(String, String, Maybe Int)] -> String
createUrl options items =
let pathSegs = case items of
[(source, table, _)] -> ["api", "v1", "datasets", T.pack (map toUpper source), T.pack (map toUpper table ++ ".json")]
_ -> ["api", "v1", "multisets.json"]
column = case items of
[(_, _, c)] -> maybeParam ("column", fmap show . const c)
_ -> param ("columns", createColumns items)
query = column ++
concatMap maybeParam [
("auth_token", opAuthToken),
("rows", fmap show . opNumRows),
("trim_start", fmap show . opStartDate),
("trim_end", fmap show . opEndDate),
("collapse", fmap (map toLower . show) . opFrequency),
("transformation", fmap (map toLower . show) . opTransformation)] ++
concatMap param [
("sort_order", if opSortAscending options then "asc" else "desc"),
("exclude_data", if opMetadataOnly options then "true" else "false")
]
maybeParam (k, fn) = maybe [] (param . (k,)) $ fn options
param (k, v) = [(k, Just $ BC.pack v)]
in BC.unpack $ toByteString $
fromByteString "http://www.quandl.com" <>
encodePathSegments pathSegs <>
renderQueryBuilder True query
createColumns :: [(String, String, Maybe Int)] -> String
createColumns items =
let toItem (source, table, column) = map toUpper source ++ "." ++ map toUpper table ++ (case column of { Nothing -> ""; Just c -> "." ++ show c })
in intercalate "," $ map toItem items
search :: [String]
-> Maybe String
-> Maybe Int
-> Maybe Int
-> IO (Maybe SearchPage)
search terms token per_page page = decode' <$> simpleHttp makeUrl where
query = concatMap param [Just ("query", intercalate "+" terms),
fmap (\t -> ("auth_token", t)) token,
fmap (\p -> ("page", show p)) page,
fmap (\p -> ("per_page", show p)) per_page]
param (Just (k, v)) = [(BC.pack k, Just $ BC.pack v)]
param Nothing = []
makeUrl = BC.unpack $ toByteString $
fromByteString "http://www.quandl.com/api/v1/datasets.json" <>
renderQueryBuilder True query