Repository: ibis-project/ibis

Issue:
bug: cannot read from cloud storage
### What happened?

for the `ibis.read_*` functions, cloud storage should be supported via fsspec in the underlying libraries. the main issue seems to be the "normalize_filename()" calls, incorrectly editing the URI

a simple example:

```python
import ibis
import polars as pl

ibis.set_backend("polars")

# this is fine
t = pl.read_parquet("az://holidaydatacontainer/Processed/*.parquet", storage_options={"account_name": "azureopendatastorage"})

# this fails
t2 = ibis.read_parquet("az://holidaydatacontainer/Processed/*.parquet", storage_options={"account_name": "azureopendatastorage"})
```

the error is pretty obvious:

```
FileNotFoundError: No such file or directory: /Users/cody/repos/ibis/az:/holidaydatacontainer/Processed/*.parquet
```

this is coming from the `normalize_filename(path)` calls. after further inspection this seems like an incredibly easy fix, PR incoming...


(separately, I'm having an issue with GCS but we can discuss that elsewhere)

### What version of ibis are you using?

master branch

### What backend(s) are you using, if any?

duckdb, polars, etc.

### Relevant log output

```sh
-----------------------------------------------------------------
FileNotFoundError               Traceback (most recent call last)
Cell In[5], line 1
----> 1 t2 = ibis.read_parquet("az://holidaydatacontainer/Processed/*.parquet", storage_options={"account_name": "azureopendatastorage"})

File ~/repos/ibis/ibis/expr/api.py:963, in read_parquet(sources, table_name, **kwargs)
    960 from ibis.config import _default_backend
    962 con = _default_backend()
--> 963 return con.read_parquet(sources, table_name=table_name, **kwargs)

File ~/repos/ibis/ibis/backends/polars/__init__.py:229, in Backend.read_parquet(self, path, table_name, **kwargs)
    227 path = normalize_filename(path)
    228 table_name = table_name or gen_name("read_parquet")
--> 229 self._add_table(table_name, pl.scan_parquet(path, **kwargs))
    230 return self.table(table_name)

File ~/repos/ibis/venv/lib/python3.11/site-packages/polars/io/parquet/functions.py:210, in scan_parquet(source, n_rows, cache, parallel, rechunk, row_count_name, row_count_offset, storage_options, low_memory, use_statistics)
    207 if isinstance(source, (str, Path)):
    208     source = normalise_filepath(source)
--> 210 return pl.LazyFrame._scan_parquet(
    211     source,
    212     n_rows=n_rows,
    213     cache=cache,
    214     parallel=parallel,
    215     rechunk=rechunk,
    216     row_count_name=row_count_name,
    217     row_count_offset=row_count_offset,
    218     storage_options=storage_options,
    219     low_memory=low_memory,
    220     use_statistics=use_statistics,
    221 )

File ~/repos/ibis/venv/lib/python3.11/site-packages/polars/lazyframe/frame.py:397, in LazyFrame._scan_parquet(cls, source, n_rows, cache, parallel, rechunk, row_count_name, row_count_offset, storage_options, low_memory, use_statistics)
    395 # try fsspec scanner
    396 if not _is_local_file(source):
--> 397     scan = _scan_parquet_fsspec(source, storage_options)
    398     if n_rows:
    399         scan = scan.head(n_rows)

File ~/repos/ibis/venv/lib/python3.11/site-packages/polars/io/par
quet/anonymous_scan.py:24, in _scan_parquet_fsspec(source, storage_options)
     22 storage_options = storage_options or {}
     23 with _prepare_file_arg(source, **storage_options) as data:
---> 24     schema = polars.io.parquet.read_parquet_schema(data)
     26 return pl.LazyFrame._scan_python_function(schema, func_serialized)

File ~/repos/ibis/venv/lib/python3.11/site-packages/polars/io/parquet/functions.py:155, in read_parquet_schema(source)
    152 if isinstance(source, (str, Path)):
    153     source = normalise_filepath(source)
--> 155 return _read_parquet_schema(source)

FileNotFoundError: No such file or directory: /Users/cody/repos/ibis/az:/holidaydatacontainer/Processed/*.parquet
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:

Resolving patch:
diff --git a/ibis/util.py b/ibis/util.py
--- a/ibis/util.py
+++ b/ibis/util.py
@@ -530,7 +530,7 @@ def _removeprefix(text, prefix):
         source = _removeprefix(source, f"{prefix}://")
 
     def _absolufy_paths(name):
-        if not name.startswith(("http", "s3")):
+        if not name.startswith(("http", "s3", "az", "abfs", "abfss", "adl", "gs")):
             return os.path.abspath(name)
         return name
 


Test patch: