fsspec-databricks 0.2.0__tar.gz → 0.3.0__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.3.0}/.gitignore +0 -3
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/DEVELOPMENT.md +105 -22
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/PKG-INFO +3 -2
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/README.md +2 -1
- fsspec_databricks-0.3.0/SECURITY.md +30 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/USAGE.md +8 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/pyproject.toml +9 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/base.py +24 -22
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/dbfs.py +3 -10
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/spec.py +23 -27
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/volume.py +294 -100
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/conftest.py +1 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/dummy_api.py +10 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_volume.py +117 -2
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_volume_fs.py +321 -3
- 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.3.0}/AUTHENTICATION.md +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/LICENSE +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/__init__.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/error.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/file.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/local.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/path.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/py.typed +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/utils.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/workspace.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/__init__.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_base_fs.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_file.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_path.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_spec_fs.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_utils.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_workspace_fs.py +0 -0
- {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/utils.py +0 -0
|
@@ -105,21 +105,63 @@ 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`.
|
|
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
|
+
The SDK's `github-oidc` exchanges a new token for each request, which slows the tests down.
|
|
151
|
+
So `.github/scripts/with-id-token-file.sh` keeps GitHub's token in a file and runs the tests with `file-oidc`, which
|
|
152
|
+
caches the exchanged token.
|
|
153
|
+
- **`github-oidc-azure`**: a federated credential on the Entra ID app, with the audience `api://AzureADTokenExchange`.
|
|
154
|
+
The app is the workspace's service principal. This needs no access to the account console.
|
|
155
|
+
|
|
156
|
+
With federation, the client secret is not passed to the tests, even if the environment still has one.
|
|
157
|
+
|
|
158
|
+
The environments are:
|
|
118
159
|
|
|
119
|
-
|
|
|
120
|
-
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
160
|
+
| Environment | Workspace | Authentication |
|
|
161
|
+
|-------------|----------------------------------|---------------------|
|
|
162
|
+
| `default` | Databricks Free Edition | Client secret |
|
|
163
|
+
| `azure` | Azure Databricks | `github-oidc-azure` |
|
|
164
|
+
| `gcp` | Databricks on Google Cloud | `github-oidc` |
|
|
123
165
|
|
|
124
166
|
At the repository level:
|
|
125
167
|
|
|
@@ -138,6 +180,12 @@ The version comes from the git tag, through hatch-vcs.
|
|
|
138
180
|
|
|
139
181
|
`uv publish` authenticates to PyPI with the workflow's OIDC token, through trusted publishing.
|
|
140
182
|
|
|
183
|
+
Repository rules protect the release path:
|
|
184
|
+
|
|
185
|
+
- The ruleset "Release tags" lets only repository admins create, move or delete `v*` tags.
|
|
186
|
+
- The `pypi` environment deploys only from `v*` tags.
|
|
187
|
+
- The ruleset "Default branch" blocks force-pushes to `main` and its deletion.
|
|
188
|
+
|
|
141
189
|
## Design
|
|
142
190
|
|
|
143
191
|
### One file system for all of Databricks' paths
|
|
@@ -171,14 +219,19 @@ exception.
|
|
|
171
219
|
|
|
172
220
|
### Picklable
|
|
173
221
|
|
|
174
|
-
File systems are pickled to run in other processes, e.g. by Dask.
|
|
175
|
-
|
|
222
|
+
File systems are pickled to run in other processes, e.g. by Dask. fsspec pickles a file system as its constructor
|
|
223
|
+
arguments, and unpickling calls the constructor again:
|
|
224
|
+
|
|
225
|
+
- clients, event loops, threads and sessions are not pickled, and are made again on first use;
|
|
226
|
+
- attributes set after construction are lost;
|
|
227
|
+
- every constructor argument must be picklable.
|
|
176
228
|
|
|
177
|
-
A `WorkspaceClient` given as `client` is
|
|
229
|
+
A `WorkspaceClient` given as `client` is not picklable. Other ways of authenticating are.
|
|
178
230
|
|
|
179
231
|
### Known limits
|
|
180
232
|
|
|
181
|
-
- **Multipart upload
|
|
233
|
+
- **Multipart upload is not in the REST reference.** The Databricks SDK for Python uses the same endpoints, as an
|
|
234
|
+
experimental feature, in `databricks/sdk/mixins/files.py`. Compare with it when the API changes.
|
|
182
235
|
- **Workspace files are at most 10 MB.** The Workspace API's limit.
|
|
183
236
|
- **No append or update modes on Volumes.** `ab` and `r+b` are refused.
|
|
184
237
|
- **No listing cache.** Each call reaches the workspace.
|
|
@@ -225,7 +278,6 @@ Within one backend, both go to the backend.
|
|
|
225
278
|
|
|
226
279
|
- the Databricks SDK `Config` and `WorkspaceClient`, made on first use from `client`, `config` or `config_params`;
|
|
227
280
|
- `close()`, also called at exit and on garbage collection;
|
|
228
|
-
- pickling without the client and config;
|
|
229
281
|
- the `dbfs` protocol: `_strip_protocol` and `unstrip_protocol` use `path.py`.
|
|
230
282
|
|
|
231
283
|
Its `mkdir`, `makedirs`, `rmdir` and `_rm` only check preconditions, such as a missing parent or a file where a folder
|
|
@@ -263,20 +315,48 @@ when no glob pattern is involved. Files are opened with the SDK's `dbfs.open`.
|
|
|
263
315
|
|
|
264
316
|
### Volumes
|
|
265
317
|
|
|
266
|
-
`VolumeFileSystem`, in `volume.py`, subclasses `DBFS
|
|
318
|
+
`VolumeFileSystem`, in `volume.py`, subclasses `DBFS`. It keeps the SDK's `dbfs` for `exists` and for creating
|
|
319
|
+
folders. Listings, copies, moves and deletes inside a volume use the Files API and the backend's own file objects.
|
|
267
320
|
|
|
268
321
|
The APIs called depend on the depth of the path:
|
|
269
322
|
|
|
270
|
-
| Path | `info` | `ls`
|
|
271
|
-
|
|
272
|
-
| `/Volumes` | None: a fixed folder | `catalogs.list`
|
|
273
|
-
| `/Volumes/<catalog>` | `catalogs.get` | `schemas.list`
|
|
274
|
-
| `/Volumes/<catalog>/<schema>` | `schemas.get` | `volumes.list`
|
|
275
|
-
| `/Volumes/<catalog>/<schema>/<volume>` | `volumes.read` | `
|
|
276
|
-
| Deeper | `files.
|
|
323
|
+
| Path | `info` | `ls` |
|
|
324
|
+
|----------------------------------------|-----------------------------------------------------------|---------------------------------|
|
|
325
|
+
| `/Volumes` | None: a fixed folder | `catalogs.list` |
|
|
326
|
+
| `/Volumes/<catalog>` | `catalogs.get` | `schemas.list` |
|
|
327
|
+
| `/Volumes/<catalog>/<schema>` | `schemas.get` | `volumes.list` |
|
|
328
|
+
| `/Volumes/<catalog>/<schema>/<volume>` | `volumes.read` | `files.list_directory_contents` |
|
|
329
|
+
| Deeper | `files.get_metadata`, else `files.get_directory_metadata` | `files.list_directory_contents` |
|
|
330
|
+
|
|
331
|
+
`ls` of a file returns the file itself, from `files.get_metadata` after the listing fails.
|
|
277
332
|
|
|
278
333
|
`include_browse` is passed to the Unity Catalog APIs.
|
|
279
334
|
|
|
335
|
+
Each metadata call is a round trip, so the checks inside a volume make as few as they can:
|
|
336
|
+
|
|
337
|
+
| Check | Calls |
|
|
338
|
+
|-----------------------------------------------|-------------------------------------------------|
|
|
339
|
+
| `info` of a file, so also `size` and reading | 1: `files.get_metadata` |
|
|
340
|
+
| `info` of a directory | 2: `files.get_metadata`, then the directory's |
|
|
341
|
+
| `isdir`, so also `mkdir` and `rmdir` | 1: `files.get_directory_metadata` |
|
|
342
|
+
| `isfile` | 1: `files.get_metadata` |
|
|
343
|
+
| Opening for writing | 1: `isdir`, to refuse directories |
|
|
344
|
+
| `ls` of a directory | 1: `files.list_directory_contents` |
|
|
345
|
+
|
|
346
|
+
`DatabricksFileSystem` passes `isdir` and `isfile` to the backends. `test_volume_fs_metadata_calls` counts the calls.
|
|
347
|
+
|
|
348
|
+
Copies, moves and deletes inside a volume:
|
|
349
|
+
|
|
350
|
+
- `cp_file` streams the file from a `VolumeReadableFile` into a `VolumeWritableFile`, as the Files API has no copy. The
|
|
351
|
+
SDK's `dbfs.copy` would hold the whole file in memory.
|
|
352
|
+
- `mv` is fsspec's default: a copy, then a delete.
|
|
353
|
+
- `rm` of a file checks it with `info`, then calls `files.delete`.
|
|
354
|
+
- A recursive `rm` lists the tree one depth at a time, deletes the files, then the folders from the deepest. Each step
|
|
355
|
+
runs up to `_max_delete_concurrency` requests in a thread pool.
|
|
356
|
+
- Paths of volumes, schemas and catalogs go to `DBFS`.
|
|
357
|
+
|
|
358
|
+
`test_volume_fs_rm_calls` and `test_volume_fs_cp_file_streams` check these on a fake `WorkspaceClient.files`.
|
|
359
|
+
|
|
280
360
|
#### The IO event loop
|
|
281
361
|
|
|
282
362
|
Each `VolumeFileSystem` runs an asyncio event loop in a daemon thread, `dbfs-io-thread`, started on first use. It
|
|
@@ -312,6 +392,9 @@ loop:
|
|
|
312
392
|
`_compute_block_size` gives the block size: the smallest power-of-2 multiple of the minimum that covers the target with
|
|
313
393
|
`max_concurrency` blocks, capped at the maximum.
|
|
314
394
|
|
|
395
|
+
A range request that gets a `429`, `500`, `502`, `503` or `504` is retried up to 3 times, with exponential backoff and
|
|
396
|
+
jitter. Writes are not retried: a repeated `PUT` can fail with "already exists" when the first one went through.
|
|
397
|
+
|
|
315
398
|
#### Writing
|
|
316
399
|
|
|
317
400
|
`VolumeWritableFile` subclasses `AbstractAsyncWritableFile`. Writes are buffered:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fsspec-databricks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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.
|
|
@@ -166,6 +166,14 @@ fs.ls("dbfs:/Volumes/my_catalog/my_schema/my_volume/data")
|
|
|
166
166
|
Reads and writes run concurrent requests. Large writes use multipart upload, or resumable upload where the workspace
|
|
167
167
|
requires it.
|
|
168
168
|
|
|
169
|
+
Reads retry after transient server errors. Writes do not.
|
|
170
|
+
|
|
171
|
+
Copies, moves and deletes:
|
|
172
|
+
|
|
173
|
+
- A copy reads the file and writes it again, as the Files API cannot copy. It uses the options for reads and writes.
|
|
174
|
+
- A move is a copy, then a delete. It replaces an existing file at the destination.
|
|
175
|
+
- A recursive delete sends concurrent requests.
|
|
176
|
+
|
|
169
177
|
### Modes
|
|
170
178
|
|
|
171
179
|
| Mode | Supported | Notes |
|
|
@@ -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",
|
|
@@ -90,17 +90,6 @@ class AbstractDatabricksFileSystem(AbstractFileSystem):
|
|
|
90
90
|
_register_cleanup(self)
|
|
91
91
|
self.log.debug("Opened: %s", self)
|
|
92
92
|
|
|
93
|
-
def __getstate__(self):
|
|
94
|
-
state = self.__dict__.copy()
|
|
95
|
-
del state["_AbstractDatabricksFileSystem__client"]
|
|
96
|
-
del state["_AbstractDatabricksFileSystem__config"]
|
|
97
|
-
return state
|
|
98
|
-
|
|
99
|
-
def __setstate__(self, state):
|
|
100
|
-
self.__dict__.update(state)
|
|
101
|
-
self.__client = None
|
|
102
|
-
self.__config = None
|
|
103
|
-
|
|
104
93
|
def close(self):
|
|
105
94
|
"""Close the file system and release any resources."""
|
|
106
95
|
if not self.closed:
|
|
@@ -197,6 +186,16 @@ class AbstractDatabricksFileSystem(AbstractFileSystem):
|
|
|
197
186
|
def cp_file(self, path1, path2, **kwargs):
|
|
198
187
|
self.log.debug("Copying file: path1=%s, path2=%s", path1, path2)
|
|
199
188
|
|
|
189
|
+
def _cp_directory(self, path2):
|
|
190
|
+
"""Copy a directory: create the destination, as directories have no content."""
|
|
191
|
+
try:
|
|
192
|
+
info = self.info(path2)
|
|
193
|
+
except FileNotFoundError:
|
|
194
|
+
self.mkdirs(path2)
|
|
195
|
+
return
|
|
196
|
+
if info["type"] != "directory":
|
|
197
|
+
raise file_exists_error(path2)
|
|
198
|
+
|
|
200
199
|
def _rm(self, path):
|
|
201
200
|
# Implement only the precondition check here
|
|
202
201
|
# Actual operations must be implemented by subclasses
|
|
@@ -213,15 +212,17 @@ class AbstractDatabricksFileSystem(AbstractFileSystem):
|
|
|
213
212
|
"Making directory: path=%s, create_parents=%s", path, create_parents
|
|
214
213
|
)
|
|
215
214
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
215
|
+
parent = self._parent(path)
|
|
216
|
+
if not self.isdir(parent):
|
|
217
|
+
# Tell a missing parent from a file
|
|
218
|
+
try:
|
|
219
|
+
self.info(parent)
|
|
220
|
+
except FileNotFoundError:
|
|
221
|
+
if create_parents:
|
|
222
|
+
return
|
|
223
|
+
else:
|
|
224
|
+
raise
|
|
225
|
+
raise not_a_directory_error(parent)
|
|
225
226
|
|
|
226
227
|
if self.exists(path):
|
|
227
228
|
raise file_exists_error(path)
|
|
@@ -248,8 +249,9 @@ class AbstractDatabricksFileSystem(AbstractFileSystem):
|
|
|
248
249
|
# Actual operations must be implemented by subclasses
|
|
249
250
|
self.log.debug("Deleting directory: path=%s", path)
|
|
250
251
|
|
|
251
|
-
|
|
252
|
-
|
|
252
|
+
if not self.isdir(path):
|
|
253
|
+
# Raises FileNotFoundError if the path is missing
|
|
254
|
+
self.info(path)
|
|
253
255
|
raise not_a_directory_error(path)
|
|
254
256
|
|
|
255
257
|
def open(
|
|
@@ -8,7 +8,7 @@ from databricks.sdk.config import Config
|
|
|
8
8
|
from databricks.sdk.service.files import FileInfo
|
|
9
9
|
|
|
10
10
|
from .base import AbstractDatabricksFileSystem
|
|
11
|
-
from .error import error_mapping,
|
|
11
|
+
from .error import error_mapping, is_a_directory_error
|
|
12
12
|
from .path import parse_dbfs_path
|
|
13
13
|
from .utils import to_datetime
|
|
14
14
|
|
|
@@ -80,15 +80,8 @@ class DBFS(AbstractDatabricksFileSystem):
|
|
|
80
80
|
|
|
81
81
|
source_info = self.info(path1)
|
|
82
82
|
if source_info["type"] == "directory":
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if dest_info["type"] == "directory":
|
|
86
|
-
return
|
|
87
|
-
else:
|
|
88
|
-
raise file_exists_error(path2)
|
|
89
|
-
except FileNotFoundError:
|
|
90
|
-
self.mkdirs(path2)
|
|
91
|
-
return
|
|
83
|
+
self._cp_directory(path2)
|
|
84
|
+
return
|
|
92
85
|
|
|
93
86
|
with error_mapping(path1):
|
|
94
87
|
self.client.dbfs.copy(
|
|
@@ -10,7 +10,7 @@ from fsspec.spec import AbstractFileSystem
|
|
|
10
10
|
|
|
11
11
|
from .base import AbstractDatabricksFileSystem, root_dir_info
|
|
12
12
|
from .dbfs import DBFS
|
|
13
|
-
from .error import
|
|
13
|
+
from .error import file_not_found_error
|
|
14
14
|
from .local import DatabricksLocalFileSystem
|
|
15
15
|
from .path import (
|
|
16
16
|
FileSystemType,
|
|
@@ -293,23 +293,6 @@ class DatabricksFileSystem(AbstractDatabricksFileSystem):
|
|
|
293
293
|
self.__local_fs = None
|
|
294
294
|
super().close()
|
|
295
295
|
|
|
296
|
-
def __getstate__(self):
|
|
297
|
-
state = super().__getstate__()
|
|
298
|
-
del state["_DatabricksFileSystem__local_fs"]
|
|
299
|
-
del state["_DatabricksFileSystem__workspace_fs"]
|
|
300
|
-
del state["_DatabricksFileSystem__volume_fs"]
|
|
301
|
-
del state["_DatabricksFileSystem__legacy_fs"]
|
|
302
|
-
del state["_running_in_workspace"]
|
|
303
|
-
return state
|
|
304
|
-
|
|
305
|
-
def __setstate__(self, state):
|
|
306
|
-
super().__setstate__(state)
|
|
307
|
-
self.__workspace_fs = None
|
|
308
|
-
self.__volume_fs = None
|
|
309
|
-
self.__legacy_fs = None
|
|
310
|
-
self.__local_fs = None
|
|
311
|
-
self._running_in_workspace = None
|
|
312
|
-
|
|
313
296
|
@property
|
|
314
297
|
def _volume_fs(self) -> VolumeFileSystem:
|
|
315
298
|
"""Get or create the UnityCatalogVolumeFileSystem instance."""
|
|
@@ -458,6 +441,26 @@ class DatabricksFileSystem(AbstractDatabricksFileSystem):
|
|
|
458
441
|
fs, p = self._resolve(path)
|
|
459
442
|
return fs.exists(p, **kwargs)
|
|
460
443
|
|
|
444
|
+
def isdir(self, path) -> bool:
|
|
445
|
+
if self._strip_protocol(path) == "/":
|
|
446
|
+
return True
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
fs, p = self._resolve(path)
|
|
450
|
+
except FileNotFoundError:
|
|
451
|
+
return False
|
|
452
|
+
return fs.isdir(p)
|
|
453
|
+
|
|
454
|
+
def isfile(self, path) -> bool:
|
|
455
|
+
if self._strip_protocol(path) == "/":
|
|
456
|
+
return False
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
fs, p = self._resolve(path)
|
|
460
|
+
except FileNotFoundError:
|
|
461
|
+
return False
|
|
462
|
+
return fs.isfile(p)
|
|
463
|
+
|
|
461
464
|
def created(self, path):
|
|
462
465
|
fs, p = self._resolve(path)
|
|
463
466
|
return fs.created(p)
|
|
@@ -475,15 +478,8 @@ class DatabricksFileSystem(AbstractDatabricksFileSystem):
|
|
|
475
478
|
else:
|
|
476
479
|
source_info = self.info(path1)
|
|
477
480
|
if source_info["type"] == "directory":
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if dest_info["type"] == "directory":
|
|
481
|
-
return
|
|
482
|
-
else:
|
|
483
|
-
raise file_exists_error(path2)
|
|
484
|
-
except FileNotFoundError:
|
|
485
|
-
self.mkdirs(path2)
|
|
486
|
-
return
|
|
481
|
+
self._cp_directory(path2)
|
|
482
|
+
return
|
|
487
483
|
|
|
488
484
|
src_fs, src_path = self._resolve(path1)
|
|
489
485
|
dst_fs, dst_path = self._resolve(path2)
|