88from io import BytesIO
99from types import SimpleNamespace
1010from typing import Any , List , Sequence
11- from uuid import uuid4
1211
1312import pandas as pd
1413import polars as pl
2827 ValidationError ,
2928)
3029from .reporting .catalog import report_dict_to_sn
31- from .utils import get_dataset_dot_path , get_timestamp , get_user
30+ from .utils import get_dataset_dot_path , get_date , get_timestamp , get_user
3231
3332_here = os .path .abspath (os .path .dirname (__file__ ))
3433_config = ConfigParser ()
@@ -184,6 +183,7 @@ def write_blob(
184183 file_buffer : bytes | Sequence [bytes ],
185184 path_after_prefix : str ,
186185 auto_version : bool = False ,
186+ append : bool = False ,
187187 ) -> None :
188188 """For writing file buffers to blob storage. Remember to include
189189 the a version to the path (i.e., {version}/{file}) or use
@@ -195,8 +195,9 @@ def write_blob(
195195 file_buffer (bytes or List[bytes]): the file buffer or list of buffers
196196 path_under_prefix (str): everything beyond the prefix
197197 auto_version (bool, optional): whether to automatically version
198+ append (bool, optional): whether to append to existing file (only for single file writes).
198199 """
199- if auto_version :
200+ if auto_version and not append :
200201 path_after_prefix = (
201202 f"{ get_timestamp ()} /{ path_after_prefix .lstrip ('/' )} "
202203 )
@@ -206,7 +207,7 @@ def write_blob(
206207 file_buffer = [file_buffer ]
207208 total_partitions = len (file_buffer )
208209 for idx , fb_i in enumerate (file_buffer ):
209- if total_partitions > 1 :
210+ if total_partitions > 1 and not append :
210211 url_parts = os .path .splitext (full_path )
211212 auto_full_path = f"{ url_parts [0 ]} _{ str (idx ).zfill (len (str (total_partitions )))} { url_parts [1 ]} "
212213 else :
@@ -216,6 +217,7 @@ def write_blob(
216217 blob_url = auto_full_path ,
217218 account_name = self .account ,
218219 container_name = self .container ,
220+ append_blob = append ,
219221 )
220222 self .ledger_entry (action = "write" )
221223 # print(f"file written to: {full_path}")
@@ -383,11 +385,154 @@ def ledger_entry(self, action: str) -> None:
383385 log_data = (json .dumps (log_entry ) + "\n " ).encode ("utf-8" )
384386 write_blob_stream ( # TODO: make this a streaming write to a single file (one per day parsed from get_timestamp())
385387 data = log_data ,
386- blob_url = f"{ self .ledger_location ['prefix' ]} /{ get_timestamp () } _ { uuid4 (). hex } .jsonl" ,
388+ blob_url = f"{ self .ledger_location ['prefix' ]} /{ get_date () } .jsonl" ,
387389 account_name = self .ledger_location ["account" ],
388390 container_name = self .ledger_location ["container" ],
391+ append_blob = True ,
392+ overwrite = False ,
389393 )
390394
395+ def save_dataframe (
396+ self ,
397+ df : pd .DataFrame | pl .DataFrame ,
398+ path_after_prefix : str ,
399+ file_format : str = "parquet" ,
400+ auto_version : bool = False ,
401+ ) -> None :
402+ """Save a dataframe to the blob endpoint
403+
404+ Args:
405+ df (pd.DataFrame | pl.DataFrame): the dataframe to save
406+ path_after_prefix (str): the path after the prefix to save to
407+ file_format (str, optional): the file format to save as.
408+ Defaults to "parquet".
409+ auto_version (bool, optional): whether to automatically version
410+ the data. Defaults to True.
411+ partition_cols (List[str], optional): columns to partition by
412+ when saving. Defaults to None.
413+ """
414+ if file_format not in ["parquet" , "csv" , "json" , "jsonl" ]:
415+ raise ValueError (
416+ f"File format { file_format } not supported. Use 'parquet', 'csv', 'json', or 'jsonl'."
417+ )
418+ if file_format in ["json" , "jsonl" ] and path_after_prefix .endswith (
419+ ".json"
420+ ):
421+ path_after_prefix = path_after_prefix [:- 5 ] + ".jsonl"
422+ print ("Changing file extension to .jsonl for line-delimited JSON." )
423+ if isinstance (df , pd .DataFrame ):
424+ if file_format == "parquet" :
425+ pq_bytes = df .to_parquet (index = False , compression = "snappy" )
426+ self .write_blob (
427+ file_buffer = pq_bytes ,
428+ path_after_prefix = path_after_prefix
429+ if path_after_prefix .endswith (".parquet" )
430+ else path_after_prefix + ".parquet" ,
431+ auto_version = auto_version ,
432+ )
433+ elif file_format == "csv" :
434+ csv_bytes = df .to_csv (index = False ).encode ("utf-8" )
435+ self .write_blob (
436+ file_buffer = csv_bytes ,
437+ path_after_prefix = path_after_prefix
438+ if path_after_prefix .endswith (".csv" )
439+ else path_after_prefix + ".csv" ,
440+ auto_version = auto_version ,
441+ )
442+ elif file_format in ["json" , "jsonl" ]:
443+ json_bytes = df .to_json (orient = "records" , lines = True ).encode (
444+ "utf-8"
445+ )
446+ self .write_blob (
447+ file_buffer = json_bytes ,
448+ path_after_prefix = path_after_prefix
449+ if path_after_prefix .endswith (".jsonl" )
450+ else path_after_prefix + ".jsonl" ,
451+ auto_version = auto_version ,
452+ )
453+ elif isinstance (df , pl .DataFrame ):
454+ if file_format == "parquet" :
455+ buffer = BytesIO ()
456+ df .write_parquet (buffer , compression = "snappy" )
457+ pq_bytes = buffer .getvalue ()
458+ self .write_blob (
459+ file_buffer = pq_bytes ,
460+ path_after_prefix = path_after_prefix
461+ if path_after_prefix .endswith (".parquet" )
462+ else path_after_prefix + ".parquet" ,
463+ auto_version = auto_version ,
464+ )
465+ elif file_format == "csv" :
466+ csv_bytes = df .write_csv ().encode ("utf-8" )
467+ self .write_blob (
468+ file_buffer = csv_bytes ,
469+ path_after_prefix = path_after_prefix
470+ if path_after_prefix .endswith (".csv" )
471+ else path_after_prefix + ".csv" ,
472+ auto_version = auto_version ,
473+ )
474+ elif file_format in ["json" , "jsonl" ]:
475+ json_bytes = df .write_ndjson ().encode ("utf-8" )
476+ self .write_blob (
477+ file_buffer = json_bytes ,
478+ path_after_prefix = path_after_prefix
479+ if path_after_prefix .endswith (".jsonl" )
480+ else path_after_prefix + ".jsonl" ,
481+ auto_version = auto_version ,
482+ )
483+
484+ def save_file_to_blob (
485+ self ,
486+ file_path : str ,
487+ path_after_prefix : str ,
488+ auto_version : bool = False ,
489+ ) -> None :
490+ """Save a local file to the blob endpoint
491+
492+ Args:
493+ file_path (str): the local file path to save
494+ path_after_prefix (str): the path after the prefix to save to
495+ auto_version (bool, optional): whether to automatically version
496+ the data. Defaults to False.
497+ """
498+ if not os .path .isfile (file_path ):
499+ raise ValueError (f"File { file_path } does not exist." )
500+ with open (file_path , "rb" ) as f :
501+ file_bytes = f .read ()
502+ self .write_blob (
503+ file_buffer = file_bytes ,
504+ path_after_prefix = path_after_prefix ,
505+ auto_version = auto_version ,
506+ )
507+
508+ def save_dir_to_blob (
509+ self ,
510+ dir_path : str ,
511+ path_after_prefix : str ,
512+ auto_version : bool = False ,
513+ ) -> None :
514+ """Save a local directory to the blob endpoint
515+
516+ Args:
517+ dir_path (str): the local directory path to save
518+ path_after_prefix (str): the path after the prefix to save to
519+ auto_version (bool, optional): whether to automatically version
520+ the data. Defaults to False.
521+ """
522+ if not os .path .isdir (dir_path ):
523+ raise ValueError (f"Directory { dir_path } does not exist." )
524+ for root , _ , files in os .walk (dir_path ):
525+ for file in files :
526+ file_path = os .path .join (root , file )
527+ with open (file_path , "rb" ) as f :
528+ file_buffer = f .read ()
529+ rel_path = f"{ '/' .join ([i for i in os .path .split (root ) if i ])} /{ '/' .join ([i for i in os .path .split (file ) if i ])} "
530+ self .write_blob (
531+ file_buffer = file_buffer ,
532+ path_after_prefix = f"{ path_after_prefix } /{ rel_path } " ,
533+ auto_version = auto_version ,
534+ )
535+
391536
392537def dict_to_sn (d : Any , defaults : dict = None , ns : str = "" ) -> SimpleNamespace :
393538 """Simple recursive namespace construction
0 commit comments