fsspec-databricks 0.1.10__tar.gz → 0.2.1__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.
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/.gitignore +0 -3
- fsspec_databricks-0.2.1/AUTHENTICATION.md +81 -0
- fsspec_databricks-0.2.1/DEVELOPMENT.md +446 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/LICENSE +1 -1
- fsspec_databricks-0.2.1/PKG-INFO +129 -0
- fsspec_databricks-0.2.1/README.md +104 -0
- fsspec_databricks-0.2.1/SECURITY.md +30 -0
- fsspec_databricks-0.2.1/USAGE.md +301 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/pyproject.toml +16 -3
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/src/fsspec_databricks/base.py +14 -11
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/src/fsspec_databricks/file.py +15 -4
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/src/fsspec_databricks/local.py +10 -5
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/src/fsspec_databricks/path.py +26 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/src/fsspec_databricks/spec.py +128 -42
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/src/fsspec_databricks/utils.py +4 -4
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/src/fsspec_databricks/volume.py +198 -91
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/tests/conftest.py +2 -1
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/tests/dummy_api.py +16 -4
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/tests/test_file.py +10 -2
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/tests/test_path.py +20 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/tests/test_spec_fs.py +81 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/tests/test_utils.py +2 -2
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/tests/test_volume.py +51 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/tests/test_volume_fs.py +121 -16
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/tests/test_workspace_fs.py +7 -7
- fsspec_databricks-0.1.10/.github/dependabot.yml +0 -11
- fsspec_databricks-0.1.10/.github/workflows/check.yaml +0 -33
- fsspec_databricks-0.1.10/.github/workflows/on_push.yaml +0 -86
- fsspec_databricks-0.1.10/.github/workflows/on_release.yaml +0 -21
- fsspec_databricks-0.1.10/.github/workflows/publish.yaml +0 -43
- fsspec_databricks-0.1.10/.github/workflows/test.yaml +0 -139
- fsspec_databricks-0.1.10/.pre-commit-config.yaml +0 -20
- fsspec_databricks-0.1.10/DEVELOPMENT.md +0 -71
- fsspec_databricks-0.1.10/PKG-INFO +0 -285
- fsspec_databricks-0.1.10/README.md +0 -259
- fsspec_databricks-0.1.10/USAGE.md +0 -319
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/src/fsspec_databricks/__init__.py +0 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/src/fsspec_databricks/dbfs.py +0 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/src/fsspec_databricks/error.py +0 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/src/fsspec_databricks/py.typed +0 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/src/fsspec_databricks/workspace.py +0 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/tests/__init__.py +0 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/tests/test_base_fs.py +0 -0
- {fsspec_databricks-0.1.10 → fsspec_databricks-0.2.1}/tests/utils.py +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
`fsspec-databricks` uses Databricks Unified Authentication, through the Databricks Python SDK. Its parameters and
|
|
4
|
+
environment variables are in the
|
|
5
|
+
[Databricks Python SDK documentation](https://databricks-sdk-py.readthedocs.io/en/latest/authentication.html).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Default
|
|
10
|
+
|
|
11
|
+
With Databricks Unified Authentication configured, e.g. a default profile, no parameter is needed:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from fsspec_databricks import DatabricksFileSystem
|
|
15
|
+
|
|
16
|
+
fs = DatabricksFileSystem()
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Constructor parameters
|
|
22
|
+
|
|
23
|
+
`DatabricksFileSystem` takes the parameters of the SDK's `WorkspaceClient`:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
# Personal access token
|
|
27
|
+
fs = DatabricksFileSystem(host=host_url, token=access_token)
|
|
28
|
+
|
|
29
|
+
# Another profile
|
|
30
|
+
fs = DatabricksFileSystem(profile="production")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Environment variables
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export DATABRICKS_CONFIG_PROFILE=production
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
fs = DatabricksFileSystem() # uses the "production" profile
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## fsspec configuration
|
|
48
|
+
|
|
49
|
+
[fsspec's configuration](https://filesystem-spec.readthedocs.io/en/latest/features.html#configuration) sets default
|
|
50
|
+
parameters for the `dbfs` protocol:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import fsspec
|
|
54
|
+
|
|
55
|
+
fsspec.config.conf["dbfs"] = {"profile": "production"}
|
|
56
|
+
|
|
57
|
+
fs = DatabricksFileSystem() # uses the "production" profile
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or with environment variables named `FSSPEC_DBFS_<PARAMETER>`, read when fsspec is imported:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export FSSPEC_DBFS_PROFILE=production
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## `WorkspaceClient`
|
|
69
|
+
|
|
70
|
+
`DatabricksFileSystem` can use a `WorkspaceClient` of yours, with its authentication:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from databricks.sdk import WorkspaceClient
|
|
74
|
+
|
|
75
|
+
client = WorkspaceClient(...)
|
|
76
|
+
|
|
77
|
+
fs = DatabricksFileSystem(client=client)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A file system created with a `WorkspaceClient` is generally not picklable, as `WorkspaceClient` objects are not. Use
|
|
81
|
+
another way to authenticate where file systems are pickled, e.g. with Dask.
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
## Layout
|
|
4
|
+
|
|
5
|
+
| Path | Contents |
|
|
6
|
+
|-------------------------------------|----------------------------------------------------------------------------------|
|
|
7
|
+
| `src/fsspec_databricks/__init__.py` | The public API: `DatabricksFileSystem` and `use()`. |
|
|
8
|
+
| `src/fsspec_databricks/spec.py` | `DatabricksFileSystem`, which routes each call to a backend. |
|
|
9
|
+
| `src/fsspec_databricks/base.py` | `AbstractDatabricksFileSystem`, the backends' base class. |
|
|
10
|
+
| `src/fsspec_databricks/volume.py` | `VolumeFileSystem`, Unity Catalog Volumes, and its file objects. |
|
|
11
|
+
| `src/fsspec_databricks/workspace.py` | `WorkspaceFileSystem`, Workspace files and notebooks, and its file object. |
|
|
12
|
+
| `src/fsspec_databricks/dbfs.py` | `DBFS`, the legacy DBFS. |
|
|
13
|
+
| `src/fsspec_databricks/local.py` | `DatabricksLocalFileSystem`, the local file system used inside a workspace. |
|
|
14
|
+
| `src/fsspec_databricks/file.py` | The file objects' base classes: concurrent ranges, and whole-file caches. |
|
|
15
|
+
| `src/fsspec_databricks/path.py` | Parsing `dbfs:/` URLs and POSIX paths, and choosing their backend. |
|
|
16
|
+
| `src/fsspec_databricks/error.py` | `OSError`s, and the mapping of the SDK's exceptions to them. |
|
|
17
|
+
| `src/fsspec_databricks/utils.py` | Timestamps and enums. |
|
|
18
|
+
| `tests/` | The tests, a file per module, and the fake Files API. |
|
|
19
|
+
|
|
20
|
+
The runtime dependencies are fsspec, databricks-sdk and aiohttp.
|
|
21
|
+
|
|
22
|
+
All Python commands go through [uv](https://docs.astral.sh/uv/).
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv sync
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Most tests run against a real Databricks workspace. They need its credentials and two work directories in it.
|
|
31
|
+
|
|
32
|
+
### Work directories
|
|
33
|
+
|
|
34
|
+
Create a folder in a Unity Catalog Volume and one in the Workspace files. Set their **POSIX paths**, without `dbfs:/`,
|
|
35
|
+
in these environment variables:
|
|
36
|
+
|
|
37
|
+
| Location | Environment variable | Default |
|
|
38
|
+
|----------------------|-----------------------------------------|--------------------------------------------------------------|
|
|
39
|
+
| Unity Catalog Volume | `FSSPEC_DATABRICKS_VOLUME_TEST_ROOT` | `/Volumes/fsspec_databricks_test/fsspec_dbx_test/test/local` |
|
|
40
|
+
| Workspace files | `FSSPEC_DATABRICKS_WORKSPACE_TEST_ROOT` | `/fsspec-databricks-test/local` |
|
|
41
|
+
|
|
42
|
+
The tests delete everything in these folders at the start of each session.
|
|
43
|
+
|
|
44
|
+
### Credentials
|
|
45
|
+
|
|
46
|
+
Configure [Databricks Unified Authentication](https://docs.databricks.com/aws/en/dev-tools/auth/unified-auth), e.g. a
|
|
47
|
+
default profile or `DATABRICKS_*` environment variables.
|
|
48
|
+
|
|
49
|
+
The tests load a `.env` file in the repository folder, through python-dotenv. It can hold the credentials and the two
|
|
50
|
+
variables above.
|
|
51
|
+
|
|
52
|
+
## Tests
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv run pytest # all tests
|
|
56
|
+
uvx ruff check . # lint
|
|
57
|
+
uvx ruff format --check . # formatting
|
|
58
|
+
uvx pre-commit run --all-files # the hooks
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| File | Tests of | Workspace |
|
|
62
|
+
|----------------------------|---------------------------------------------------|-----------|
|
|
63
|
+
| `test_spec_fs.py` | `DatabricksFileSystem` | Mostly |
|
|
64
|
+
| `test_base_fs.py` | `AbstractDatabricksFileSystem` | Yes |
|
|
65
|
+
| `test_volume_fs.py` | `VolumeFileSystem` | Mostly |
|
|
66
|
+
| `test_volume.py` | The Volume file objects | No |
|
|
67
|
+
| `test_workspace_fs.py` | `WorkspaceFileSystem` | Yes |
|
|
68
|
+
| `test_file.py` | The file objects' base classes | No |
|
|
69
|
+
| `test_path.py` | `path.py` | No |
|
|
70
|
+
| `test_utils.py` | `utils.py` | No |
|
|
71
|
+
|
|
72
|
+
Without credentials, run the files that need no workspace, and the tests of the others that take no workspace fixture.
|
|
73
|
+
|
|
74
|
+
The fixtures, in `tests/conftest.py`:
|
|
75
|
+
|
|
76
|
+
- `client`: a `WorkspaceClient` with the default authentication;
|
|
77
|
+
- `volume_test_root` and `workspace_test_root`: the work directories, emptied;
|
|
78
|
+
- `dummy_api` and `dummy_api_context`: the fake Files API, and its state;
|
|
79
|
+
- `client_event_loop` and `aiohttp_session`: an event loop in a thread, and a session on it;
|
|
80
|
+
- `dummy_table_parquet`: Parquet bytes of a generated table.
|
|
81
|
+
|
|
82
|
+
`tests/dummy_api.py` is a FastAPI app of the Files API's endpoints, served by Hypercorn on a free local port. It
|
|
83
|
+
covers downloads, single uploads, multipart and resumable uploads, presigned URLs and aborts.
|
|
84
|
+
`dummy_api_context.upload_mode` chooses between multipart and resumable uploads.
|
|
85
|
+
|
|
86
|
+
`tests/utils.py` has the helpers that test files share.
|
|
87
|
+
|
|
88
|
+
## CI
|
|
89
|
+
|
|
90
|
+
The workflows are in `.github/workflows/`:
|
|
91
|
+
|
|
92
|
+
| Workflow | Runs on | Does |
|
|
93
|
+
|-------------------|-----------------------|--------------------------------------------------------------------------|
|
|
94
|
+
| `on_push.yaml` | Pushes | `check.yaml`, then `test.yaml` for each Python version and workspace. |
|
|
95
|
+
| `check.yaml` | Called | ruff check and ruff format check, with the latest ruff. |
|
|
96
|
+
| `test.yaml` | Called, or by hand | pytest in one GitHub environment, optionally with coverage to Codecov. |
|
|
97
|
+
| `on_release.yaml` | Published releases | `publish.yaml` to PyPI. |
|
|
98
|
+
| `publish.yaml` | Called | `uv build` and `uv publish`. |
|
|
99
|
+
|
|
100
|
+
`on_push.yaml` runs these test jobs:
|
|
101
|
+
|
|
102
|
+
- Python 3.14 on the `default` environment, with coverage on `main`;
|
|
103
|
+
- on `main`, the other supported Python versions on `default`;
|
|
104
|
+
- Python 3.14 on `azure`, `aws` and `gcp`, each when the `TEST_ENVIRONMENTS` variable names it.
|
|
105
|
+
|
|
106
|
+
Each run's work directories are `<root>/<branch>/<Python version>` under the environment's roots.
|
|
107
|
+
|
|
108
|
+
### Supply chain
|
|
109
|
+
|
|
110
|
+
The test jobs hold the workspaces' credentials, so what they run is fixed:
|
|
111
|
+
|
|
112
|
+
- `uv.lock` is committed, with every package's hash. CI installs with `uv sync --locked`, which fails if the lock is
|
|
113
|
+
out of date.
|
|
114
|
+
- Actions are pinned to commit SHAs. setup-uv installs only uv versions whose checksums it knows (`latest-known`).
|
|
115
|
+
- Dependabot updates `uv.lock` and the Actions' SHAs weekly.
|
|
116
|
+
- Workflows run only on pushes and releases, never on pull requests from forks.
|
|
117
|
+
- Each workflow's token can only read the repository. Publishing adds `id-token: write`, for PyPI's trusted
|
|
118
|
+
publishing.
|
|
119
|
+
- The release build uses no cache.
|
|
120
|
+
|
|
121
|
+
ruff, run with `uvx`, is the exception: CI runs its latest version, in a job without secrets.
|
|
122
|
+
|
|
123
|
+
### Environments
|
|
124
|
+
|
|
125
|
+
Each GitHub environment is a Databricks workspace. Create at least `default`, and `azure`, `aws` or `gcp` as needed.
|
|
126
|
+
The tests authenticate as a service principal with read-write access to the work directories.
|
|
127
|
+
|
|
128
|
+
Every environment has these:
|
|
129
|
+
|
|
130
|
+
| Name | Value |
|
|
131
|
+
|----------------------------------------------------|------------------------------------------------|
|
|
132
|
+
| `DATABRICKS_HOST` (secret) | The workspace's URL |
|
|
133
|
+
| `FSSPEC_DATABRICKS_VOLUME_TEST_ROOT` (variable) | The POSIX path of the Volume work directory |
|
|
134
|
+
| `FSSPEC_DATABRICKS_WORKSPACE_TEST_ROOT` (variable) | The POSIX path of the Workspace work directory |
|
|
135
|
+
|
|
136
|
+
The variable `DATABRICKS_AUTH_TYPE` chooses how the tests authenticate:
|
|
137
|
+
|
|
138
|
+
| `DATABRICKS_AUTH_TYPE` | Authentication | Also needs |
|
|
139
|
+
|------------------------|-------------------------------------------------------|------------------------------------------------------------------|
|
|
140
|
+
| Not set | The service principal's client secret | `DATABRICKS_CLIENT_ID` and `DATABRICKS_CLIENT_SECRET` (secrets) |
|
|
141
|
+
| `github-oidc` | Databricks federation policy | `DATABRICKS_CLIENT_ID` (secret), `DATABRICKS_TOKEN_AUDIENCE` |
|
|
142
|
+
| `github-oidc-azure` | Federated credential of a Microsoft Entra ID app | `ARM_CLIENT_ID`, the app's ID |
|
|
143
|
+
|
|
144
|
+
Prefer federation: it needs no stored secret. GitHub's OIDC token identifies each environment by the subject
|
|
145
|
+
`repo:sugibuchi/fsspec-databricks:environment:<name>`. The federation's trust must name that subject and the issuer
|
|
146
|
+
`https://token.actions.githubusercontent.com`.
|
|
147
|
+
|
|
148
|
+
- **`github-oidc`**: a federation policy on the service principal, in the Databricks account console. Its audience is
|
|
149
|
+
`DATABRICKS_TOKEN_AUDIENCE`, e.g. the account ID.
|
|
150
|
+
- **`github-oidc-azure`**: a federated credential on the Entra ID app, with the audience `api://AzureADTokenExchange`.
|
|
151
|
+
The app is the workspace's service principal. This needs no access to the account console.
|
|
152
|
+
|
|
153
|
+
With federation, the client secret is not passed to the tests, even if the environment still has one.
|
|
154
|
+
|
|
155
|
+
The environments are:
|
|
156
|
+
|
|
157
|
+
| Environment | Workspace | Authentication |
|
|
158
|
+
|-------------|----------------------------------|---------------------|
|
|
159
|
+
| `default` | Databricks Free Edition | Client secret |
|
|
160
|
+
| `azure` | Azure Databricks | `github-oidc-azure` |
|
|
161
|
+
| `gcp` | Databricks on Google Cloud | `github-oidc` |
|
|
162
|
+
|
|
163
|
+
At the repository level:
|
|
164
|
+
|
|
165
|
+
| Name | Value |
|
|
166
|
+
|-------------------------------|--------------------------------------------------------------------------|
|
|
167
|
+
| `CODECOV_TOKEN` (secret) | The repository's upload token for [Codecov](https://about.codecov.io/) |
|
|
168
|
+
| `TEST_ENVIRONMENTS` (variable) | The environments tested on each push, e.g. `azure gcp` |
|
|
169
|
+
|
|
170
|
+
## Releasing
|
|
171
|
+
|
|
172
|
+
The version comes from the git tag, through hatch-vcs.
|
|
173
|
+
|
|
174
|
+
1. Create a GitHub release with a new tag `vX.Y.Z` on `main`.
|
|
175
|
+
2. Publishing it runs `on_release.yaml`. It builds the package and publishes it to PyPI, in the GitHub environment
|
|
176
|
+
`pypi`.
|
|
177
|
+
|
|
178
|
+
`uv publish` authenticates to PyPI with the workflow's OIDC token, through trusted publishing.
|
|
179
|
+
|
|
180
|
+
Repository rules protect the release path:
|
|
181
|
+
|
|
182
|
+
- The ruleset "Release tags" lets only repository admins create, move or delete `v*` tags.
|
|
183
|
+
- The `pypi` environment deploys only from `v*` tags.
|
|
184
|
+
- The ruleset "Default branch" blocks force-pushes to `main` and its deletion.
|
|
185
|
+
|
|
186
|
+
## Design
|
|
187
|
+
|
|
188
|
+
### One file system for all of Databricks' paths
|
|
189
|
+
|
|
190
|
+
`DatabricksFileSystem` takes the paths Databricks itself takes: `dbfs:/` URLs and POSIX paths. It sends each call to
|
|
191
|
+
the backend of its path, with the same rules as Databricks:
|
|
192
|
+
|
|
193
|
+
| Path | Backend |
|
|
194
|
+
|------------------------------------------------|-----------------------|
|
|
195
|
+
| `/Volumes/<catalog>/<schema>/<volume>/...` | `VolumeFileSystem` |
|
|
196
|
+
| `/Workspace/...` | `WorkspaceFileSystem` |
|
|
197
|
+
| Any other | `DBFS`, if enabled |
|
|
198
|
+
|
|
199
|
+
The same code then runs inside and outside Databricks.
|
|
200
|
+
|
|
201
|
+
### Inside a workspace, the local file system
|
|
202
|
+
|
|
203
|
+
On a Databricks cluster, `DATABRICKS_RUNTIME_VERSION` is set. `DatabricksFileSystem` then serves every path from the
|
|
204
|
+
local file system, unless `use_local_fs_in_workspace=False`.
|
|
205
|
+
|
|
206
|
+
### The SDK for metadata, HTTP for Volume data
|
|
207
|
+
|
|
208
|
+
Listings, metadata and changes go through the Databricks SDK. Volume file contents go through the Files API directly,
|
|
209
|
+
with aiohttp, so that ranges and parts run concurrently.
|
|
210
|
+
|
|
211
|
+
### Standard exceptions
|
|
212
|
+
|
|
213
|
+
Errors are `OSError` subclasses, as from local files: `FileNotFoundError`, `FileExistsError`, `IsADirectoryError`,
|
|
214
|
+
`NotADirectoryError` and `PermissionError`. A failed transfer is an `OSError` with `errno.EIO`, caused by the original
|
|
215
|
+
exception.
|
|
216
|
+
|
|
217
|
+
### Picklable
|
|
218
|
+
|
|
219
|
+
File systems are pickled to run in other processes, e.g. by Dask. Clients, event loops, threads and sessions are left
|
|
220
|
+
out, and made again on first use.
|
|
221
|
+
|
|
222
|
+
A `WorkspaceClient` given as `client` is kept, and is not picklable. Other ways of authenticating are.
|
|
223
|
+
|
|
224
|
+
### Known limits
|
|
225
|
+
|
|
226
|
+
- **Multipart upload is not in the REST reference.** The Databricks SDK for Python uses the same endpoints, as an
|
|
227
|
+
experimental feature, in `databricks/sdk/mixins/files.py`. Compare with it when the API changes.
|
|
228
|
+
- **Workspace files are at most 10 MB.** The Workspace API's limit.
|
|
229
|
+
- **No append or update modes on Volumes.** `ab` and `r+b` are refused.
|
|
230
|
+
- **No listing cache.** Each call reaches the workspace.
|
|
231
|
+
|
|
232
|
+
## Architecture
|
|
233
|
+
|
|
234
|
+
- **`DatabricksFileSystem`**, in `spec.py`, routes calls to backends, created on first use.
|
|
235
|
+
- **The backends** subclass `AbstractDatabricksFileSystem`, in `base.py`. Each wraps one Databricks API.
|
|
236
|
+
- **The file objects**, in `file.py`, are `RawIOBase`s. Volumes read and write ranges concurrently. Workspace files are
|
|
237
|
+
cached whole.
|
|
238
|
+
- **Paths**, in `path.py`, are parsed with regular expressions into each backend's parts.
|
|
239
|
+
|
|
240
|
+
### Routing
|
|
241
|
+
|
|
242
|
+
`DatabricksFileSystem._resolve(path)` gives the file system for a path, and the path to pass to it:
|
|
243
|
+
|
|
244
|
+
- the local file system and the local path, when running in a workspace with `use_local_fs_in_workspace`;
|
|
245
|
+
- else the Volume, Workspace or legacy DBFS backend of `path.fs_type`, and the path unchanged;
|
|
246
|
+
- `FileNotFoundError` for legacy DBFS paths when the workspace disables legacy DBFS.
|
|
247
|
+
|
|
248
|
+
Every method calls `_resolve`. With the local file system, `ls` and `info` map the names returned back to DBFS paths.
|
|
249
|
+
|
|
250
|
+
Whether legacy DBFS is enabled is checked once:
|
|
251
|
+
|
|
252
|
+
- with the local file system, by whether `legacy_dbfs_local_root` exists;
|
|
253
|
+
- otherwise, from the workspace setting `disable_legacy_dbfs`.
|
|
254
|
+
|
|
255
|
+
When legacy DBFS is disabled, `ls("/")` and `info("/")` answer without a backend: the root holds `/Volumes` and
|
|
256
|
+
`/Workspace`.
|
|
257
|
+
|
|
258
|
+
The backends are created on first use, with the file system's client, config and options. `close()` closes them.
|
|
259
|
+
|
|
260
|
+
Transfers between backends:
|
|
261
|
+
|
|
262
|
+
- `cp_file` streams the file from one backend's file object into the other's, in blocks of
|
|
263
|
+
`cross_fs_copy_block_size`. A folder is created, not copied.
|
|
264
|
+
- `mv` uses fsspec's default, a copy then a delete.
|
|
265
|
+
|
|
266
|
+
Within one backend, both go to the backend.
|
|
267
|
+
|
|
268
|
+
### The base class
|
|
269
|
+
|
|
270
|
+
`AbstractDatabricksFileSystem` holds what every backend shares:
|
|
271
|
+
|
|
272
|
+
- the Databricks SDK `Config` and `WorkspaceClient`, made on first use from `client`, `config` or `config_params`;
|
|
273
|
+
- `close()`, also called at exit and on garbage collection;
|
|
274
|
+
- pickling without the client and config;
|
|
275
|
+
- the `dbfs` protocol: `_strip_protocol` and `unstrip_protocol` use `path.py`.
|
|
276
|
+
|
|
277
|
+
Its `mkdir`, `makedirs`, `rmdir` and `_rm` only check preconditions, such as a missing parent or a file where a folder
|
|
278
|
+
is expected. Subclasses call them first, then make the API call.
|
|
279
|
+
|
|
280
|
+
`open` passes text mode on, and writes text with `"\n"` newlines by default.
|
|
281
|
+
|
|
282
|
+
### Paths
|
|
283
|
+
|
|
284
|
+
`path.py` matches paths with three regular expressions: `dbfs:/` URLs, Workspace paths and Volume paths.
|
|
285
|
+
|
|
286
|
+
- `strip_scheme` and `unstrip_scheme` convert between `dbfs:/` URLs and POSIX paths.
|
|
287
|
+
- `fs_type` gives a path's backend.
|
|
288
|
+
- `parse_volume_path` gives the catalog, schema, volume and path in the volume. Missing parts are `None`.
|
|
289
|
+
- `parse_workspace_path` and `parse_dbfs_path` give the path in their API.
|
|
290
|
+
- `to_local_path` and `from_local_path` convert to and from local paths on Databricks compute.
|
|
291
|
+
|
|
292
|
+
### Errors
|
|
293
|
+
|
|
294
|
+
`error_mapping(path)` is a context manager around SDK calls. It maps the SDK's exceptions:
|
|
295
|
+
|
|
296
|
+
| SDK exception | Raised |
|
|
297
|
+
|-----------------------------------------|-------------------------------------|
|
|
298
|
+
| `NotFound` | `FileNotFoundError` |
|
|
299
|
+
| `AlreadyExists`, `ResourceAlreadyExists`| `FileExistsError` |
|
|
300
|
+
| `Unauthenticated`, `PermissionDenied` | `PermissionError` with `EACCES` |
|
|
301
|
+
|
|
302
|
+
Other exceptions pass through. The helpers `file_not_found_error` and the like make `OSError`s with the path and the
|
|
303
|
+
system's message.
|
|
304
|
+
|
|
305
|
+
### Legacy DBFS
|
|
306
|
+
|
|
307
|
+
`DBFS`, in `dbfs.py`, uses the SDK's `WorkspaceClient.dbfs`. Copies, moves and recursive deletes use the API's own,
|
|
308
|
+
when no glob pattern is involved. Files are opened with the SDK's `dbfs.open`.
|
|
309
|
+
|
|
310
|
+
### Volumes
|
|
311
|
+
|
|
312
|
+
`VolumeFileSystem`, in `volume.py`, subclasses `DBFS`, as the DBFS API also lists Volume folders.
|
|
313
|
+
|
|
314
|
+
The APIs called depend on the depth of the path:
|
|
315
|
+
|
|
316
|
+
| Path | `info` | `ls` |
|
|
317
|
+
|----------------------------------------|-----------------------------------------------------------|-----------------|
|
|
318
|
+
| `/Volumes` | None: a fixed folder | `catalogs.list` |
|
|
319
|
+
| `/Volumes/<catalog>` | `catalogs.get` | `schemas.list` |
|
|
320
|
+
| `/Volumes/<catalog>/<schema>` | `schemas.get` | `volumes.list` |
|
|
321
|
+
| `/Volumes/<catalog>/<schema>/<volume>` | `volumes.read` | `dbfs.list` |
|
|
322
|
+
| Deeper | `files.get_metadata`, else `files.get_directory_metadata` | `dbfs.list` |
|
|
323
|
+
|
|
324
|
+
`include_browse` is passed to the Unity Catalog APIs.
|
|
325
|
+
|
|
326
|
+
Each metadata call is a round trip, so the checks inside a volume make as few as they can:
|
|
327
|
+
|
|
328
|
+
| Check | Calls |
|
|
329
|
+
|-----------------------------------------------|-------------------------------------------------|
|
|
330
|
+
| `info` of a file, so also `size` and reading | 1: `files.get_metadata` |
|
|
331
|
+
| `info` of a directory | 2: `files.get_metadata`, then the directory's |
|
|
332
|
+
| `isdir`, so also `mkdir` and `rmdir` | 1: `files.get_directory_metadata` |
|
|
333
|
+
| `isfile` | 1: `files.get_metadata` |
|
|
334
|
+
| Opening for writing | 1: `isdir`, to refuse directories |
|
|
335
|
+
|
|
336
|
+
`DatabricksFileSystem` passes `isdir` and `isfile` to the backends. `test_volume_fs_metadata_calls` counts the calls.
|
|
337
|
+
|
|
338
|
+
#### The IO event loop
|
|
339
|
+
|
|
340
|
+
Each `VolumeFileSystem` runs an asyncio event loop in a daemon thread, `dbfs-io-thread`, started on first use. It
|
|
341
|
+
holds one aiohttp `ClientSession`, whose `TCPConnector` allows `connection_pool_size` connections.
|
|
342
|
+
|
|
343
|
+
- The session is created in the loop, as `TCPConnector` needs a running loop. From the loop itself, it is created
|
|
344
|
+
directly.
|
|
345
|
+
- `close()` closes the session in the loop, stops the loop and joins the thread.
|
|
346
|
+
- Requests are authenticated by an aiohttp middleware, with the SDK `Config`'s headers.
|
|
347
|
+
|
|
348
|
+
#### Storage proxy or presigned URLs
|
|
349
|
+
|
|
350
|
+
On first open, the file system probes the Databricks storage proxy, `storage-proxy.databricks.com`, with the workspace
|
|
351
|
+
ID:
|
|
352
|
+
|
|
353
|
+
- If it answers, all requests go to it.
|
|
354
|
+
- Otherwise, requests go to the workspace's host. Data moves through presigned URLs from the Files API.
|
|
355
|
+
|
|
356
|
+
Files smaller than `min_presigned_url_size`, 1 MB, are read from the Files API directly. Presigned URLs for reads are
|
|
357
|
+
renewed after 30 minutes.
|
|
358
|
+
|
|
359
|
+
#### Reading
|
|
360
|
+
|
|
361
|
+
`VolumeReadableFile` subclasses `AbstractAsyncReadableFile`. A read schedules range requests, each a task in the IO
|
|
362
|
+
loop:
|
|
363
|
+
|
|
364
|
+
- At most `max_concurrency` run at once.
|
|
365
|
+
- `read()` of the whole file splits the rest of the file evenly across them, within the block size limits.
|
|
366
|
+
- Smaller reads grow the block size with the bytes read in a row. After `n` bytes read in a row, the next `n` are
|
|
367
|
+
fetched ahead.
|
|
368
|
+
- A seek outside the fetched ranges cancels them, and resets the bytes read in a row.
|
|
369
|
+
|
|
370
|
+
`_compute_block_size` gives the block size: the smallest power-of-2 multiple of the minimum that covers the target with
|
|
371
|
+
`max_concurrency` blocks, capped at the maximum.
|
|
372
|
+
|
|
373
|
+
#### Writing
|
|
374
|
+
|
|
375
|
+
`VolumeWritableFile` subclasses `AbstractAsyncWritableFile`. Writes are buffered:
|
|
376
|
+
|
|
377
|
+
1. Data is buffered until it reaches `min_multipart_upload_size`. A file closed before then is uploaded by one `PUT`.
|
|
378
|
+
2. Then the upload is initiated, and the server chooses multipart or resumable upload.
|
|
379
|
+
3. Full blocks are uploaded as the buffer fills. After `n` bytes written, block sizes aim at `n` more.
|
|
380
|
+
4. `close()` uploads the rest, waits for the parts, and completes the upload.
|
|
381
|
+
|
|
382
|
+
The two kinds of upload differ:
|
|
383
|
+
|
|
384
|
+
- **Multipart**: parts upload concurrently, then the upload completes with their ETags.
|
|
385
|
+
- **Resumable**: one part at a time, with `Content-Range`. A `308` answer gives the bytes received, and the rest is
|
|
386
|
+
sent again.
|
|
387
|
+
|
|
388
|
+
If a part fails, `close()` aborts the upload and raises an `OSError`. Azure Databricks refuses aborts of multipart
|
|
389
|
+
uploads with a 400 answer, and the abort is then skipped.
|
|
390
|
+
|
|
391
|
+
Part sizes must be multiples of 256 KB.
|
|
392
|
+
|
|
393
|
+
### Workspace files
|
|
394
|
+
|
|
395
|
+
`WorkspaceFileSystem`, in `workspace.py`, uses the SDK's `WorkspaceClient.workspace`.
|
|
396
|
+
|
|
397
|
+
`WorkspaceFile` is a `MemoryCachedFile`:
|
|
398
|
+
|
|
399
|
+
- Opening for reading or appending downloads the whole file into memory.
|
|
400
|
+
- Closing or `flush(upload=True)` uploads it whole, if changed.
|
|
401
|
+
- Files are at most 10 MB.
|
|
402
|
+
|
|
403
|
+
Notebooks are objects of type `NOTEBOOK`. `notebook_format` gives their export and import format, `SOURCE` by default.
|
|
404
|
+
|
|
405
|
+
- An existing notebook keeps its language.
|
|
406
|
+
- A new notebook needs `language`.
|
|
407
|
+
- Other files use `AUTO` export and `RAW` import, and refuse `notebook_format`.
|
|
408
|
+
|
|
409
|
+
`cp_file` downloads and uploads, keeping notebooks as notebooks. `rm` uses the API's recursive delete when no glob
|
|
410
|
+
pattern is involved.
|
|
411
|
+
|
|
412
|
+
### File objects
|
|
413
|
+
|
|
414
|
+
`file.py` has two families of file objects:
|
|
415
|
+
|
|
416
|
+
| Class | Does |
|
|
417
|
+
|-------------------------------|-------------------------------------------------------------------------------|
|
|
418
|
+
| `AbstractAsyncFile` | Runs coroutines in an event loop of another thread, and waits for them. |
|
|
419
|
+
| `FileRangeTaskSupport` | Byte-range tasks, at most `max_concurrency` at once, cancelled on close. |
|
|
420
|
+
| `AbstractAsyncReadableFile` | Reading with range tasks, prefetching and seeking. |
|
|
421
|
+
| `AbstractAsyncWritableFile` | Buffered writing, single uploads, and multipart uploads with part tasks. |
|
|
422
|
+
| `AbstractCachedFile` | A whole remote file cached locally, downloaded on open and uploaded on close. |
|
|
423
|
+
| `MemoryCachedFile` | The cache in memory, with a required `max_size`. |
|
|
424
|
+
| `FileCachedFile` | The cache in a temporary file. |
|
|
425
|
+
|
|
426
|
+
Synchronous methods of async files raise `RuntimeError` when called from their own event loop. Use the `async`
|
|
427
|
+
methods there.
|
|
428
|
+
|
|
429
|
+
A failed range task stops the file: later reads and writes raise, with the task's exception as the cause.
|
|
430
|
+
|
|
431
|
+
### Local file system
|
|
432
|
+
|
|
433
|
+
`DatabricksLocalFileSystem`, in `local.py`, is fsspec's `LocalFileSystem` with the `dbfs` protocol. It gets local
|
|
434
|
+
paths from `DatabricksFileSystem`.
|
|
435
|
+
|
|
436
|
+
`path.to_local_path` gives a path's local path on Databricks compute:
|
|
437
|
+
|
|
438
|
+
- Volume and Workspace paths are the same.
|
|
439
|
+
- Legacy DBFS paths are under `legacy_dbfs_local_root`, `/dbfs` by default.
|
|
440
|
+
|
|
441
|
+
`path.from_local_path` is the reverse, for the names that `ls` and `info` return.
|
|
442
|
+
|
|
443
|
+
### Logging
|
|
444
|
+
|
|
445
|
+
Each module has a logger of its name. Debug logs on every read, write and task are written only with
|
|
446
|
+
`verbose_debug_log`.
|
|
@@ -199,4 +199,4 @@
|
|
|
199
199
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
200
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
201
|
See the License for the specific language governing permissions and
|
|
202
|
-
limitations under the License.
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fsspec-databricks
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: `fsspec`-based file system interface for Databricks file system"s"
|
|
5
|
+
Project-URL: Homepage, https://github.com/sugibuchi/fsspec-databricks
|
|
6
|
+
Author-email: Tsuyoshi SUGIBUCHI <sugibuchi@gmail.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: databricks,filesystem,fsspec
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: aiohttp>3.12
|
|
22
|
+
Requires-Dist: databricks-sdk>=0.156.0
|
|
23
|
+
Requires-Dist: fsspec
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# fsspec-databricks
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/fsspec-databricks/)
|
|
29
|
+
[](https://codecov.io/github/sugibuchi/fsspec-databricks)
|
|
30
|
+
|
|
31
|
+
An [fsspec](https://filesystem-spec.readthedocs.io/en/latest/) file system for Databricks:
|
|
32
|
+
|
|
33
|
+
* [Unity Catalog Volumes](https://docs.databricks.com/aws/en/volumes/)
|
|
34
|
+
* [Workspace files](https://docs.databricks.com/aws/en/files/workspace)
|
|
35
|
+
* [Legacy DBFS](https://docs.databricks.com/aws/en/dbfs/)
|
|
36
|
+
|
|
37
|
+
The same `dbfs:/` URLs and POSIX paths work inside and outside Databricks, with pandas, PyArrow and any library that
|
|
38
|
+
takes fsspec.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
* Routes `dbfs:/` URLs and POSIX paths to the right file system, as Databricks does.
|
|
45
|
+
* Copies and moves files between file systems, e.g. from Workspace files to a Volume.
|
|
46
|
+
* Uses the local file system when running inside a Databricks workspace.
|
|
47
|
+
* Reads and writes Volume files with concurrent requests.
|
|
48
|
+
* Authenticates with [Databricks Unified Authentication](https://docs.databricks.com/aws/en/dev-tools/auth/unified-auth).
|
|
49
|
+
|
|
50
|
+
fsspec's own `DatabricksFileSystem` supports legacy DBFS only.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install fsspec-databricks
|
|
58
|
+
# or
|
|
59
|
+
uv add fsspec-databricks
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from fsspec_databricks import DatabricksFileSystem
|
|
68
|
+
|
|
69
|
+
fs = DatabricksFileSystem()
|
|
70
|
+
|
|
71
|
+
fs.ls("dbfs:/Volumes/my_catalog/my_schema/my_volume/path")
|
|
72
|
+
|
|
73
|
+
with fs.open("dbfs:/Volumes/my_catalog/my_schema/my_volume/data.csv", "rb") as f:
|
|
74
|
+
data = f.read()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
To use it for every `dbfs:/` URL in fsspec and the libraries using it, call `use()` once:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import fsspec
|
|
81
|
+
import fsspec_databricks
|
|
82
|
+
|
|
83
|
+
fsspec_databricks.use()
|
|
84
|
+
|
|
85
|
+
fs = fsspec.filesystem("dbfs") # DatabricksFileSystem
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
See [USAGE.md](https://github.com/sugibuchi/fsspec-databricks/blob/main/USAGE.md) for operations, each file system's
|
|
89
|
+
behaviour and options. See [AUTHENTICATION.md](https://github.com/sugibuchi/fsspec-databricks/blob/main/AUTHENTICATION.md)
|
|
90
|
+
for authentication.
|
|
91
|
+
|
|
92
|
+
### Paths
|
|
93
|
+
|
|
94
|
+
| Path | File system |
|
|
95
|
+
|--------------------------------------------------------|---------------------------------------------|
|
|
96
|
+
| `dbfs:/Volumes/<catalog>/<schema>/<volume>/path` | Unity Catalog Volume |
|
|
97
|
+
| `dbfs:/Workspace/path` | Workspace files |
|
|
98
|
+
| `dbfs:/path`, any other | Legacy DBFS |
|
|
99
|
+
|
|
100
|
+
POSIX paths without `dbfs:` work the same: `/Volumes/...`, `/Workspace/...` and `/path`. Databricks maps `/Workspace`
|
|
101
|
+
to Workspace files only in workspaces with legacy DBFS disabled. See
|
|
102
|
+
[Databricks' documentation](https://docs.databricks.com/aws/en/files/).
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Compatibility
|
|
107
|
+
|
|
108
|
+
* Python 3.11 to 3.14
|
|
109
|
+
* `databricks-sdk`: 0.156.0 or later
|
|
110
|
+
* `fsspec`: 2024.6.0 or later
|
|
111
|
+
* `aiohttp`: 3.12.0 or later
|
|
112
|
+
* Tested on Databricks Free Edition, Azure Databricks and Databricks on Google Cloud
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Status
|
|
117
|
+
|
|
118
|
+
**Early beta.** The API and behaviour may change.
|
|
119
|
+
|
|
120
|
+
* Writing Volume files uses the Files API's multipart upload. The Databricks REST reference does not document it, but
|
|
121
|
+
the official Databricks SDK for Python uses it, as an experimental feature.
|
|
122
|
+
* Databricks on AWS is not tested.
|
|
123
|
+
* Legacy DBFS is not tested.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
Apache License 2.0. See [LICENSE](https://github.com/sugibuchi/fsspec-databricks/blob/main/LICENSE).
|