Skip to content

Commit f55e6ee

Browse files
authored
feat: Better unsupported file error (#63)
1 parent c213c9f commit f55e6ee

13 files changed

Lines changed: 11231 additions & 9465 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ jobs:
1818
- name: Checkout repository
1919
uses: actions/checkout@v4
2020
- name: Setup Pixi
21-
uses: prefix-dev/setup-pixi@v0.9.3
21+
uses: prefix-dev/setup-pixi@v0.9.6
2222
with:
23-
pixi-version: v0.62.2
23+
pixi-version: v0.70.2
2424
cache: true
25+
cache-write: ${{ github.event_name == 'push' && github.ref_name == 'main' }}
2526
manifest-path: pyproject.toml
2627
- name: Run checks and formatters through pre-commit
2728
run: pixi run pre-commit run --all-files
@@ -33,10 +34,11 @@ jobs:
3334
- name: Checkout repository
3435
uses: actions/checkout@v4
3536
- name: Setup Pixi
36-
uses: prefix-dev/setup-pixi@v0.9.3
37+
uses: prefix-dev/setup-pixi@v0.9.6
3738
with:
38-
pixi-version: v0.62.2
39+
pixi-version: v0.70.2
3940
cache: true
41+
cache-write: ${{ github.event_name == 'push' && github.ref_name == 'main' }}
4042
manifest-path: pyproject.toml
4143
- name: Test server extension
4244
run: pixi run test-pytest
@@ -88,6 +90,7 @@ jobs:
8890
name: Integration tests
8991
needs: test-unit
9092
runs-on: ubuntu-latest
93+
timeout-minutes: 20
9194
env:
9295
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers
9396
steps:
@@ -118,13 +121,13 @@ jobs:
118121
- name: Install browser
119122
run: |
120123
set -eux
121-
jlpm playwright install-deps
122-
jlpm playwright install chromium
124+
npx playwright install-deps
125+
npx playwright install chromium
123126
working-directory: ui-tests
124127
- name: Execute integration tests
125128
working-directory: ui-tests
126129
run: |
127-
jlpm playwright test
130+
npx playwright test
128131
- name: Upload Playwright Test report
129132
if: always()
130133
uses: actions/upload-artifact@v4

‎.github/workflows/enforce-label.yml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ jobs:
2020
# pixi/commitlint from target branch.
2121
ref: ${{ github.event.pull_request.base.sha }}
2222
- name: Setup Pixi
23-
uses: prefix-dev/setup-pixi@v0.9.3
23+
uses: prefix-dev/setup-pixi@v0.9.6
2424
with:
25-
pixi-version: v0.62.2
25+
pixi-version: v0.70.2
2626
cache: true
27+
cache-write: ${{ github.event_name == 'push' && github.ref_name == 'main' }}
2728
manifest-path: pyproject.toml
2829
- name: Verify conventional commit conventions on PR title
2930
id: commitlint

‎.pre-commit-config.yaml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@ repos:
4444
entry: pixi run fmt-biome""
4545
language: system
4646
types_or: [javascript, jsx, ts, tsx, json, css]
47+
exclude: "(^|/)package\\.json$"
4748

4849
# Linters
4950
- id: pixi-biome
5051
name: Run biome check through pixi
5152
entry: pixi run check-biome
5253
language: system
5354
types_or: [javascript, jsx, ts, tsx, json]
55+
exclude: "(^|/)package\\.json$"
5456
- id: pixi-typescript
5557
name: Run typescript checks through pixi
5658
entry: pixi run check-typescript

‎arbalister/arrow.py‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import codecs
2+
import importlib.util
23
import pathlib
34
from typing import Any, Callable
45

@@ -9,6 +10,19 @@
910

1011
ReadCallable = Callable[..., dn.DataFrame]
1112

13+
# Optional third-party modules required to read each format. Formats not listed here
14+
# rely only on the core dependencies and are always readable.
15+
_READER_REQUIREMENTS: dict[ff.FileFormat, tuple[str, ...]] = {
16+
ff.FileFormat.Sqlite: ("adbc_driver_manager", "adbc_driver_sqlite"),
17+
}
18+
19+
20+
def missing_requirements(format: ff.FileFormat) -> list[str]:
21+
"""Return the modules required to read the format that are not installed."""
22+
return [
23+
module for module in _READER_REQUIREMENTS.get(format, ()) if importlib.util.find_spec(module) is None
24+
]
25+
1226

1327
def _read_csv(
1428
ctx: dn.SessionContext, path: str | pathlib.Path, delimiter: str, **kwargs: dict[str, Any]
@@ -22,7 +36,7 @@ def _read_ipc(ctx: dn.SessionContext, path: str | pathlib.Path, **kwargs: dict[s
2236
import pyarrow.feather
2337

2438
# table = pyarrow.feather.read_table(path, {**{"memory_map": True}, **kwargs})
25-
table = pyarrow.feather.read_table(path, **kwargs)
39+
table = pyarrow.feather.read_table(path, **kwargs) # type: ignore[no-untyped-call]
2640
return ctx.from_arrow(table)
2741

2842

@@ -31,7 +45,7 @@ def _read_orc(ctx: dn.SessionContext, path: str | pathlib.Path, **kwargs: dict[s
3145
# Evolution for native datafusion reader
3246
import pyarrow.orc
3347

34-
table = pyarrow.orc.read_table(path, **kwargs)
48+
table = pyarrow.orc.read_table(path, **kwargs) # type: ignore[no-untyped-call]
3549
return ctx.from_arrow(table)
3650

3751

@@ -128,11 +142,11 @@ def write_csv(
128142
memory_pool: pa.MemoryPool | None = None,
129143
**kwargs: dict[str, Any],
130144
) -> None:
131-
pyarrow.csv.write_csv(
145+
pyarrow.csv.write_csv( # type: ignore[attr-defined]
132146
data=data,
133147
output_file=str(output_file),
134148
memory_pool=memory_pool,
135-
write_options=pyarrow.csv.WriteOptions(**kwargs),
149+
write_options=pyarrow.csv.WriteOptions(**kwargs), # type: ignore[attr-defined]
136150
)
137151

138152
out = write_csv

‎arbalister/routes.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,42 @@ async def get(self, path: str) -> None:
242242
await self.finish(dataclasses.asdict(no_response))
243243

244244

245+
@dataclasses.dataclass(frozen=True, slots=True)
246+
class FileSupportResponse:
247+
"""Whether a file can be read, and why not when it cannot."""
248+
249+
supported: bool
250+
reason: str | None = None
251+
252+
253+
class FileSupportRouteHandler(BaseRouteHandler):
254+
"""A handler to check whether a file path is supported before opening it."""
255+
256+
@tornado.web.authenticated
257+
async def get(self, path: str) -> None:
258+
"""HTTP GET return whether the file is supported."""
259+
file = self.data_file(path)
260+
261+
try:
262+
file_format = ff.FileFormat.from_filename(file)
263+
except ValueError as error:
264+
response = FileSupportResponse(supported=False, reason=str(error))
265+
await self.finish(dataclasses.asdict(response))
266+
return
267+
268+
missing = abw.missing_requirements(file_format)
269+
if missing:
270+
reason = (
271+
f"Reading {file_format} files requires additional package(s) that are not "
272+
f"installed: {', '.join(missing)}."
273+
)
274+
response = FileSupportResponse(supported=False, reason=reason)
275+
else:
276+
response = FileSupportResponse(supported=True)
277+
278+
await self.finish(dataclasses.asdict(response))
279+
280+
245281
def make_datafusion_config() -> dn.SessionConfig:
246282
"""Return the datafusion config."""
247283
config = (
@@ -270,6 +306,11 @@ def setup_route_handlers(web_app: jupyter_server.serverapp.ServerWebApplication)
270306
FileInfoRouteHandler,
271307
{"context": context},
272308
),
309+
(
310+
url_path_join(base_url, r"arbalister/file/supported/([^?]*)"),
311+
FileSupportRouteHandler,
312+
{"context": context},
313+
),
273314
]
274315

275316
web_app.add_handlers(host_pattern, handlers) # type: ignore[no-untyped-call]

‎arbalister/tests/test_routes.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,37 @@ async def test_stats_route(
229229
assert table.schema.names == full_table.schema.names
230230

231231

232+
async def test_file_supported_route(
233+
jp_fetch: JpFetch,
234+
table_file: pathlib.Path,
235+
) -> None:
236+
"""Test that a readable file is reported as supported."""
237+
response = await jp_fetch("arbalister/file/supported/", str(table_file))
238+
239+
assert response.code == 200
240+
assert response.headers["Content-Type"] == "application/json; charset=UTF-8"
241+
242+
payload = json.loads(response.body)
243+
assert payload["supported"] is True
244+
assert payload["reason"] is None
245+
246+
247+
async def test_file_supported_route_unknown_type(
248+
jp_fetch: JpFetch,
249+
jp_root_dir: pathlib.Path,
250+
) -> None:
251+
"""Test that an unknown file type is reported as unsupported with a reason."""
252+
unknown = jp_root_dir / "data.unknown_ext"
253+
unknown.write_text("nothing")
254+
255+
response = await jp_fetch("arbalister/file/supported/", "data.unknown_ext")
256+
257+
assert response.code == 200
258+
payload = json.loads(response.body)
259+
assert payload["supported"] is False
260+
assert payload["reason"]
261+
262+
232263
async def test_file_info_route_sqlite(
233264
jp_fetch: JpFetch,
234265
table_file: pathlib.Path,

‎data/generate.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ def _generate_coordinate_table_slice(
5151
.with_columns(
5252
*[
5353
dnf.concat(
54-
dn.lit("("), # type: ignore[no-untyped-call]
54+
dn.lit("("),
5555
dnf.col("row"),
56-
dn.lit(f", {j})"), # type: ignore[no-untyped-call]
56+
dn.lit(f", {j})"),
5757
).alias(f"col_{j}")
5858
for j in range(num_cols)
5959
]
@@ -75,7 +75,7 @@ def _sink_coordinate_table(
7575
print(f"Generating {row_start}", flush=True)
7676
row_end = min(row_start + chunk_size, num_rows)
7777
table = _generate_coordinate_table_slice(row_start, row_end, num_cols=num_cols, ctx=ctx)
78-
writer.write(table)
78+
writer.write(table) # type: ignore[no-untyped-call]
7979

8080

8181
def sink_coordinate_table(
@@ -87,9 +87,9 @@ def sink_coordinate_table(
8787
ctx = dn.SessionContext()
8888
print("Generating schema", flush=True)
8989
schema = _generate_coordinate_table_slice(0, 1, num_cols=num_cols, ctx=ctx).schema
90-
writer = paq.ParquetWriter(path, schema)
90+
writer = paq.ParquetWriter(path, schema) # type: ignore[no-untyped-call]
9191
_sink_coordinate_table(num_rows=num_rows, num_cols=num_cols, writer=writer, ctx=ctx)
92-
writer.close()
92+
writer.close() # type: ignore[no-untyped-call]
9393

9494

9595
def configure_command_single(cmd: argparse.ArgumentParser) -> argparse.ArgumentParser:

0 commit comments

Comments
 (0)