packages feed

dataframe-parquet 1.0.1.2 → 1.1.0.0

raw patch · 2 files changed

+24/−203 lines, 2 filesdep −aesondep −http-conduitPVP ok

version bump matches the API change (PVP)

Dependencies removed: aeson, http-conduit

API changes (from Hackage documentation)

- DataFrame.IO.Parquet: HFParquetFile :: Text -> Text -> Text -> Text -> HFParquetFile
- DataFrame.IO.Parquet: HFParquetResponse :: [HFParquetFile] -> HFParquetResponse
- DataFrame.IO.Parquet: HFRef :: Text -> Text -> Text -> HFRef
- DataFrame.IO.Parquet: [hfDataset] :: HFRef -> Text
- DataFrame.IO.Parquet: [hfGlob] :: HFRef -> Text
- DataFrame.IO.Parquet: [hfOwner] :: HFRef -> Text
- DataFrame.IO.Parquet: [hfParquetFiles] :: HFParquetResponse -> [HFParquetFile]
- DataFrame.IO.Parquet: [hfpConfig] :: HFParquetFile -> Text
- DataFrame.IO.Parquet: [hfpFilename] :: HFParquetFile -> Text
- DataFrame.IO.Parquet: [hfpSplit] :: HFParquetFile -> Text
- DataFrame.IO.Parquet: [hfpUrl] :: HFParquetFile -> Text
- DataFrame.IO.Parquet: data HFParquetFile
- DataFrame.IO.Parquet: data HFRef
- DataFrame.IO.Parquet: directHFUrl :: HFRef -> Text
- DataFrame.IO.Parquet: downloadHFFiles :: Maybe ByteString -> [HFParquetFile] -> IO [FilePath]
- DataFrame.IO.Parquet: fetchHFParquetFiles :: FilePath -> IO [FilePath]
- DataFrame.IO.Parquet: getHFToken :: IO (Maybe ByteString)
- DataFrame.IO.Parquet: hasGlob :: Text -> Bool
- DataFrame.IO.Parquet: hfUrlRepoPath :: HFParquetFile -> String
- DataFrame.IO.Parquet: instance Data.Aeson.Types.FromJSON.FromJSON DataFrame.IO.Parquet.HFParquetFile
- DataFrame.IO.Parquet: instance Data.Aeson.Types.FromJSON.FromJSON DataFrame.IO.Parquet.HFParquetResponse
- DataFrame.IO.Parquet: instance GHC.Show.Show DataFrame.IO.Parquet.HFParquetFile
- DataFrame.IO.Parquet: isHFUri :: FilePath -> Bool
- DataFrame.IO.Parquet: matchesGlob :: Text -> HFParquetFile -> Bool
- DataFrame.IO.Parquet: newtype HFParquetResponse
- DataFrame.IO.Parquet: parseHFUri :: FilePath -> Either String HFRef
- DataFrame.IO.Parquet: resolveHFUrls :: Maybe ByteString -> HFRef -> IO [HFParquetFile]

Files

dataframe-parquet.cabal view
@@ -1,15 +1,16 @@ cabal-version:      2.4 name:               dataframe-parquet-version:            1.0.1.2+version:            1.1.0.0  synopsis:           Parquet reader and writer for the dataframe ecosystem. description:     @DataFrame.IO.Parquet@ — pure-Haskell Parquet 2.0 reader and writer     (with snappy, zstd, gzip codecs, dictionary decoding, nested-    list/repeated columns, predicate pushdown, and HTTP range reads).-    Heavy package — pulls in @pinch@, @streamly@, compression codecs,-    and @http-conduit@. Most users want @dataframe-csv@ instead unless-    they specifically need Parquet.+    list/repeated columns, and predicate pushdown).+    Heavy package — pulls in @pinch@, @streamly@, and compression codecs.+    Reading directly from HuggingFace (@hf://@) datasets lives in the+    separate @dataframe-huggingface@ package. Most users want+    @dataframe-csv@ instead unless they specifically need Parquet.  bug-reports:        https://github.com/mchav/dataframe/issues license:            MIT@@ -45,7 +46,6 @@                         DataFrame.IO.Parquet.Utils                         DataFrame.IO.Utils.RandomAccess     build-depends:      base >= 4 && < 5,-                        aeson >= 0.11.0.0 && < 3,                         bytestring >= 0.11 && < 0.13,                         containers >= 0.6.7 && < 0.9,                         dataframe-core ^>= 1.1,@@ -54,12 +54,11 @@                         directory >= 1.3.0.0 && < 2,                         filepath >= 1.4 && < 2,                         Glob >= 0.10 && < 1,-                        http-conduit >= 2.3 && < 3,                         pinch >= 0.5 && < 1,                         snappy-hs ^>= 0.1,                         streamly-bytestring >= 0.2.0 && < 0.4,                         streamly-core >= 0.2.3 && < 0.4,-                        text >= 2.0 && < 3,+                        text >= 2.1 && < 3,                         time >= 1.12 && < 2,                         vector ^>= 0.13,                         zlib >= 0.5 && < 1,
src/DataFrame/IO/Parquet.hs view
@@ -9,10 +9,9 @@  module DataFrame.IO.Parquet where -import Control.Exception (throw, try)+import Control.Exception (throw) import Control.Monad import Control.Monad.IO.Class (MonadIO (..))-import Data.Aeson (FromJSON (..), eitherDecodeStrict, withObject, (.:)) import Data.Bits (Bits (shiftL), (.|.)) import qualified Data.ByteString as BS import Data.Either (fromRight)@@ -26,7 +25,6 @@ import qualified Data.List as L import qualified Data.Map as Map import qualified Data.Text as T-import Data.Text.Encoding (encodeUtf8) import Data.Time (UTCTime) import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import qualified Data.Vector as Vector@@ -82,23 +80,11 @@ import DataFrame.Internal.Expression (Expr, getColumns) import DataFrame.Operations.Merge () import qualified DataFrame.Operations.Subset as DS-import Network.HTTP.Simple (-    getResponseBody,-    getResponseStatusCode,-    httpBS,-    parseRequest,-    setRequestHeader,- ) import qualified Pinch import qualified Streamly.Data.Stream as Stream-import System.Directory (-    doesDirectoryExist,-    getHomeDirectory,-    getTemporaryDirectory,- )-import System.Environment (lookupEnv)+import System.Directory (doesDirectoryExist) import System.FilePath ((</>))-import System.FilePath.Glob (compile, glob, match)+import System.FilePath.Glob (glob) import System.IO (IOMode (ReadMode))  -- Options -----------------------------------------------------------------@@ -175,13 +161,7 @@ are auto-included for decoding, then projected back to the requested output columns. -} readParquetWithOpts :: ParquetReadOptions -> FilePath -> IO DataFrame-readParquetWithOpts opts path-    | isHFUri path = do-        paths <- fetchHFParquetFiles path-        let optsNoRange = opts{rowRange = Nothing}-        dfs <- mapM (_readParquetWithOpts Nothing optsNoRange) paths-        pure (applyRowRange opts (mconcat dfs))-    | otherwise = _readParquetWithOpts Nothing opts path+readParquetWithOpts = _readParquetWithOpts Nothing  -- | Internal entry point used by tests to force non-seekable mode. _readParquetWithOpts ::@@ -212,29 +192,23 @@ @ -} readParquetFilesWithOpts :: ParquetReadOptions -> FilePath -> IO DataFrame-readParquetFilesWithOpts opts path-    | isHFUri path = do-        files <- fetchHFParquetFiles path-        let optsWithoutRowRange = opts{rowRange = Nothing}-        dfs <- mapM (_readParquetWithOpts Nothing optsWithoutRowRange) files-        pure (applyRowRange opts (mconcat dfs))-    | otherwise = do-        isDir <- doesDirectoryExist path+readParquetFilesWithOpts opts path = do+    isDir <- doesDirectoryExist path -        let pat = if isDir then path </> "*.parquet" else path+    let pat = if isDir then path </> "*.parquet" else path -        matches <- glob pat+    matches <- glob pat -        files <- filterM (fmap not . doesDirectoryExist) matches+    files <- filterM (fmap not . doesDirectoryExist) matches -        case files of-            [] ->-                error $-                    "readParquetFiles: no parquet files found for " ++ path-            _ -> do-                let optsWithoutRowRange = opts{rowRange = Nothing}-                dfs <- mapM (readParquetWithOpts optsWithoutRowRange) files-                pure (applyRowRange opts (mconcat dfs))+    case files of+        [] ->+            error $+                "readParquetFiles: no parquet files found for " ++ path+        _ -> do+            let optsWithoutRowRange = opts{rowRange = Nothing}+            dfs <- mapM (readParquetWithOpts optsWithoutRowRange) files+            pure (applyRowRange opts (mconcat dfs))  -- Core parsing pipeline --------------------------------------------------- @@ -586,155 +560,3 @@ microsecondsToUTCTime :: Int64 -> UTCTime microsecondsToUTCTime us =     posixSecondsToUTCTime (fromIntegral us / 1_000_000)---- HuggingFace support -------------------------------------------------------data HFRef = HFRef-    { hfOwner :: T.Text-    , hfDataset :: T.Text-    , hfGlob :: T.Text-    }--data HFParquetFile = HFParquetFile-    { hfpUrl :: T.Text-    , hfpConfig :: T.Text-    , hfpSplit :: T.Text-    , hfpFilename :: T.Text-    }-    deriving (Show)--instance FromJSON HFParquetFile where-    parseJSON = withObject "HFParquetFile" $ \o ->-        HFParquetFile-            <$> o .: "url"-            <*> o .: "config"-            <*> o .: "split"-            <*> o .: "filename"--newtype HFParquetResponse = HFParquetResponse {hfParquetFiles :: [HFParquetFile]}--instance FromJSON HFParquetResponse where-    parseJSON = withObject "HFParquetResponse" $ \o ->-        HFParquetResponse <$> o .: "parquet_files"--isHFUri :: FilePath -> Bool-isHFUri = L.isPrefixOf "hf://"--parseHFUri :: FilePath -> Either String HFRef-parseHFUri path =-    let stripped = drop (length ("hf://datasets/" :: String)) path-     in case T.splitOn "/" (T.pack stripped) of-            (owner : dataset : rest)-                | not (null rest) ->-                    Right $ HFRef owner dataset (T.intercalate "/" rest)-            _ ->-                Left $ "Invalid hf:// URI (expected hf://datasets/owner/dataset/glob): " ++ path--getHFToken :: IO (Maybe BS.ByteString)-getHFToken = do-    envToken <- lookupEnv "HF_TOKEN"-    case envToken of-        Just t -> pure (Just (encodeUtf8 (T.pack t)))-        Nothing -> do-            home <- getHomeDirectory-            let tokenPath = home </> ".cache" </> "huggingface" </> "token"-            result <- try (BS.readFile tokenPath) :: IO (Either IOError BS.ByteString)-            case result of-                Right bs -> pure (Just (BS.takeWhile (/= 10) bs))-                Left _ -> pure Nothing--{- | Extract the repo-relative path from a HuggingFace download URL.-URL format: https://huggingface.co/datasets/{owner}/{dataset}/resolve/{ref}/{path}-Returns the {path} portion (e.g. "data/train-00000-of-00001.parquet").--}-hfUrlRepoPath :: HFParquetFile -> String-hfUrlRepoPath f =-    case T.breakOn "/resolve/" (hfpUrl f) of-        (_, rest)-            | not (T.null rest) ->-                -- Drop "/resolve/", then drop the ref component (up to and including "/")-                T.unpack $ T.drop 1 $ T.dropWhile (/= '/') $ T.drop (T.length "/resolve/") rest-        _ ->-            T.unpack (hfpConfig f) </> T.unpack (hfpSplit f) </> T.unpack (hfpFilename f)--matchesGlob :: T.Text -> HFParquetFile -> Bool-matchesGlob g f = match (compile (T.unpack g)) (hfUrlRepoPath f)--resolveHFUrls :: Maybe BS.ByteString -> HFRef -> IO [HFParquetFile]-resolveHFUrls mToken ref = do-    let dataset = hfOwner ref <> "/" <> hfDataset ref-    let apiUrl = "https://datasets-server.huggingface.co/parquet?dataset=" ++ T.unpack dataset-    req0 <- parseRequest apiUrl-    let req = case mToken of-            Nothing -> req0-            Just tok -> setRequestHeader "Authorization" ["Bearer " <> tok] req0-    resp <- httpBS req-    let status = getResponseStatusCode resp-    when (status /= 200) $-        ioError $-            userError $-                "HuggingFace API returned status "-                    ++ show status-                    ++ " for dataset "-                    ++ T.unpack dataset-    case eitherDecodeStrict (getResponseBody resp) of-        Left err -> ioError $ userError $ "Failed to parse HF API response: " ++ err-        Right hfResp -> pure $ filter (matchesGlob (hfGlob ref)) (hfParquetFiles hfResp)--downloadHFFiles :: Maybe BS.ByteString -> [HFParquetFile] -> IO [FilePath]-downloadHFFiles mToken files = do-    tmpDir <- getTemporaryDirectory-    forM files $ \f -> do-        -- Derive a collision-resistant temp name from the URL path components-        let fname = case (hfpConfig f, hfpSplit f) of-                (c, s) | T.null c && T.null s -> T.unpack (hfpFilename f)-                (c, s) -> T.unpack c <> "_" <> T.unpack s <> "_" <> T.unpack (hfpFilename f)-        let destPath = tmpDir </> fname-        req0 <- parseRequest (T.unpack (hfpUrl f))-        let req = case mToken of-                Nothing -> req0-                Just tok -> setRequestHeader "Authorization" ["Bearer " <> tok] req0-        resp <- httpBS req-        let status = getResponseStatusCode resp-        when (status /= 200) $-            ioError $-                userError $-                    "Failed to download " ++ T.unpack (hfpUrl f) ++ " (HTTP " ++ show status ++ ")"-        BS.writeFile destPath (getResponseBody resp)-        pure destPath---- | True when the path contains glob wildcard characters.-hasGlob :: T.Text -> Bool-hasGlob = T.any (\c -> c == '*' || c == '?' || c == '[')--{- | Build the direct HF repo download URL for a path with no wildcards.-Format: https://huggingface.co/datasets/{owner}/{dataset}/resolve/main/{path}--}-directHFUrl :: HFRef -> T.Text-directHFUrl ref =-    "https://huggingface.co/datasets/"-        <> hfOwner ref-        <> "/"-        <> hfDataset ref-        <> "/resolve/main/"-        <> hfGlob ref--fetchHFParquetFiles :: FilePath -> IO [FilePath]-fetchHFParquetFiles uri = do-    ref <- case parseHFUri uri of-        Left err -> ioError (userError err)-        Right r -> pure r-    mToken <- getHFToken-    if hasGlob (hfGlob ref)-        then do-            hfFiles <- resolveHFUrls mToken ref-            when (null hfFiles) $-                ioError $-                    userError $-                        "No parquet files found for " ++ uri-            downloadHFFiles mToken hfFiles-        else do-            -- Direct repo file download — no datasets-server needed-            let url = directHFUrl ref-            let filename = last $ T.splitOn "/" (hfGlob ref)-            downloadHFFiles mToken [HFParquetFile url "" "" filename]