fsspec-databricks 0.2.0__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.2.0 → fsspec_databricks-0.2.1}/.gitignore +0 -3
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/DEVELOPMENT.md +69 -11
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/PKG-INFO +3 -2
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/README.md +2 -1
- fsspec_databricks-0.2.1/SECURITY.md +30 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/pyproject.toml +9 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/src/fsspec_databricks/base.py +14 -11
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/src/fsspec_databricks/spec.py +20 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/src/fsspec_databricks/volume.py +78 -25
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/tests/test_volume_fs.py +74 -1
- fsspec_databricks-0.2.0/.github/dependabot.yml +0 -11
- fsspec_databricks-0.2.0/.github/workflows/check.yaml +0 -33
- fsspec_databricks-0.2.0/.github/workflows/on_push.yaml +0 -85
- fsspec_databricks-0.2.0/.github/workflows/on_release.yaml +0 -21
- fsspec_databricks-0.2.0/.github/workflows/publish.yaml +0 -43
- fsspec_databricks-0.2.0/.github/workflows/test.yaml +0 -139
- fsspec_databricks-0.2.0/.pre-commit-config.yaml +0 -20
- fsspec_databricks-0.2.0/CLAUDE.md +0 -108
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/AUTHENTICATION.md +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/LICENSE +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/USAGE.md +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/src/fsspec_databricks/__init__.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/src/fsspec_databricks/dbfs.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/src/fsspec_databricks/error.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/src/fsspec_databricks/file.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/src/fsspec_databricks/local.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/src/fsspec_databricks/path.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/src/fsspec_databricks/py.typed +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/src/fsspec_databricks/utils.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/src/fsspec_databricks/workspace.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/tests/__init__.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/tests/conftest.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/tests/dummy_api.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/tests/test_base_fs.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/tests/test_file.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/tests/test_path.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/tests/test_spec_fs.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/tests/test_utils.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/tests/test_volume.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/tests/test_workspace_fs.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.2.1}/tests/utils.py +0 -0
|
@@ -105,21 +105,60 @@ The workflows are in `.github/workflows/`:
|
|
|
105
105
|
|
|
106
106
|
Each run's work directories are `<root>/<branch>/<Python version>` under the environment's roots.
|
|
107
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
|
+
|
|
108
123
|
### Environments
|
|
109
124
|
|
|
110
125
|
Each GitHub environment is a Databricks workspace. Create at least `default`, and `azure`, `aws` or `gcp` as needed.
|
|
111
126
|
The tests authenticate as a service principal with read-write access to the work directories.
|
|
112
127
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
|
116
|
-
|
|
117
|
-
| `
|
|
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`.
|
|
118
147
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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` |
|
|
123
162
|
|
|
124
163
|
At the repository level:
|
|
125
164
|
|
|
@@ -138,6 +177,12 @@ The version comes from the git tag, through hatch-vcs.
|
|
|
138
177
|
|
|
139
178
|
`uv publish` authenticates to PyPI with the workflow's OIDC token, through trusted publishing.
|
|
140
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
|
+
|
|
141
186
|
## Design
|
|
142
187
|
|
|
143
188
|
### One file system for all of Databricks' paths
|
|
@@ -178,7 +223,8 @@ A `WorkspaceClient` given as `client` is kept, and is not picklable. Other ways
|
|
|
178
223
|
|
|
179
224
|
### Known limits
|
|
180
225
|
|
|
181
|
-
- **Multipart upload
|
|
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.
|
|
182
228
|
- **Workspace files are at most 10 MB.** The Workspace API's limit.
|
|
183
229
|
- **No append or update modes on Volumes.** `ab` and `r+b` are refused.
|
|
184
230
|
- **No listing cache.** Each call reaches the workspace.
|
|
@@ -273,10 +319,22 @@ The APIs called depend on the depth of the path:
|
|
|
273
319
|
| `/Volumes/<catalog>` | `catalogs.get` | `schemas.list` |
|
|
274
320
|
| `/Volumes/<catalog>/<schema>` | `schemas.get` | `volumes.list` |
|
|
275
321
|
| `/Volumes/<catalog>/<schema>/<volume>` | `volumes.read` | `dbfs.list` |
|
|
276
|
-
| Deeper | `files.
|
|
322
|
+
| Deeper | `files.get_metadata`, else `files.get_directory_metadata` | `dbfs.list` |
|
|
277
323
|
|
|
278
324
|
`include_browse` is passed to the Unity Catalog APIs.
|
|
279
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
|
+
|
|
280
338
|
#### The IO event loop
|
|
281
339
|
|
|
282
340
|
Each `VolumeFileSystem` runs an asyncio event loop in a daemon thread, `dbfs-io-thread`, started on first use. It
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fsspec-databricks
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: `fsspec`-based file system interface for Databricks file system"s"
|
|
5
5
|
Project-URL: Homepage, https://github.com/sugibuchi/fsspec-databricks
|
|
6
6
|
Author-email: Tsuyoshi SUGIBUCHI <sugibuchi@gmail.com>
|
|
@@ -117,7 +117,8 @@ to Workspace files only in workspaces with legacy DBFS disabled. See
|
|
|
117
117
|
|
|
118
118
|
**Early beta.** The API and behaviour may change.
|
|
119
119
|
|
|
120
|
-
* Writing Volume files uses
|
|
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.
|
|
121
122
|
* Databricks on AWS is not tested.
|
|
122
123
|
* Legacy DBFS is not tested.
|
|
123
124
|
|
|
@@ -92,7 +92,8 @@ to Workspace files only in workspaces with legacy DBFS disabled. See
|
|
|
92
92
|
|
|
93
93
|
**Early beta.** The API and behaviour may change.
|
|
94
94
|
|
|
95
|
-
* Writing Volume files uses
|
|
95
|
+
* Writing Volume files uses the Files API's multipart upload. The Databricks REST reference does not document it, but
|
|
96
|
+
the official Databricks SDK for Python uses it, as an experimental feature.
|
|
96
97
|
* Databricks on AWS is not tested.
|
|
97
98
|
* Legacy DBFS is not tested.
|
|
98
99
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Security fixes are made in the latest release only.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Report vulnerabilities privately, through
|
|
10
|
+
[GitHub's private vulnerability reporting](https://github.com/sugibuchi/fsspec-databricks/security/advisories/new).
|
|
11
|
+
Do not open a public issue.
|
|
12
|
+
|
|
13
|
+
Include:
|
|
14
|
+
|
|
15
|
+
- the version of `fsspec-databricks`, and of `databricks-sdk`;
|
|
16
|
+
- the steps to reproduce, or a proof of concept;
|
|
17
|
+
- the impact, e.g. leaked credentials or access to files beyond the user's permissions.
|
|
18
|
+
|
|
19
|
+
Never include real credentials, tokens or workspace URLs in a report.
|
|
20
|
+
|
|
21
|
+
## Scope
|
|
22
|
+
|
|
23
|
+
`fsspec-databricks` accesses Databricks with the credentials of its user, through the Databricks SDK. In scope are,
|
|
24
|
+
for example:
|
|
25
|
+
|
|
26
|
+
- credentials or tokens written to logs, exceptions or pickled file systems;
|
|
27
|
+
- access to files or paths beyond what the given path names;
|
|
28
|
+
- requests sent to hosts other than the workspace, its storage proxy, or the URLs Databricks returns.
|
|
29
|
+
|
|
30
|
+
Vulnerabilities of Databricks itself or of the Databricks SDK go to Databricks.
|
|
@@ -73,6 +73,15 @@ source = [
|
|
|
73
73
|
source = "vcs"
|
|
74
74
|
raw-options = { 'version_scheme' = 'post-release' }
|
|
75
75
|
|
|
76
|
+
[tool.hatch.build.targets.sdist]
|
|
77
|
+
# Files for development only
|
|
78
|
+
exclude = [
|
|
79
|
+
".github",
|
|
80
|
+
".pre-commit-config.yaml",
|
|
81
|
+
"CLAUDE.md",
|
|
82
|
+
"uv.lock",
|
|
83
|
+
]
|
|
84
|
+
|
|
76
85
|
[tool.ruff]
|
|
77
86
|
exclude = [
|
|
78
87
|
".tox",
|
|
@@ -213,15 +213,17 @@ class AbstractDatabricksFileSystem(AbstractFileSystem):
|
|
|
213
213
|
"Making directory: path=%s, create_parents=%s", path, create_parents
|
|
214
214
|
)
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
216
|
+
parent = self._parent(path)
|
|
217
|
+
if not self.isdir(parent):
|
|
218
|
+
# Tell a missing parent from a file
|
|
219
|
+
try:
|
|
220
|
+
self.info(parent)
|
|
221
|
+
except FileNotFoundError:
|
|
222
|
+
if create_parents:
|
|
223
|
+
return
|
|
224
|
+
else:
|
|
225
|
+
raise
|
|
226
|
+
raise not_a_directory_error(parent)
|
|
225
227
|
|
|
226
228
|
if self.exists(path):
|
|
227
229
|
raise file_exists_error(path)
|
|
@@ -248,8 +250,9 @@ class AbstractDatabricksFileSystem(AbstractFileSystem):
|
|
|
248
250
|
# Actual operations must be implemented by subclasses
|
|
249
251
|
self.log.debug("Deleting directory: path=%s", path)
|
|
250
252
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
+
if not self.isdir(path):
|
|
254
|
+
# Raises FileNotFoundError if the path is missing
|
|
255
|
+
self.info(path)
|
|
253
256
|
raise not_a_directory_error(path)
|
|
254
257
|
|
|
255
258
|
def open(
|
|
@@ -458,6 +458,26 @@ class DatabricksFileSystem(AbstractDatabricksFileSystem):
|
|
|
458
458
|
fs, p = self._resolve(path)
|
|
459
459
|
return fs.exists(p, **kwargs)
|
|
460
460
|
|
|
461
|
+
def isdir(self, path) -> bool:
|
|
462
|
+
if self._strip_protocol(path) == "/":
|
|
463
|
+
return True
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
fs, p = self._resolve(path)
|
|
467
|
+
except FileNotFoundError:
|
|
468
|
+
return False
|
|
469
|
+
return fs.isdir(p)
|
|
470
|
+
|
|
471
|
+
def isfile(self, path) -> bool:
|
|
472
|
+
if self._strip_protocol(path) == "/":
|
|
473
|
+
return False
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
fs, p = self._resolve(path)
|
|
477
|
+
except FileNotFoundError:
|
|
478
|
+
return False
|
|
479
|
+
return fs.isfile(p)
|
|
480
|
+
|
|
461
481
|
def created(self, path):
|
|
462
482
|
fs, p = self._resolve(path)
|
|
463
483
|
return fs.created(p)
|
|
@@ -20,7 +20,7 @@ from aiohttp import (
|
|
|
20
20
|
from databricks.sdk import WorkspaceClient
|
|
21
21
|
from databricks.sdk.client_types import HostType
|
|
22
22
|
from databricks.sdk.config import Config
|
|
23
|
-
from databricks.sdk.errors import BadRequest,
|
|
23
|
+
from databricks.sdk.errors import BadRequest, NotFound
|
|
24
24
|
from databricks.sdk.service.catalog import CatalogInfo, SchemaInfo, VolumeInfo
|
|
25
25
|
from databricks.sdk.service.files import DirectoryEntry, GetMetadataResponse
|
|
26
26
|
|
|
@@ -403,19 +403,61 @@ class VolumeFileSystem(DBFS):
|
|
|
403
403
|
detail=True,
|
|
404
404
|
)
|
|
405
405
|
else:
|
|
406
|
+
# Files first: they are asked about more often than directories
|
|
406
407
|
try:
|
|
407
|
-
self.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
408
|
+
return self._file_info(full_path)
|
|
409
|
+
except NotFound:
|
|
410
|
+
return self._directory_info(full_path)
|
|
411
|
+
|
|
412
|
+
def _file_info(self, full_path: str) -> dict[str, Any]:
|
|
413
|
+
"""Info of a file in a volume, with one API call. Raises `NotFound` for other paths."""
|
|
414
|
+
metadata = self.client.files.get_metadata(full_path)
|
|
415
|
+
return _map_info(metadata, detail=True, path=full_path)
|
|
416
|
+
|
|
417
|
+
def _directory_info(self, full_path: str) -> dict[str, Any]:
|
|
418
|
+
"""Info of a directory in a volume, with one API call. Raises `NotFound` for other paths."""
|
|
419
|
+
self.client.files.get_directory_metadata(full_path)
|
|
420
|
+
return {
|
|
421
|
+
"name": full_path.rstrip("/"),
|
|
422
|
+
"size": None,
|
|
423
|
+
"type": "directory",
|
|
424
|
+
"created": None,
|
|
425
|
+
"modified": None,
|
|
426
|
+
"islink": False,
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
def isdir(self, path):
|
|
430
|
+
try:
|
|
431
|
+
_, _, _, path_in_volume, full_path = parse_volume_path(path)
|
|
432
|
+
except ValueError:
|
|
433
|
+
return super().isdir(path)
|
|
434
|
+
|
|
435
|
+
if path_in_volume is None or path_in_volume == "/":
|
|
436
|
+
# The volumes root, catalogs, schemas and volumes
|
|
437
|
+
return super().isdir(path)
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
with error_mapping(path):
|
|
441
|
+
self._directory_info(full_path)
|
|
442
|
+
return True
|
|
443
|
+
except OSError:
|
|
444
|
+
return False
|
|
445
|
+
|
|
446
|
+
def isfile(self, path):
|
|
447
|
+
try:
|
|
448
|
+
_, _, _, path_in_volume, full_path = parse_volume_path(path)
|
|
449
|
+
except ValueError:
|
|
450
|
+
return False
|
|
451
|
+
|
|
452
|
+
if path_in_volume is None or path_in_volume == "/":
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
with error_mapping(path):
|
|
457
|
+
self._file_info(full_path)
|
|
458
|
+
return True
|
|
459
|
+
except Exception:
|
|
460
|
+
return False
|
|
419
461
|
|
|
420
462
|
def ls(self, path, detail=True, include_browse: bool | None = None, **kwargs):
|
|
421
463
|
self.log.debug(
|
|
@@ -472,6 +514,28 @@ class VolumeFileSystem(DBFS):
|
|
|
472
514
|
else:
|
|
473
515
|
raise
|
|
474
516
|
|
|
517
|
+
def _check_open_target(self, path: str, mode: str) -> dict[str, Any] | None:
|
|
518
|
+
"""Check that the path can be opened in the mode, and return its info if needed."""
|
|
519
|
+
if "r" not in mode and "x" not in mode:
|
|
520
|
+
# Writing only needs to refuse directories
|
|
521
|
+
if self.isdir(path):
|
|
522
|
+
raise is_a_directory_error(path)
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
info = self.info(path)
|
|
527
|
+
except FileNotFoundError:
|
|
528
|
+
info = None
|
|
529
|
+
|
|
530
|
+
if info is not None and info["type"] == "directory":
|
|
531
|
+
raise is_a_directory_error(path)
|
|
532
|
+
if "r" in mode and info is None:
|
|
533
|
+
raise file_not_found_error(path)
|
|
534
|
+
if "x" in mode and info is not None:
|
|
535
|
+
raise file_exists_error(path)
|
|
536
|
+
|
|
537
|
+
return info
|
|
538
|
+
|
|
475
539
|
def _open(
|
|
476
540
|
self,
|
|
477
541
|
path,
|
|
@@ -487,18 +551,7 @@ class VolumeFileSystem(DBFS):
|
|
|
487
551
|
if "a" in mode or "+" in mode:
|
|
488
552
|
raise UnsupportedOperation("Append and read/write modes are not supported.")
|
|
489
553
|
|
|
490
|
-
|
|
491
|
-
info = self.info(path)
|
|
492
|
-
if info["type"] == "directory":
|
|
493
|
-
raise is_a_directory_error(path)
|
|
494
|
-
except FileNotFoundError:
|
|
495
|
-
info = None
|
|
496
|
-
|
|
497
|
-
if "r" in mode and info is None:
|
|
498
|
-
raise file_not_found_error(path)
|
|
499
|
-
|
|
500
|
-
if "x" in mode and info is not None:
|
|
501
|
-
raise file_exists_error(path)
|
|
554
|
+
info = self._check_open_target(path, mode)
|
|
502
555
|
|
|
503
556
|
auth = _workspace_authenticator(self.config)
|
|
504
557
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import contextlib
|
|
2
3
|
import errno
|
|
3
4
|
import inspect
|
|
4
5
|
import logging
|
|
@@ -6,7 +7,7 @@ import pickle
|
|
|
6
7
|
from datetime import UTC, datetime, timedelta
|
|
7
8
|
from io import BytesIO
|
|
8
9
|
from random import randbytes
|
|
9
|
-
from unittest.mock import patch
|
|
10
|
+
from unittest.mock import MagicMock, patch
|
|
10
11
|
|
|
11
12
|
import pytest
|
|
12
13
|
from databricks.sdk import WorkspaceClient
|
|
@@ -182,6 +183,78 @@ def test_volume_fs_session_in_event_loop():
|
|
|
182
183
|
assert session is fs._session
|
|
183
184
|
|
|
184
185
|
|
|
186
|
+
class _CountingFilesAPI:
|
|
187
|
+
"""A fake of `WorkspaceClient.files` for metadata, recording the calls."""
|
|
188
|
+
|
|
189
|
+
def __init__(self, files: list[str], directories: list[str]):
|
|
190
|
+
self.files = files
|
|
191
|
+
self.directories = directories
|
|
192
|
+
self.calls: list[tuple[str, str]] = []
|
|
193
|
+
|
|
194
|
+
def get_metadata(self, path):
|
|
195
|
+
self.calls.append(("file", path))
|
|
196
|
+
if path not in self.files:
|
|
197
|
+
raise NotFound(f"Not found: {path}")
|
|
198
|
+
return GetMetadataResponse(
|
|
199
|
+
content_length=123,
|
|
200
|
+
content_type="application/octet-stream",
|
|
201
|
+
last_modified="Wed, 21 Oct 2026 07:28:00 GMT",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def get_directory_metadata(self, path):
|
|
205
|
+
self.calls.append(("directory", path))
|
|
206
|
+
if path not in self.directories:
|
|
207
|
+
raise NotFound(f"Not found: {path}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_volume_fs_metadata_calls():
|
|
211
|
+
"""Each check of a file or a directory makes as few API calls as possible."""
|
|
212
|
+
file = "/Volumes/c/s/v/dir/file.bin"
|
|
213
|
+
directory = "/Volumes/c/s/v/dir"
|
|
214
|
+
missing = "/Volumes/c/s/v/dir/missing"
|
|
215
|
+
|
|
216
|
+
files_api = _CountingFilesAPI(files=[file], directories=[directory])
|
|
217
|
+
client = MagicMock()
|
|
218
|
+
client.files = files_api
|
|
219
|
+
|
|
220
|
+
def calls_of(func):
|
|
221
|
+
files_api.calls.clear()
|
|
222
|
+
with contextlib.suppress(OSError):
|
|
223
|
+
func()
|
|
224
|
+
return list(files_api.calls)
|
|
225
|
+
|
|
226
|
+
with VolumeFileSystem(client=client) as fs:
|
|
227
|
+
# Files: one call
|
|
228
|
+
assert calls_of(lambda: fs.info(file)) == [("file", file)]
|
|
229
|
+
assert fs.info(file)["type"] == "file"
|
|
230
|
+
assert calls_of(lambda: fs.isfile(file)) == [("file", file)]
|
|
231
|
+
assert fs.isfile(file)
|
|
232
|
+
assert not fs.isfile(directory)
|
|
233
|
+
|
|
234
|
+
# Directories: one call to isdir, two to info
|
|
235
|
+
assert calls_of(lambda: fs.isdir(directory)) == [("directory", directory)]
|
|
236
|
+
assert fs.isdir(directory)
|
|
237
|
+
assert not fs.isdir(file)
|
|
238
|
+
assert calls_of(lambda: fs.info(directory)) == [
|
|
239
|
+
("file", directory),
|
|
240
|
+
("directory", directory),
|
|
241
|
+
]
|
|
242
|
+
assert fs.info(directory)["type"] == "directory"
|
|
243
|
+
|
|
244
|
+
# Missing paths
|
|
245
|
+
with pytest.raises(FileNotFoundError):
|
|
246
|
+
fs.info(missing)
|
|
247
|
+
|
|
248
|
+
# Opening a directory for writing: one call
|
|
249
|
+
with pytest.raises(IsADirectoryError):
|
|
250
|
+
fs.open(directory, "wb")
|
|
251
|
+
assert calls_of(lambda: fs.open(directory, "wb")) == [("directory", directory)]
|
|
252
|
+
|
|
253
|
+
# Volumes, schemas and catalogs are directories, and never files
|
|
254
|
+
assert calls_of(lambda: fs.isfile("/Volumes/c/s/v")) == []
|
|
255
|
+
assert not fs.isfile("/Volumes/c/s/v")
|
|
256
|
+
|
|
257
|
+
|
|
185
258
|
def test_volume_fs_workspace_id(client: WorkspaceClient):
|
|
186
259
|
with VolumeFileSystem(client=client) as fs:
|
|
187
260
|
workspace_id = fs.workspace_id
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
name: Check the source code
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_call:
|
|
5
|
-
inputs:
|
|
6
|
-
lint:
|
|
7
|
-
description: "Whether to run the linter."
|
|
8
|
-
type: boolean
|
|
9
|
-
required: false
|
|
10
|
-
default: true
|
|
11
|
-
|
|
12
|
-
concurrency:
|
|
13
|
-
group: "check-${{ github.ref_name }}"
|
|
14
|
-
cancel-in-progress: true
|
|
15
|
-
|
|
16
|
-
jobs:
|
|
17
|
-
lint:
|
|
18
|
-
name: Lint
|
|
19
|
-
if: ${{ inputs.lint }}
|
|
20
|
-
runs-on: ubuntu-latest
|
|
21
|
-
steps:
|
|
22
|
-
- uses: actions/checkout@v7
|
|
23
|
-
|
|
24
|
-
- name: Install uv
|
|
25
|
-
uses: astral-sh/setup-uv@v7
|
|
26
|
-
with:
|
|
27
|
-
enable-cache: true
|
|
28
|
-
|
|
29
|
-
- name: Run ruff check
|
|
30
|
-
run: uvx ruff check .
|
|
31
|
-
|
|
32
|
-
- name: Run ruff format check
|
|
33
|
-
run: uvx ruff format --check .
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
name: Actions for push events
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
paths:
|
|
6
|
-
- ".github/**"
|
|
7
|
-
- "src/**"
|
|
8
|
-
- "tests/**"
|
|
9
|
-
- "pyproject.toml"
|
|
10
|
-
|
|
11
|
-
concurrency:
|
|
12
|
-
group: "on-push-${{ github.ref_name }}"
|
|
13
|
-
cancel-in-progress: true
|
|
14
|
-
|
|
15
|
-
jobs:
|
|
16
|
-
check:
|
|
17
|
-
name: Check the source code
|
|
18
|
-
uses: ./.github/workflows/check.yaml
|
|
19
|
-
with:
|
|
20
|
-
lint: true
|
|
21
|
-
|
|
22
|
-
test:
|
|
23
|
-
name: Test Python package
|
|
24
|
-
needs:
|
|
25
|
-
- check
|
|
26
|
-
uses: ./.github/workflows/test.yaml
|
|
27
|
-
with:
|
|
28
|
-
python-version: "3.14"
|
|
29
|
-
coverage: ${{ github.ref_name == 'main' }}
|
|
30
|
-
environment: "default"
|
|
31
|
-
secrets: inherit
|
|
32
|
-
|
|
33
|
-
cross_version_test:
|
|
34
|
-
name: Test Python package (Cross version)
|
|
35
|
-
if: ${{ github.ref_name == 'main' }}
|
|
36
|
-
needs:
|
|
37
|
-
- check
|
|
38
|
-
strategy:
|
|
39
|
-
matrix:
|
|
40
|
-
python-version:
|
|
41
|
-
- "3.11"
|
|
42
|
-
- "3.12"
|
|
43
|
-
- "3.13"
|
|
44
|
-
uses: ./.github/workflows/test.yaml
|
|
45
|
-
with:
|
|
46
|
-
python-version: ${{ matrix.python-version }}
|
|
47
|
-
coverage: false
|
|
48
|
-
environment: "default"
|
|
49
|
-
secrets: inherit
|
|
50
|
-
|
|
51
|
-
test_on_azure:
|
|
52
|
-
name: Test Python package on Azure Databricks
|
|
53
|
-
if: ${{ contains(vars.TEST_ENVIRONMENTS, 'azure') }}
|
|
54
|
-
needs:
|
|
55
|
-
- check
|
|
56
|
-
uses: ./.github/workflows/test.yaml
|
|
57
|
-
with:
|
|
58
|
-
python-version: "3.14"
|
|
59
|
-
coverage: ${{ github.ref_name == 'main' }}
|
|
60
|
-
environment: "azure"
|
|
61
|
-
secrets: inherit
|
|
62
|
-
|
|
63
|
-
test_on_aws:
|
|
64
|
-
name: Test Python package on Databricks on AWS
|
|
65
|
-
needs:
|
|
66
|
-
- check
|
|
67
|
-
if: ${{ contains(vars.TEST_ENVIRONMENTS, 'aws') }}
|
|
68
|
-
uses: ./.github/workflows/test.yaml
|
|
69
|
-
with:
|
|
70
|
-
python-version: "3.14"
|
|
71
|
-
coverage: ${{ github.ref_name == 'main' }}
|
|
72
|
-
environment: "aws"
|
|
73
|
-
secrets: inherit
|
|
74
|
-
|
|
75
|
-
test_on_gcp:
|
|
76
|
-
name: Test Python package on Databricks on GCP
|
|
77
|
-
needs:
|
|
78
|
-
- check
|
|
79
|
-
if: ${{ contains(vars.TEST_ENVIRONMENTS, 'gcp') }}
|
|
80
|
-
uses: ./.github/workflows/test.yaml
|
|
81
|
-
with:
|
|
82
|
-
python-version: "3.14"
|
|
83
|
-
coverage: ${{ github.ref_name == 'main' }}
|
|
84
|
-
environment: "gcp"
|
|
85
|
-
secrets: inherit
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
name: Actions for release events
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
release:
|
|
5
|
-
types:
|
|
6
|
-
- published
|
|
7
|
-
|
|
8
|
-
concurrency:
|
|
9
|
-
group: "on-release-${{ github.ref_name }}"
|
|
10
|
-
cancel-in-progress: true
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
publish:
|
|
14
|
-
name: Publish Python package
|
|
15
|
-
uses: ./.github/workflows/publish.yaml
|
|
16
|
-
with:
|
|
17
|
-
environment: "pypi"
|
|
18
|
-
python-version: "3.14"
|
|
19
|
-
permissions:
|
|
20
|
-
id-token: write
|
|
21
|
-
contents: read
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
name: Publish Python package
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_call:
|
|
5
|
-
inputs:
|
|
6
|
-
environment:
|
|
7
|
-
description: "The environment to publish to. Should be either 'pypi' or 'testpypi'."
|
|
8
|
-
type: string
|
|
9
|
-
required: true
|
|
10
|
-
python-version:
|
|
11
|
-
description: "The Python version to use for building and publishing the package."
|
|
12
|
-
type: string
|
|
13
|
-
required: true
|
|
14
|
-
|
|
15
|
-
concurrency:
|
|
16
|
-
group: "publish-${{ github.ref_name }}"
|
|
17
|
-
cancel-in-progress: true
|
|
18
|
-
|
|
19
|
-
jobs:
|
|
20
|
-
publish:
|
|
21
|
-
name: Publish Python package to PyPI
|
|
22
|
-
runs-on: ubuntu-latest
|
|
23
|
-
environment:
|
|
24
|
-
name: ${{ inputs.environment }}
|
|
25
|
-
|
|
26
|
-
steps:
|
|
27
|
-
- uses: actions/checkout@v7
|
|
28
|
-
|
|
29
|
-
- name: Set up Python
|
|
30
|
-
uses: actions/setup-python@v7
|
|
31
|
-
with:
|
|
32
|
-
python-version: ${{ inputs.python-version }}
|
|
33
|
-
|
|
34
|
-
- name: Install uv
|
|
35
|
-
uses: astral-sh/setup-uv@v7
|
|
36
|
-
with:
|
|
37
|
-
enable-cache: true
|
|
38
|
-
|
|
39
|
-
- name: Build Python package
|
|
40
|
-
run: uv build
|
|
41
|
-
|
|
42
|
-
- name: Publish Python package
|
|
43
|
-
run: uv publish
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
name: Test Python package
|
|
2
|
-
run-name: Test the Python package
|
|
3
|
-
|
|
4
|
-
on:
|
|
5
|
-
workflow_call:
|
|
6
|
-
inputs:
|
|
7
|
-
python-version:
|
|
8
|
-
description: "The Python version to use for testing the package."
|
|
9
|
-
type: string
|
|
10
|
-
required: true
|
|
11
|
-
coverage:
|
|
12
|
-
description: "Whether to run tests with coverage reporting."
|
|
13
|
-
type: boolean
|
|
14
|
-
required: false
|
|
15
|
-
default: false
|
|
16
|
-
environment:
|
|
17
|
-
description: "The environment to run tests"
|
|
18
|
-
type: string
|
|
19
|
-
required: true
|
|
20
|
-
secrets:
|
|
21
|
-
DATABRICKS_HOST:
|
|
22
|
-
required: true
|
|
23
|
-
DATABRICKS_CLIENT_ID:
|
|
24
|
-
required: true
|
|
25
|
-
DATABRICKS_CLIENT_SECRET:
|
|
26
|
-
required: true
|
|
27
|
-
|
|
28
|
-
workflow_dispatch:
|
|
29
|
-
inputs:
|
|
30
|
-
python-version:
|
|
31
|
-
description: "The Python version to use for testing the package."
|
|
32
|
-
type: string
|
|
33
|
-
required: true
|
|
34
|
-
default: "3.14"
|
|
35
|
-
coverage:
|
|
36
|
-
description: "Whether to run tests with coverage reporting."
|
|
37
|
-
type: boolean
|
|
38
|
-
required: true
|
|
39
|
-
default: false
|
|
40
|
-
environment:
|
|
41
|
-
description: "The environment to run tests"
|
|
42
|
-
type: choice
|
|
43
|
-
required: true
|
|
44
|
-
options:
|
|
45
|
-
- default
|
|
46
|
-
- azure
|
|
47
|
-
- aws
|
|
48
|
-
- gcp
|
|
49
|
-
default: default
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
concurrency:
|
|
53
|
-
group: "test-${{ inputs.environment }}-${{ github.ref_name }}-${{ inputs.python-version }}"
|
|
54
|
-
cancel-in-progress: false
|
|
55
|
-
|
|
56
|
-
jobs:
|
|
57
|
-
|
|
58
|
-
test:
|
|
59
|
-
name: Test Python package
|
|
60
|
-
runs-on: ubuntu-latest
|
|
61
|
-
environment: ${{ inputs.environment }}
|
|
62
|
-
|
|
63
|
-
steps:
|
|
64
|
-
- uses: actions/checkout@v7
|
|
65
|
-
|
|
66
|
-
- name: Set up Python ${{ inputs.python-version }}
|
|
67
|
-
uses: actions/setup-python@v7
|
|
68
|
-
with:
|
|
69
|
-
python-version: ${{ inputs.python-version }}
|
|
70
|
-
|
|
71
|
-
- name: Install uv
|
|
72
|
-
uses: astral-sh/setup-uv@v7
|
|
73
|
-
with:
|
|
74
|
-
enable-cache: true
|
|
75
|
-
|
|
76
|
-
- name: Install the project
|
|
77
|
-
run: uv sync
|
|
78
|
-
|
|
79
|
-
- name: Set up env variables for working directories
|
|
80
|
-
shell: bash
|
|
81
|
-
run: |
|
|
82
|
-
test_path="${{ github.ref_name }}/${{ inputs.python-version }}"
|
|
83
|
-
|
|
84
|
-
has_error=no
|
|
85
|
-
if [[ "${{ vars.FSSPEC_DATABRICKS_VOLUME_TEST_ROOT }}" == "" ]]; then
|
|
86
|
-
echo "GitHub Actions variable FSSPEC_DATABRICKS_VOLUME_TEST_ROOT must be set."
|
|
87
|
-
has_error=yes
|
|
88
|
-
else
|
|
89
|
-
echo "FSSPEC_DATABRICKS_VOLUME_TEST_ROOT=${{ vars.FSSPEC_DATABRICKS_VOLUME_TEST_ROOT }}/${test_path}" >> $GITHUB_ENV
|
|
90
|
-
fi
|
|
91
|
-
|
|
92
|
-
if [[ "${{ vars.FSSPEC_DATABRICKS_WORKSPACE_TEST_ROOT }}" == "" ]]; then
|
|
93
|
-
echo "GitHub Actions variable FSSPEC_DATABRICKS_WORKSPACE_TEST_ROOT must be set."
|
|
94
|
-
has_error=yes
|
|
95
|
-
else
|
|
96
|
-
echo "FSSPEC_DATABRICKS_WORKSPACE_TEST_ROOT=${{ vars.FSSPEC_DATABRICKS_WORKSPACE_TEST_ROOT }}/${test_path}" >> $GITHUB_ENV
|
|
97
|
-
fi
|
|
98
|
-
|
|
99
|
-
if [[ "$has_error" = "yes" ]]; then
|
|
100
|
-
exit 1
|
|
101
|
-
fi
|
|
102
|
-
|
|
103
|
-
- name: Run test
|
|
104
|
-
if: ${{ !inputs.coverage }}
|
|
105
|
-
shell: bash
|
|
106
|
-
env:
|
|
107
|
-
DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
|
|
108
|
-
DATABRICKS_CLIENT_ID: ${{ secrets.DATABRICKS_CLIENT_ID }}
|
|
109
|
-
DATABRICKS_CLIENT_SECRET: ${{ secrets.DATABRICKS_CLIENT_SECRET }}
|
|
110
|
-
run: |
|
|
111
|
-
uv run pytest
|
|
112
|
-
|
|
113
|
-
- name: Run test with coverage
|
|
114
|
-
if: ${{ inputs.coverage }}
|
|
115
|
-
shell: bash
|
|
116
|
-
env:
|
|
117
|
-
DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
|
|
118
|
-
DATABRICKS_CLIENT_ID: ${{ secrets.DATABRICKS_CLIENT_ID }}
|
|
119
|
-
DATABRICKS_CLIENT_SECRET: ${{ secrets.DATABRICKS_CLIENT_SECRET }}
|
|
120
|
-
run: |
|
|
121
|
-
uv run pytest --cov \
|
|
122
|
-
--cov-report=term \
|
|
123
|
-
--cov-report=xml:coverage.xml \
|
|
124
|
-
--junitxml=junit.xml \
|
|
125
|
-
-o junit_family=legacy
|
|
126
|
-
|
|
127
|
-
- name: Upload coverage to Codecov
|
|
128
|
-
if: ${{ inputs.coverage && !cancelled() }}
|
|
129
|
-
uses: codecov/codecov-action@v7
|
|
130
|
-
with:
|
|
131
|
-
token: ${{ secrets.CODECOV_TOKEN }}
|
|
132
|
-
report_type: coverage
|
|
133
|
-
|
|
134
|
-
- name: Upload test results to Codecov
|
|
135
|
-
if: ${{ inputs.coverage && !cancelled() }}
|
|
136
|
-
uses: codecov/codecov-action@v7
|
|
137
|
-
with:
|
|
138
|
-
token: ${{ secrets.CODECOV_TOKEN }}
|
|
139
|
-
report_type: test_results
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
exclude: >
|
|
2
|
-
(?x)^(
|
|
3
|
-
\.tox/.*
|
|
4
|
-
)$
|
|
5
|
-
repos:
|
|
6
|
-
|
|
7
|
-
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
8
|
-
rev: v6.0.0
|
|
9
|
-
hooks:
|
|
10
|
-
- id: trailing-whitespace
|
|
11
|
-
- id: end-of-file-fixer
|
|
12
|
-
- id: check-json
|
|
13
|
-
- id: check-yaml
|
|
14
|
-
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
15
|
-
rev: v0.17.2
|
|
16
|
-
hooks:
|
|
17
|
-
- id: ruff-check
|
|
18
|
-
args: [ --fix, "--show-fixes"]
|
|
19
|
-
- id: ruff-format
|
|
20
|
-
types_or: [python]
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Commands
|
|
6
|
-
|
|
7
|
-
All Python goes through uv: never call `python` or `pip` directly.
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
uv sync # the package, editable, with the dev dependencies
|
|
11
|
-
uv run pytest # all tests: most need a Databricks workspace (see below)
|
|
12
|
-
uv run pytest tests/test_path.py -k volume # one file / matching tests
|
|
13
|
-
uv run pytest --cov # with coverage of src/
|
|
14
|
-
uvx ruff check . # lint (ruff is not a dev dependency; config in pyproject.toml)
|
|
15
|
-
uvx ruff format . # format
|
|
16
|
-
uvx pre-commit run --all-files # the hooks: ruff, whitespace, end of files, YAML
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
CI, in `.github/workflows/on_push.yaml`, runs on pushes:
|
|
20
|
-
|
|
21
|
-
- ruff check and ruff format check, with the latest ruff;
|
|
22
|
-
- pytest on Python 3.14 against the default workspace, with coverage on `main`;
|
|
23
|
-
- on `main`, pytest on the other supported Python versions;
|
|
24
|
-
- pytest on Azure, AWS or GCP workspaces named in the `TEST_ENVIRONMENTS` variable.
|
|
25
|
-
|
|
26
|
-
Releases are published GitHub releases. DEVELOPMENT.md describes them.
|
|
27
|
-
|
|
28
|
-
## Tests and layout
|
|
29
|
-
|
|
30
|
-
DEVELOPMENT.md describes the layout, the tests and their setup. Read its Tests section before writing tests. The traps:
|
|
31
|
-
|
|
32
|
-
- Most tests run against a real workspace. They need credentials and the two test root variables, from `.env` or the
|
|
33
|
-
environment.
|
|
34
|
-
- The session fixtures `volume_test_root` and `workspace_test_root` delete everything in the test roots first. Never
|
|
35
|
-
point them at folders holding anything else.
|
|
36
|
-
- Without credentials, run `tests/test_file.py`, `tests/test_volume.py`, `tests/test_path.py` and `tests/test_utils.py`.
|
|
37
|
-
`test_volume.py` and `test_file.py` use the fake Files API in `tests/dummy_api.py`.
|
|
38
|
-
- A test of anything a workspace serves gets a check without one where possible, on the fake API or with mocks.
|
|
39
|
-
- Test helpers shared by files are in `tests/utils.py`.
|
|
40
|
-
- `tests/conftest.py` sets `cachable = False`, so each test gets new file system instances.
|
|
41
|
-
|
|
42
|
-
## Architecture
|
|
43
|
-
|
|
44
|
-
DEVELOPMENT.md's Architecture section describes each part: read it before changing one. The traps:
|
|
45
|
-
|
|
46
|
-
- `DatabricksFileSystem` routes each call to a backend by path. It holds no file system logic of its own, except for
|
|
47
|
-
the root folder and transfers between backends.
|
|
48
|
-
- Its methods get the backend and the path to pass with `_resolve(path)`. Inside a workspace, that path is a local
|
|
49
|
-
path, and names returned are mapped back with `from_local_path`.
|
|
50
|
-
- Base class methods in `base.py` only check preconditions. Subclasses call `super()` first, then the API.
|
|
51
|
-
- Wrap Databricks SDK calls in `error_mapping(path)`, so that callers get `OSError` subclasses.
|
|
52
|
-
- Parse paths with the helpers of `path.py`. Every method takes `dbfs:/` URLs and POSIX paths alike.
|
|
53
|
-
- aiohttp objects are created and used in the Volume backend's IO event loop. `TCPConnector` needs a running loop.
|
|
54
|
-
- Never block on the IO loop from code running in it. File objects' synchronous methods raise there.
|
|
55
|
-
- `DatabricksFileSystem.__init__` takes every parameter of the SDK's `WorkspaceClient`, in its order.
|
|
56
|
-
`test_dbx_fs_accepts_workspace_client_params` fails when the SDK adds one: add it to the signature, the docstring
|
|
57
|
-
and `config_params`.
|
|
58
|
-
- File systems must stay picklable. A new attribute holding a client, a loop, a thread or a session is dropped in
|
|
59
|
-
`__getstate__` and reset in `__setstate__`.
|
|
60
|
-
- Multipart upload uses an undocumented Databricks API. Keep its fallback to resumable upload working.
|
|
61
|
-
- Volume write block sizes are multiples of 256 KB.
|
|
62
|
-
- Workspace files are cached whole in memory, up to the API's 10 MB.
|
|
63
|
-
- Chatty debug logs go under `if self.verbose_debug_log:`.
|
|
64
|
-
|
|
65
|
-
## Conventions
|
|
66
|
-
|
|
67
|
-
### Code
|
|
68
|
-
|
|
69
|
-
- Comments are short, and say why the code does something not obvious. Keep that density and tone.
|
|
70
|
-
- Type hints use built-in generics and `X | None`.
|
|
71
|
-
- Each module ends with `__all__`.
|
|
72
|
-
|
|
73
|
-
### Docstrings
|
|
74
|
-
|
|
75
|
-
Docstrings of the public API are in numpy style: `Parameters`, `Returns`, `Raises`. The public API is what users
|
|
76
|
-
construct or call:
|
|
77
|
-
|
|
78
|
-
- `DatabricksFileSystem` and `use`;
|
|
79
|
-
- `VolumeFileSystem`, `WorkspaceFileSystem` and `DBFS`, with their constructors and `open`.
|
|
80
|
-
|
|
81
|
-
Internal docstrings stay one line or short prose: file objects, helpers and underscore names.
|
|
82
|
-
|
|
83
|
-
### Writing docs
|
|
84
|
-
|
|
85
|
-
Write docs so that people can review them. That covers this file, DEVELOPMENT.md and the user docs.
|
|
86
|
-
|
|
87
|
-
- One idea per sentence: about 30 words, never more than 50.
|
|
88
|
-
- At most one parenthesis per sentence, never nested.
|
|
89
|
-
- Three or more items make a list.
|
|
90
|
-
- Add a new fact as its own sentence or bullet where it belongs. Never append it to an existing sentence.
|
|
91
|
-
- Name an identifier only when the reader needs it to find the code. A function's details belong in its docstring.
|
|
92
|
-
- Describe what is, not what was: git keeps the history.
|
|
93
|
-
- When editing a passage that breaks these rules, rewrite it.
|
|
94
|
-
|
|
95
|
-
### Where docs go
|
|
96
|
-
|
|
97
|
-
- README.md is the quick start: features, installation, paths, compatibility, status and limitations.
|
|
98
|
-
- USAGE.md has the user-facing details: operations, each backend's behaviour, and the options.
|
|
99
|
-
- AUTHENTICATION.md covers the ways to configure authentication.
|
|
100
|
-
- DEVELOPMENT.md has setup, tests, CI, releasing, the design choices and the architecture.
|
|
101
|
-
|
|
102
|
-
Update the docs when behaviour, parameters or options change. Keep user docs short and compact: what users do, not how
|
|
103
|
-
it works or why.
|
|
104
|
-
|
|
105
|
-
### Links
|
|
106
|
-
|
|
107
|
-
README.md, USAGE.md and AUTHENTICATION.md are user docs. README.md uses absolute GitHub URLs for links to files, as
|
|
108
|
-
PyPI shows it but cannot resolve relative paths. The other docs are read on GitHub only, so they use relative paths.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|