# pyright: reportPrivateUsage=false
import asyncio
from collections.abc import AsyncIterator, Iterator
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast, overload
from urllib.parse import urlparse
from mypy_extensions import mypyc_attr
from sqlspec.storage._arrow_stream import iter_parquet_row_groups, validate_parquet_stream_options
from sqlspec.storage._paths import resolve_storage_path
from sqlspec.storage._utils import _log_storage_event, import_pyarrow_parquet
from sqlspec.storage.backends.base import AsyncArrowBatchIterator, AsyncThreadedBytesIterator
from sqlspec.storage.errors import execute_sync_storage_operation
from sqlspec.utils.module_loader import ensure_fsspec
from sqlspec.utils.sync_tools import async_
if TYPE_CHECKING:
from sqlspec.typing import ArrowRecordBatch, ArrowTable
__all__ = ("FSSpecBackend",)
_OBJECT_STORE_PROTOCOLS = {"s3", "gs", "az", "gcs"}
@mypyc_attr(allow_interpreted_subclasses=True)
class FSSpecBackend:
"""Storage backend using fsspec.
Implements ObjectStoreProtocol using fsspec for various protocols
including HTTP, HTTPS, FTP, and cloud storage services.
All synchronous methods use the *_sync suffix for consistency with async methods.
"""
__slots__ = ("_fs_uri", "base_path", "fs", "protocol")
backend_type: ClassVar[str] = "fsspec"
def __init__(self, uri: str, **kwargs: Any) -> None:
"""Initialize the fsspec-backed storage backend.
Args:
uri: Filesystem URI (protocol://path).
**kwargs: Additional fsspec configuration options, including an optional base_path.
For cloud URIs (S3/GS/Azure) and file:// URIs, we derive a default base_path from the
URI path when no explicit base_path is provided. When both URI and base_path are provided,
they are combined (base_path is appended to URI-derived path).
"""
ensure_fsspec()
import fsspec
explicit_base_path = kwargs.pop("base_path", "")
if "://" in uri:
self.protocol = uri.split("://", maxsplit=1)[0]
self._fs_uri = uri
if self.protocol in _OBJECT_STORE_PROTOCOLS:
parsed = urlparse(uri)
if parsed.netloc:
uri_base_path = parsed.netloc
if parsed.path and parsed.path != "/":
uri_base_path = f"{uri_base_path}{parsed.path}"
# Combine URI path with explicit base_path if both provided
if explicit_base_path:
uri_base_path = f"{uri_base_path.rstrip('/')}/{explicit_base_path.lstrip('/')}"
explicit_base_path = uri_base_path
elif self.protocol == "file":
parsed = urlparse(uri)
if parsed.path and parsed.path != "/":
# For file protocol, keep the path as-is (preserve leading slash for absolute paths)
uri_base_path = parsed.path
# Combine URI path with explicit base_path if both provided
if explicit_base_path:
uri_base_path = f"{uri_base_path.rstrip('/')}/{explicit_base_path.lstrip('/')}"
explicit_base_path = uri_base_path
else:
self.protocol = uri
self._fs_uri = f"{uri}://"
self.base_path = explicit_base_path.rstrip("/") if explicit_base_path else ""
self.fs = fsspec.filesystem(self.protocol, **kwargs)
_log_storage_event(
"storage.backend.ready",
backend_type=self.backend_type,
protocol=self.protocol,
operation="init",
path=self._fs_uri,
)
super().__init__()