containerio 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {containerio-2.2.0 → containerio-2.3.0}/PKG-INFO +1 -1
- {containerio-2.2.0 → containerio-2.3.0}/pyproject.toml +1 -1
- {containerio-2.2.0 → containerio-2.3.0}/src/containerio/handler.py +75 -64
- {containerio-2.2.0 → containerio-2.3.0}/README.md +0 -0
- {containerio-2.2.0 → containerio-2.3.0}/src/containerio/__init__.py +0 -0
- {containerio-2.2.0 → containerio-2.3.0}/src/containerio/azure_config.py +0 -0
- {containerio-2.2.0 → containerio-2.3.0}/src/containerio/session.py +0 -0
- {containerio-2.2.0 → containerio-2.3.0}/src/containerio/tools/cli.py +0 -0
|
@@ -377,7 +377,24 @@ class StorageHandler:
|
|
|
377
377
|
)
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
def
|
|
380
|
+
def read_blob_as_bytes(self, blob_name: str) -> bytes:
|
|
381
|
+
"""Read a blob's raw bytes from local or Azure storage.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
blob_name (str): The path/name of the blob to read.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
bytes: The blob's raw contents.
|
|
388
|
+
|
|
389
|
+
Raises:
|
|
390
|
+
FileNotFoundError: If the blob does not exist.
|
|
391
|
+
"""
|
|
392
|
+
if self.local:
|
|
393
|
+
try:
|
|
394
|
+
return (self.base_path / blob_name).read_bytes()
|
|
395
|
+
except FileNotFoundError as error:
|
|
396
|
+
raise FileNotFoundError(f"Blob not found: {blob_name}") from error
|
|
397
|
+
|
|
381
398
|
try:
|
|
382
399
|
return self._get_blob_client(blob_name=blob_name).download_blob().readall()
|
|
383
400
|
except ResourceNotFoundError as error:
|
|
@@ -686,12 +703,8 @@ class StorageHandler:
|
|
|
686
703
|
pl.DataFrame: The content of the CSV file as a Polars
|
|
687
704
|
DataFrame.
|
|
688
705
|
"""
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
return pl.read_csv(source=file_path, **kwargs)
|
|
692
|
-
else:
|
|
693
|
-
data = self._read_blob_as_bytes(blob_name)
|
|
694
|
-
return pl.read_csv(source=BytesIO(data), **kwargs)
|
|
706
|
+
data = self.read_blob_as_bytes(blob_name=blob_name)
|
|
707
|
+
return pl.read_csv(source=BytesIO(data), **kwargs)
|
|
695
708
|
|
|
696
709
|
def scan_csv_blob(self, blob_name: str, **kwargs: Dict) -> pl.LazyFrame:
|
|
697
710
|
"""Scans a CSV file or multiple files from storage.
|
|
@@ -731,12 +744,8 @@ class StorageHandler:
|
|
|
731
744
|
pl.DataFrame: The content of the Excel file as a Polars
|
|
732
745
|
DataFrame.
|
|
733
746
|
"""
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
df = pl.read_excel(source=file_path, **kwargs)
|
|
737
|
-
else:
|
|
738
|
-
data = self._read_blob_as_bytes(blob_name)
|
|
739
|
-
df = pl.read_excel(source=BytesIO(data), **kwargs)
|
|
747
|
+
data = self.read_blob_as_bytes(blob_name=blob_name)
|
|
748
|
+
df = pl.read_excel(source=BytesIO(data), **kwargs)
|
|
740
749
|
|
|
741
750
|
if not isinstance(df, pl.DataFrame):
|
|
742
751
|
raise TypeError(
|
|
@@ -757,16 +766,10 @@ class StorageHandler:
|
|
|
757
766
|
Tuple[pl.DataFrame, Dict]: A tuple containing the DataFrame
|
|
758
767
|
and the Parquet file's metadata.
|
|
759
768
|
"""
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
return df, metadata
|
|
765
|
-
else:
|
|
766
|
-
data = self._read_blob_as_bytes(blob_name)
|
|
767
|
-
df = pl.read_parquet(source=BytesIO(data))
|
|
768
|
-
metadata = pl.read_parquet_metadata(source=BytesIO(data))
|
|
769
|
-
return df, metadata
|
|
769
|
+
data = self.read_blob_as_bytes(blob_name=blob_name)
|
|
770
|
+
df = pl.read_parquet(source=BytesIO(data))
|
|
771
|
+
metadata = pl.read_parquet_metadata(source=BytesIO(data))
|
|
772
|
+
return df, metadata
|
|
770
773
|
|
|
771
774
|
def scan_parquet_blob(
|
|
772
775
|
self, blob_name: str, **kwargs: Dict
|
|
@@ -845,15 +848,43 @@ class StorageHandler:
|
|
|
845
848
|
|
|
846
849
|
return download_path
|
|
847
850
|
|
|
848
|
-
def
|
|
851
|
+
def write_blob_as_bytes(
|
|
852
|
+
self, blob_name: str, data: bytes
|
|
853
|
+
) -> Optional[Dict[str, Any]]:
|
|
854
|
+
"""Write raw bytes to a blob in local or Azure storage.
|
|
855
|
+
|
|
856
|
+
Existing blobs are overwritten. For local storage, parent
|
|
857
|
+
directories are created as needed.
|
|
858
|
+
|
|
859
|
+
Args:
|
|
860
|
+
blob_name (str): The path/name of the blob to write.
|
|
861
|
+
data (bytes): The raw bytes to write.
|
|
862
|
+
|
|
863
|
+
Returns:
|
|
864
|
+
Optional[Dict[str, Any]]: The Azure upload response, or
|
|
865
|
+
`None` for local storage.
|
|
866
|
+
|
|
867
|
+
Raises:
|
|
868
|
+
ValueError: If `blob_name` is empty or not a string.
|
|
869
|
+
TypeError: If `data` is not bytes.
|
|
870
|
+
"""
|
|
849
871
|
if not isinstance(blob_name, str) or not blob_name:
|
|
850
872
|
raise ValueError("Blob name must be str and not empty")
|
|
851
873
|
if not isinstance(data, bytes):
|
|
852
874
|
raise TypeError("Data must be bytes")
|
|
875
|
+
|
|
876
|
+
if self.local:
|
|
877
|
+
file_path = self.base_path / blob_name
|
|
878
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
879
|
+
file_path.write_bytes(data)
|
|
880
|
+
return None
|
|
881
|
+
|
|
853
882
|
# Overwrite if the blob exists
|
|
854
883
|
try:
|
|
855
884
|
blob_client = self._get_blob_client(blob_name=blob_name)
|
|
856
|
-
return blob_client.upload_blob(
|
|
885
|
+
return blob_client.upload_blob(
|
|
886
|
+
data=data, blob_type="BlockBlob", overwrite=True
|
|
887
|
+
)
|
|
857
888
|
except Exception as e:
|
|
858
889
|
raise RuntimeError(f"Failed to write blob '{blob_name}': {e}") from e
|
|
859
890
|
|
|
@@ -893,17 +924,9 @@ class StorageHandler:
|
|
|
893
924
|
Returns:
|
|
894
925
|
None
|
|
895
926
|
"""
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
df.write_csv(file=file_path, **kwargs)
|
|
900
|
-
else:
|
|
901
|
-
# Write the DataFrame to an in-memory stream
|
|
902
|
-
buffer = BytesIO()
|
|
903
|
-
df.write_csv(file=buffer, **kwargs)
|
|
904
|
-
buffer.seek(0)
|
|
905
|
-
# Upload the bytes to Azure Blob Storage
|
|
906
|
-
self._write_blob_as_bytes(blob_name, buffer.getvalue())
|
|
927
|
+
buffer = BytesIO()
|
|
928
|
+
df.write_csv(file=buffer, **kwargs)
|
|
929
|
+
self.write_blob_as_bytes(blob_name=blob_name, data=buffer.getvalue())
|
|
907
930
|
|
|
908
931
|
def write_excel_blob(
|
|
909
932
|
self, blob_name: str, df: pl.DataFrame, **kwargs: Dict
|
|
@@ -919,17 +942,9 @@ class StorageHandler:
|
|
|
919
942
|
Returns:
|
|
920
943
|
None
|
|
921
944
|
"""
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
df.write_excel(workbook=file_path, **kwargs)
|
|
926
|
-
else:
|
|
927
|
-
# Write the DataFrame to an in-memory stream
|
|
928
|
-
buffer = BytesIO()
|
|
929
|
-
df.write_excel(workbook=buffer, **kwargs)
|
|
930
|
-
buffer.seek(0)
|
|
931
|
-
# Upload the bytes to Azure Blob Storage
|
|
932
|
-
self._write_blob_as_bytes(blob_name, buffer.getvalue())
|
|
945
|
+
buffer = BytesIO()
|
|
946
|
+
df.write_excel(workbook=buffer, **kwargs)
|
|
947
|
+
self.write_blob_as_bytes(blob_name=blob_name, data=buffer.getvalue())
|
|
933
948
|
|
|
934
949
|
def write_parquet_blob(
|
|
935
950
|
self, blob_name: str, df: pl.DataFrame, **kwargs: Dict
|
|
@@ -945,17 +960,9 @@ class StorageHandler:
|
|
|
945
960
|
Returns:
|
|
946
961
|
None
|
|
947
962
|
"""
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
df.write_parquet(file=file_path, **kwargs)
|
|
952
|
-
else:
|
|
953
|
-
# Write the DataFrame to an in-memory stream
|
|
954
|
-
buffer = BytesIO()
|
|
955
|
-
df.write_parquet(file=buffer, **kwargs)
|
|
956
|
-
buffer.seek(0)
|
|
957
|
-
# Upload the bytes to Azure Blob Storage
|
|
958
|
-
self._write_blob_as_bytes(blob_name, buffer.getvalue())
|
|
963
|
+
buffer = BytesIO()
|
|
964
|
+
df.write_parquet(file=buffer, **kwargs)
|
|
965
|
+
self.write_blob_as_bytes(blob_name=blob_name, data=buffer.getvalue())
|
|
959
966
|
|
|
960
967
|
def sink_parquet_blob(
|
|
961
968
|
self, blob_name: str, lf: pl.LazyFrame, **kwargs: Dict
|
|
@@ -1049,18 +1056,22 @@ class StorageHandler:
|
|
|
1049
1056
|
prefix = blob_name.rstrip("/") + "/"
|
|
1050
1057
|
children = list(self.container_client.list_blobs(name_starts_with=prefix))
|
|
1051
1058
|
|
|
1052
|
-
#
|
|
1059
|
+
# Delete children deepest-first. On hierarchical-namespace
|
|
1060
|
+
# (ADLS Gen2) accounts a directory placeholder cannot be
|
|
1061
|
+
# removed until empty, and a parent path always sorts before
|
|
1062
|
+
# its descendants, so reverse order empties each placeholder
|
|
1063
|
+
# before deleting it. Flat namespaces are order-independent.
|
|
1064
|
+
for blob in sorted(children, key=lambda child: child.name, reverse=True):
|
|
1065
|
+
self._get_blob_client(blob_name=blob.name).delete_blob()
|
|
1066
|
+
deleted.append(blob.name)
|
|
1067
|
+
|
|
1068
|
+
# Delete the exact blob (file or now-empty dir) last.
|
|
1053
1069
|
try:
|
|
1054
1070
|
self._get_blob_client(blob_name=blob_name).delete_blob()
|
|
1055
1071
|
deleted.append(blob_name)
|
|
1056
1072
|
except ResourceNotFoundError:
|
|
1057
1073
|
pass
|
|
1058
1074
|
|
|
1059
|
-
# Delete all blobs under the prefix
|
|
1060
|
-
for blob in children:
|
|
1061
|
-
self._get_blob_client(blob_name=blob.name).delete_blob()
|
|
1062
|
-
deleted.append(blob.name)
|
|
1063
|
-
|
|
1064
1075
|
if not deleted:
|
|
1065
1076
|
raise FileNotFoundError(f"Blob not found: {blob_name}")
|
|
1066
1077
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|