Skip to content

Commit 81d33cb

Browse files
committed
Add object_store parameter to register/read file methods
Add an optional object_store parameter to register_parquet, read_parquet, register_csv, read_csv, register_json, read_json, register_avro, read_avro, register_arrow, and read_arrow methods. When provided, the store is automatically registered for the URL scheme and host parsed from the path, removing the need to manually call register_object_store separately. This enables thread-safe cloud credential handling without os.environ mutation. Includes unit tests (mock-based) for URL parsing logic and integration tests using LocalFileSystem for end-to-end validation. Closes #1624
1 parent 5b341d3 commit 81d33cb

2 files changed

Lines changed: 439 additions & 3 deletions

File tree

python/datafusion/context.py

Lines changed: 153 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
from typing_extensions import deprecated # Python 3.12
5555

5656

57+
from urllib.parse import urlparse
58+
5759
import pyarrow as pa
5860

5961
from datafusion.catalog import (
@@ -611,6 +613,34 @@ def deregister_object_store(self, schema: str, host: str | None = None) -> None:
611613
"""
612614
self.ctx.deregister_object_store(schema, host)
613615

616+
def _register_object_store_for_path(
617+
self, path: str | pathlib.Path, store: Any
618+
) -> None:
619+
"""Parse a URL path and register the given object store for its scheme and host.
620+
621+
This is a convenience helper used by methods like
622+
:py:meth:`register_parquet` and :py:meth:`read_parquet` to
623+
automatically register an object store when an ``object_store``
624+
parameter is provided.
625+
626+
Args:
627+
path: A URL-style path (e.g. ``"s3://bucket/key.parquet"``).
628+
store: An object store instance to register.
629+
630+
Raises:
631+
ValueError: If the path does not contain a recognized URL scheme.
632+
"""
633+
parsed = urlparse(str(path))
634+
if not parsed.scheme or not parsed.netloc:
635+
msg = (
636+
f"Cannot determine object store URL from path {path!r}. "
637+
"The path must use a URL scheme (e.g. 's3://bucket/key')."
638+
)
639+
raise ValueError(msg)
640+
scheme = f"{parsed.scheme}://"
641+
host = parsed.netloc
642+
self.register_object_store(scheme, store, host=host)
643+
614644
def register_listing_table(
615645
self,
616646
name: str,
@@ -1028,6 +1058,7 @@ def register_parquet(
10281058
skip_metadata: bool = True,
10291059
schema: pa.Schema | None = None,
10301060
file_sort_order: Sequence[Sequence[SortKey]] | None = None,
1061+
object_store: Any | None = None,
10311062
) -> None:
10321063
"""Register a Parquet file as a table.
10331064
@@ -1049,7 +1080,41 @@ def register_parquet(
10491080
file_sort_order: Sort order for the file. Each sort key can be
10501081
specified as a column name (``str``), an expression
10511082
(``Expr``), or a ``SortExpr``.
1052-
"""
1083+
object_store: A pre-configured object store instance (e.g.
1084+
:py:class:`~datafusion.object_store.AmazonS3`,
1085+
:py:class:`~datafusion.object_store.GoogleCloud`,
1086+
:py:class:`~datafusion.object_store.MicrosoftAzure`) to use
1087+
for accessing the file. When provided, the store is
1088+
automatically registered for the URL scheme and host parsed
1089+
from ``path``, removing the need to call
1090+
:py:meth:`register_object_store` separately. This is
1091+
especially useful in multi-threaded environments where
1092+
setting credentials via ``os.environ`` is not thread-safe.
1093+
1094+
Examples:
1095+
Register a local Parquet file:
1096+
1097+
>>> import datafusion
1098+
>>> ctx = datafusion.SessionContext()
1099+
>>> ctx.register_parquet("my_table", "data.parquet")
1100+
1101+
Register from S3 with inline credentials (thread-safe):
1102+
1103+
>>> from datafusion.object_store import AmazonS3 # doctest: +SKIP
1104+
>>> store = AmazonS3(
1105+
... bucket_name="my-bucket",
1106+
... region="us-east-1",
1107+
... access_key_id="...",
1108+
... secret_access_key="...",
1109+
... ) # doctest: +SKIP
1110+
>>> ctx.register_parquet(
1111+
... "my_table",
1112+
... "s3://my-bucket/data.parquet",
1113+
... object_store=store,
1114+
... ) # doctest: +SKIP
1115+
"""
1116+
if object_store is not None:
1117+
self._register_object_store_for_path(path, object_store)
10531118
if table_partition_cols is None:
10541119
table_partition_cols = []
10551120
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
@@ -1075,6 +1140,7 @@ def register_csv(
10751140
file_extension: str = ".csv",
10761141
file_compression_type: str | None = None,
10771142
options: CsvReadOptions | None = None,
1143+
object_store: Any | None = None,
10781144
) -> None:
10791145
"""Register a CSV file as a table.
10801146
@@ -1096,7 +1162,14 @@ def register_csv(
10961162
file_compression_type: File compression type.
10971163
options: Set advanced options for CSV reading. This cannot be
10981164
combined with any of the other options in this method.
1099-
"""
1165+
object_store: A pre-configured object store instance to use for
1166+
accessing the file. When provided, the store is automatically
1167+
registered for the URL scheme and host parsed from ``path``.
1168+
"""
1169+
if object_store is not None:
1170+
# For list paths, register from the first entry
1171+
register_path = path[0] if isinstance(path, list) else path
1172+
self._register_object_store_for_path(register_path, object_store)
11001173
if options is not None and (
11011174
schema is not None
11021175
or not has_header
@@ -1143,6 +1216,7 @@ def register_json(
11431216
file_extension: str = ".json",
11441217
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
11451218
file_compression_type: str | None = None,
1219+
object_store: Any | None = None,
11461220
) -> None:
11471221
"""Register a JSON file as a table.
11481222
@@ -1159,7 +1233,12 @@ def register_json(
11591233
selected for data input.
11601234
table_partition_cols: Partition columns.
11611235
file_compression_type: File compression type.
1236+
object_store: A pre-configured object store instance to use for
1237+
accessing the file. When provided, the store is automatically
1238+
registered for the URL scheme and host parsed from ``path``.
11621239
"""
1240+
if object_store is not None:
1241+
self._register_object_store_for_path(path, object_store)
11631242
if table_partition_cols is None:
11641243
table_partition_cols = []
11651244
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
@@ -1180,6 +1259,7 @@ def register_avro(
11801259
schema: pa.Schema | None = None,
11811260
file_extension: str = ".avro",
11821261
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
1262+
object_store: Any | None = None,
11831263
) -> None:
11841264
"""Register an Avro file as a table.
11851265
@@ -1192,7 +1272,12 @@ def register_avro(
11921272
schema: The data source schema.
11931273
file_extension: File extension to select.
11941274
table_partition_cols: Partition columns.
1275+
object_store: A pre-configured object store instance to use for
1276+
accessing the file. When provided, the store is automatically
1277+
registered for the URL scheme and host parsed from ``path``.
11951278
"""
1279+
if object_store is not None:
1280+
self._register_object_store_for_path(path, object_store)
11961281
if table_partition_cols is None:
11971282
table_partition_cols = []
11981283
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
@@ -1205,6 +1290,7 @@ def register_arrow(
12051290
schema: pa.Schema | None = None,
12061291
file_extension: str = ".arrow",
12071292
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
1293+
object_store: Any | None = None,
12081294
) -> None:
12091295
"""Register an Arrow IPC file as a table.
12101296
@@ -1217,6 +1303,9 @@ def register_arrow(
12171303
schema: The data source schema.
12181304
file_extension: File extension to select.
12191305
table_partition_cols: Partition columns.
1306+
object_store: A pre-configured object store instance to use for
1307+
accessing the file. When provided, the store is automatically
1308+
registered for the URL scheme and host parsed from ``path``.
12201309
12211310
Examples:
12221311
>>> import tempfile, os
@@ -1271,6 +1360,8 @@ def register_arrow(
12711360
30
12721361
]
12731362
"""
1363+
if object_store is not None:
1364+
self._register_object_store_for_path(path, object_store)
12741365
if table_partition_cols is None:
12751366
table_partition_cols = []
12761367
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
@@ -1690,6 +1781,7 @@ def read_json(
16901781
file_extension: str = ".json",
16911782
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
16921783
file_compression_type: str | None = None,
1784+
object_store: Any | None = None,
16931785
) -> DataFrame:
16941786
"""Read a line-delimited JSON data source.
16951787
@@ -1702,10 +1794,15 @@ def read_json(
17021794
selected for data input.
17031795
table_partition_cols: Partition columns.
17041796
file_compression_type: File compression type.
1797+
object_store: A pre-configured object store instance to use for
1798+
accessing the file. When provided, the store is automatically
1799+
registered for the URL scheme and host parsed from ``path``.
17051800
17061801
Returns:
17071802
DataFrame representation of the read JSON files.
17081803
"""
1804+
if object_store is not None:
1805+
self._register_object_store_for_path(path, object_store)
17091806
if table_partition_cols is None:
17101807
table_partition_cols = []
17111808
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
@@ -1731,6 +1828,7 @@ def read_csv(
17311828
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
17321829
file_compression_type: str | None = None,
17331830
options: CsvReadOptions | None = None,
1831+
object_store: Any | None = None,
17341832
) -> DataFrame:
17351833
"""Read a CSV data source.
17361834
@@ -1750,10 +1848,16 @@ def read_csv(
17501848
file_compression_type: File compression type.
17511849
options: Set advanced options for CSV reading. This cannot be
17521850
combined with any of the other options in this method.
1851+
object_store: A pre-configured object store instance to use for
1852+
accessing the file. When provided, the store is automatically
1853+
registered for the URL scheme and host parsed from ``path``.
17531854
17541855
Returns:
17551856
DataFrame representation of the read CSV files
17561857
"""
1858+
if object_store is not None:
1859+
register_path = path[0] if isinstance(path, list) else path
1860+
self._register_object_store_for_path(register_path, object_store)
17571861
if options is not None and (
17581862
schema is not None
17591863
or not has_header
@@ -1803,6 +1907,7 @@ def read_parquet(
18031907
skip_metadata: bool = True,
18041908
schema: pa.Schema | None = None,
18051909
file_sort_order: Sequence[Sequence[SortKey]] | None = None,
1910+
object_store: Any | None = None,
18061911
) -> DataFrame:
18071912
"""Read a Parquet source into a :py:class:`~datafusion.dataframe.Dataframe`.
18081913
@@ -1822,10 +1927,43 @@ def read_parquet(
18221927
file_sort_order: Sort order for the file. Each sort key can be
18231928
specified as a column name (``str``), an expression
18241929
(``Expr``), or a ``SortExpr``.
1930+
object_store: A pre-configured object store instance (e.g.
1931+
:py:class:`~datafusion.object_store.AmazonS3`,
1932+
:py:class:`~datafusion.object_store.GoogleCloud`,
1933+
:py:class:`~datafusion.object_store.MicrosoftAzure`) to use
1934+
for accessing the file. When provided, the store is
1935+
automatically registered for the URL scheme and host parsed
1936+
from ``path``, removing the need to call
1937+
:py:meth:`register_object_store` separately. This is
1938+
especially useful in multi-threaded environments where
1939+
setting credentials via ``os.environ`` is not thread-safe.
18251940
18261941
Returns:
18271942
DataFrame representation of the read Parquet files
1828-
"""
1943+
1944+
Examples:
1945+
Read a local Parquet file:
1946+
1947+
>>> import datafusion
1948+
>>> ctx = datafusion.SessionContext()
1949+
>>> df = ctx.read_parquet("data.parquet") # doctest: +SKIP
1950+
1951+
Read from S3 with inline credentials (thread-safe):
1952+
1953+
>>> from datafusion.object_store import AmazonS3 # doctest: +SKIP
1954+
>>> store = AmazonS3(
1955+
... bucket_name="my-bucket",
1956+
... region="us-east-1",
1957+
... access_key_id="...",
1958+
... secret_access_key="...",
1959+
... ) # doctest: +SKIP
1960+
>>> df = ctx.read_parquet(
1961+
... "s3://my-bucket/data.parquet",
1962+
... object_store=store,
1963+
... ) # doctest: +SKIP
1964+
"""
1965+
if object_store is not None:
1966+
self._register_object_store_for_path(path, object_store)
18291967
if table_partition_cols is None:
18301968
table_partition_cols = []
18311969
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
@@ -1848,6 +1986,7 @@ def read_avro(
18481986
schema: pa.Schema | None = None,
18491987
file_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
18501988
file_extension: str = ".avro",
1989+
object_store: Any | None = None,
18511990
) -> DataFrame:
18521991
"""Create a :py:class:`DataFrame` for reading Avro data source.
18531992
@@ -1856,10 +1995,15 @@ def read_avro(
18561995
schema: The data source schema.
18571996
file_partition_cols: Partition columns.
18581997
file_extension: File extension to select.
1998+
object_store: A pre-configured object store instance to use for
1999+
accessing the file. When provided, the store is automatically
2000+
registered for the URL scheme and host parsed from ``path``.
18592001
18602002
Returns:
18612003
DataFrame representation of the read Avro file
18622004
"""
2005+
if object_store is not None:
2006+
self._register_object_store_for_path(path, object_store)
18632007
if file_partition_cols is None:
18642008
file_partition_cols = []
18652009
file_partition_cols = _convert_table_partition_cols(file_partition_cols)
@@ -1873,6 +2017,7 @@ def read_arrow(
18732017
schema: pa.Schema | None = None,
18742018
file_extension: str = ".arrow",
18752019
file_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
2020+
object_store: Any | None = None,
18762021
) -> DataFrame:
18772022
"""Create a :py:class:`DataFrame` for reading an Arrow IPC data source.
18782023
@@ -1881,6 +2026,9 @@ def read_arrow(
18812026
schema: The data source schema.
18822027
file_extension: File extension to select.
18832028
file_partition_cols: Partition columns.
2029+
object_store: A pre-configured object store instance to use for
2030+
accessing the file. When provided, the store is automatically
2031+
registered for the URL scheme and host parsed from ``path``.
18842032
18852033
Returns:
18862034
DataFrame representation of the read Arrow IPC file.
@@ -1932,6 +2080,8 @@ def read_arrow(
19322080
3
19332081
]
19342082
"""
2083+
if object_store is not None:
2084+
self._register_object_store_for_path(path, object_store)
19352085
if file_partition_cols is None:
19362086
file_partition_cols = []
19372087
file_partition_cols = _convert_table_partition_cols(file_partition_cols)

0 commit comments

Comments
 (0)