66from configparser import ConfigParser
77from importlib import import_module
88from io import BytesIO
9+ from pathlib import PurePosixPath
910from types import SimpleNamespace
10- from typing import Any , List , Sequence
11+ from typing import Any , List , Literal , Sequence , overload
1112
1213import pandas as pd
1314import polars as pl
1415import tomli
16+ from azure .identity import ManagedIdentityCredential
1517
1618from cfa .cloudops .blob_helpers import (
1719 read_blob_stream ,
@@ -386,37 +388,111 @@ def download_version_to_local(
386388 self .ledger_entry (action = "read" )
387389 return written
388390
391+ @overload
389392 def get_dataframe (
390393 self ,
391- output = "pandas" ,
392- version = "latest" ,
393- pl_lazy : bool = False ,
394+ output : Literal ["pandas" , "pd" ] = "pandas" ,
395+ version : str = "latest" ,
396+ newest : bool = True ,
397+ ) -> pd .DataFrame : ...
398+
399+ @overload
400+ def get_dataframe (
401+ self ,
402+ output : Literal ["polars" , "pl" ],
403+ version : str = "latest" ,
404+ newest : bool = True ,
405+ ) -> pl .DataFrame : ...
406+
407+ @overload
408+ def get_dataframe (
409+ self ,
410+ output : Literal ["pl_lazy" , "lazy" ],
411+ version : str = "latest" ,
394412 newest : bool = True ,
395- ) -> pd .DataFrame | pl .DataFrame :
413+ ) -> pl .LazyFrame : ...
414+
415+ def get_dataframe (
416+ self ,
417+ output : Literal [
418+ "pandas" , "pd" , "polars" , "pl" , "pl_lazy" , "lazy"
419+ ] = "pandas" ,
420+ version : str = "latest" ,
421+ newest : bool = True ,
422+ ) -> pd .DataFrame | pl .DataFrame | pl .LazyFrame :
396423 """Get the data as a pandas or polars dataframe
397424
398425 Args:
399426 output (str, optional): the type of dataframe to return,
400- either 'pandas' or 'polars'. Defaults to "pandas".
427+ either 'pandas' or 'polars' or 'pl_lazy' . Defaults to "pandas".
401428 version (str, optional): the version of the data to get.
402429 Defaults to "latest".
403- pl_lazy (bool, optional): whether to return a lazy polars dataframe.
404- Defaults to False.
405430 newest (bool, optional): whether to get the newest matching version. Defaults to True.
406431 False returns the oldest matching version.
407432
408433 Raises:
409- ValueError: if output is not 'pandas' or 'polars'
434+ ValueError: if output is not one of
435+ 'pandas', 'pd', 'polars', 'pl', 'pl_lazy', or 'lazy'
410436
411437 Returns:
412- pd.DataFrame | pl.DataFrame: the dataframe
438+ pd.DataFrame | pl.DataFrame | pl.LazyFrame : the dataframe
413439 """
414- if output not in ["pandas" , "polars" , "pd" , "pl" ]:
440+ if output not in ["pandas" , "polars" , "pd" , "pl" , "pl_lazy" , "lazy" ]:
441+ raise ValueError (
442+ f"Output { output } needs to be 'pandas', 'polars', 'pd', 'pl', 'pl_lazy', or 'lazy'."
443+ )
444+ # Fetch version blobs once and validate before deriving file extension.
445+ version_blobs = self ._get_version_blobs (version = version , newest = newest )
446+ if not version_blobs :
415447 raise ValueError (
416- f"Output { output } needs to be 'pandas', 'polars', 'pd, or 'pl '."
448+ f"No blobs found for version ' { version } ' in container ' { self . container } '."
417449 )
450+ name = version_blobs [0 ]["name" ]
451+ file_ext = PurePosixPath (name ).suffix .lstrip ("." ).lower ()
452+ if output in ["pl_lazy" , "lazy" ]:
453+ if file_ext in ["parquet" , "parq" ]:
454+ path = str (PurePosixPath (name ).parent / f"*.{ file_ext } " )
455+ fullpath = f"az://{ self .container } /{ path } "
456+ df = pl .scan_parquet (
457+ fullpath ,
458+ storage_options = {"account_name" : self .account },
459+ credential_provider = pl .CredentialProviderAzure (
460+ credential = ManagedIdentityCredential ()
461+ ),
462+ )
463+ self .ledger_entry (action = "read" )
464+ return df
465+ elif file_ext == "csv" :
466+ path = str (PurePosixPath (name ).parent / f"*.{ file_ext } " )
467+ fullpath = f"az://{ self .container } /{ path } "
468+ df = pl .scan_csv (
469+ fullpath ,
470+ infer_schema_length = None ,
471+ storage_options = {"account_name" : self .account },
472+ credential_provider = pl .CredentialProviderAzure (
473+ credential = ManagedIdentityCredential ()
474+ ),
475+ )
476+ self .ledger_entry (action = "read" )
477+ return df
478+ elif file_ext == "ndjson" or file_ext == "jsonl" :
479+ path = str (PurePosixPath (name ).parent / f"*.{ file_ext } " )
480+ fullpath = f"az://{ self .container } /{ path } "
481+ df = pl .scan_ndjson (
482+ fullpath ,
483+ infer_schema_length = None ,
484+ storage_options = {"account_name" : self .account },
485+ credential_provider = pl .CredentialProviderAzure (
486+ credential = ManagedIdentityCredential ()
487+ ),
488+ )
489+ self .ledger_entry (action = "read" )
490+ return df
491+ else :
492+ raise ValueError (
493+ f"Lazy loading not supported for { file_ext } files."
494+ )
418495 blobs = self .read_blobs (version , newest = newest )
419- file_ext = self .get_file_ext (version = version )
420496 blob_bytes = [blob .content_as_bytes () for blob in blobs ]
421497 blob_files = [BytesIO (pq ) for pq in blob_bytes ]
422498 if file_ext == "csv" :
@@ -431,8 +507,6 @@ def get_dataframe(
431507 ],
432508 how = "diagonal" ,
433509 )
434- if pl_lazy :
435- df = df .lazy ()
436510 return df
437511 elif file_ext == "json" :
438512 if output in ["pandas" , "pd" ]:
@@ -446,10 +520,8 @@ def get_dataframe(
446520 ],
447521 how = "diagonal" ,
448522 )
449- if pl_lazy :
450- df = df .lazy ()
451523 return df
452- elif file_ext == "jsonl" :
524+ elif file_ext == "jsonl" or file_ext == "ndjson" :
453525 if output in ["pandas" , "pd" ]:
454526 df = pd .concat (
455527 [pd .read_json (blob , lines = True ) for blob in blob_files ]
@@ -460,8 +532,6 @@ def get_dataframe(
460532 [pl .read_ndjson (blob ) for blob in blob_files ],
461533 how = "diagonal" ,
462534 )
463- if pl_lazy :
464- df = df .lazy ()
465535 return df
466536 elif file_ext == "parquet" or file_ext == "parq" :
467537 if output in ["pandas" , "pd" ]:
@@ -474,8 +544,6 @@ def get_dataframe(
474544 [pl .read_parquet (pq_file ) for pq_file in blob_files ],
475545 how = "diagonal" ,
476546 )
477- if pl_lazy :
478- df = df .lazy ()
479547 return df
480548
481549 def ledger_entry (self , action : str ) -> None :
0 commit comments