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.
Files changed (41) hide show
  1. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/.gitignore +0 -3
  2. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/DEVELOPMENT.md +105 -22
  3. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/PKG-INFO +3 -2
  4. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/README.md +2 -1
  5. fsspec_databricks-0.3.0/SECURITY.md +30 -0
  6. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/USAGE.md +8 -0
  7. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/pyproject.toml +9 -0
  8. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/base.py +24 -22
  9. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/dbfs.py +3 -10
  10. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/spec.py +23 -27
  11. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/volume.py +294 -100
  12. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/conftest.py +1 -0
  13. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/dummy_api.py +10 -0
  14. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_volume.py +117 -2
  15. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_volume_fs.py +321 -3
  16. fsspec_databricks-0.2.0/.github/dependabot.yml +0 -11
  17. fsspec_databricks-0.2.0/.github/workflows/check.yaml +0 -33
  18. fsspec_databricks-0.2.0/.github/workflows/on_push.yaml +0 -85
  19. fsspec_databricks-0.2.0/.github/workflows/on_release.yaml +0 -21
  20. fsspec_databricks-0.2.0/.github/workflows/publish.yaml +0 -43
  21. fsspec_databricks-0.2.0/.github/workflows/test.yaml +0 -139
  22. fsspec_databricks-0.2.0/.pre-commit-config.yaml +0 -20
  23. fsspec_databricks-0.2.0/CLAUDE.md +0 -108
  24. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/AUTHENTICATION.md +0 -0
  25. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/LICENSE +0 -0
  26. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/__init__.py +0 -0
  27. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/error.py +0 -0
  28. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/file.py +0 -0
  29. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/local.py +0 -0
  30. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/path.py +0 -0
  31. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/py.typed +0 -0
  32. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/utils.py +0 -0
  33. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/src/fsspec_databricks/workspace.py +0 -0
  34. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/__init__.py +0 -0
  35. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_base_fs.py +0 -0
  36. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_file.py +0 -0
  37. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_path.py +0 -0
  38. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_spec_fs.py +0 -0
  39. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_utils.py +0 -0
  40. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/test_workspace_fs.py +0 -0
  41. {fsspec_databricks-0.2.0 → fsspec_databricks-0.3.0}/tests/utils.py +0 -0
@@ -82,9 +82,6 @@ ENV/
82
82
  # vi*
83
83
  *.swp
84
84
 
85
- # UV
86
- uv.lock
87
-
88
85
  build/
89
86
  downstream/*
90
87
 
@@ -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
- | Secret | Value |
114
- |----------------------------|-----------------------------------------------|
115
- | `DATABRICKS_HOST` | The workspace's URL |
116
- | `DATABRICKS_CLIENT_ID` | The service principal's client ID |
117
- | `DATABRICKS_CLIENT_SECRET` | The service principal's client secret |
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
- | Variable | Value |
120
- |-----------------------------------------|-----------------------------------------------|
121
- | `FSSPEC_DATABRICKS_VOLUME_TEST_ROOT` | The POSIX path of the Volume work directory |
122
- | `FSSPEC_DATABRICKS_WORKSPACE_TEST_ROOT` | The POSIX path of the Workspace work directory |
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. Clients, event loops, threads and sessions are left
175
- out, and made again on first use.
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 kept, and is not picklable. Other ways of authenticating are.
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 uses an undocumented API.** Databricks may change it without notice.
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`, as the DBFS API also lists Volume folders.
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` | `dbfs.list` |
276
- | Deeper | `files.get_directory_metadata`, else `files.get_metadata` | `dbfs.list` |
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.2.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 Databricks' multipart upload API, which is undocumented and may change without notice.
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 Databricks' multipart upload API, which is undocumented and may change without notice.
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
- try:
217
- info = self.info(self._parent(path))
218
- if info["type"] != "directory":
219
- raise not_a_directory_error(self._parent(path))
220
- except FileNotFoundError:
221
- if create_parents:
222
- return
223
- else:
224
- raise
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
- info = self.info(path)
252
- if info["type"] != "directory":
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, file_exists_error, is_a_directory_error
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
- try:
84
- dest_info = self.info(path2)
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 file_exists_error, file_not_found_error
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
- try:
479
- dest_info = self.info(path2)
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)