fuse4dbricks 0.7.2__tar.gz → 0.7.3__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.
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/CHANGELOG.md +19 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/PKG-INFO +1 -1
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/auth/provider.py +17 -3
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/fs/operations.py +18 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/pyproject.toml +1 -1
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_auth_provider.py +25 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_operations.py +46 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/uv.lock +1 -1
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/.env.example +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/.github/workflows/test.yml +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/.gitignore +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/README.md +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/__init__.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/api/__init__.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/api/errors.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/api/uc_client.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/auth/__init__.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/fs/__init__.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/fs/auth_manager.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/fs/data_manager.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/fs/inode_manager.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/fs/metadata_manager.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/fs/ram_cache.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/fs/securable_filter.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/fs/utils.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/fs/write_buffer.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/main.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/storage/__init__.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/fuse4dbricks/storage/persistence.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_auth_manager.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_data_manager.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_e2e_mount.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_inode_manager.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_main.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_metadata_manager.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_persistence.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_securable_filter.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_uc_client.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_uc_client_consistency.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_uc_client_live.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_utils.py +0 -0
- {fuse4dbricks-0.7.2 → fuse4dbricks-0.7.3}/tests/test_write_buffer.py +0 -0
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
# 0.7.3 (2026-06-19)
|
|
2
|
+
|
|
3
|
+
- Fix `mv` of a freshly written file failing with an I/O error. `rename()`
|
|
4
|
+
copies the source by downloading it with an `If-Unmodified-Since` guard built
|
|
5
|
+
from the inode's `st_mtime`. A file just created/written through the mount
|
|
6
|
+
carries a locally-stamped mtime (the `create()`-time wall clock), which is
|
|
7
|
+
earlier than the server's `Last-Modified` (assigned when the upload
|
|
8
|
+
completed), so the server reported `412 Precondition Failed` and the copy read
|
|
9
|
+
failed with `EIO`. `rename()` now refreshes the source's metadata from the
|
|
10
|
+
server before copying, so the guard matches the server's view. (Ordinary reads
|
|
11
|
+
were unaffected: they refresh via `getattr` before reading; only `rename`
|
|
12
|
+
bypassed that.)
|
|
13
|
+
- Stop logging a missing Databricks config file at `ERROR`. A uid without a
|
|
14
|
+
Databricks setup (e.g. `root` or a system daemon probing the mount) produced
|
|
15
|
+
three error lines per request, flooding the journal. A missing default
|
|
16
|
+
`~/.databrickscfg` and the resulting "no token" outcome are now logged at
|
|
17
|
+
`debug`; genuinely unexpected stat failures (e.g. permission denied) are
|
|
18
|
+
logged at `warning`.
|
|
19
|
+
|
|
1
20
|
# 0.7.2 (2026-06-19)
|
|
2
21
|
|
|
3
22
|
- Fix a just-created file or directory being reported as nonexistent for up to
|
|
@@ -171,8 +171,17 @@ class DatabricksUnifiedAuthProvider:
|
|
|
171
171
|
"""
|
|
172
172
|
try:
|
|
173
173
|
st = os.stat(config_file)
|
|
174
|
+
except FileNotFoundError:
|
|
175
|
+
# A missing config file is the normal state for any uid without a
|
|
176
|
+
# Databricks setup (e.g. root or a system daemon probing the mount).
|
|
177
|
+
# Logging it at error level floods the journal with one line per
|
|
178
|
+
# request, so keep it at debug.
|
|
179
|
+
logger.debug("Config file %s does not exist, ignoring", config_file)
|
|
180
|
+
return False
|
|
174
181
|
except OSError as exc:
|
|
175
|
-
|
|
182
|
+
# Other stat failures (permission denied, etc.) are unexpected and
|
|
183
|
+
# worth surfacing, but are not fatal to the mount.
|
|
184
|
+
logger.warning("Cannot stat config file %s: %s", config_file, exc)
|
|
176
185
|
return False
|
|
177
186
|
if not stat.S_ISREG(st.st_mode):
|
|
178
187
|
logger.error("Config file %s is not a regular file, ignoring", config_file)
|
|
@@ -216,7 +225,9 @@ class DatabricksUnifiedAuthProvider:
|
|
|
216
225
|
config_file = default_config
|
|
217
226
|
|
|
218
227
|
if config_file is None:
|
|
219
|
-
|
|
228
|
+
# Expected for any uid without a Databricks config; debug-level so a
|
|
229
|
+
# repeatedly-probing uid (e.g. root) does not flood the journal.
|
|
230
|
+
logger.debug("No databricks config file found for uid %s. Checked environment variable DATABRICKS_CONFIG_FILE and default location", uid)
|
|
220
231
|
return None, None
|
|
221
232
|
return profile, config_file
|
|
222
233
|
|
|
@@ -247,7 +258,10 @@ class DatabricksUnifiedAuthProvider:
|
|
|
247
258
|
# The process does not define a token, but defines a profile or config file:
|
|
248
259
|
profile, config_file = self._get_profile_and_config_file_name(env, ctx.uid)
|
|
249
260
|
if profile is None or config_file is None:
|
|
250
|
-
|
|
261
|
+
# Routine when a uid has neither a token in its environment nor a
|
|
262
|
+
# config file; the caller turns this into EACCES. Debug-level to
|
|
263
|
+
# avoid one error line per request from a probing uid.
|
|
264
|
+
logger.debug("No databricks token found in environment variables for pid %s, and no profile or config file found for uid %s", ctx.pid, ctx.uid)
|
|
251
265
|
return None
|
|
252
266
|
return self._get_token_from_config(profile, config_file)
|
|
253
267
|
|
|
@@ -757,6 +757,24 @@ class UnityCatalogFS(pyfuse3.Operations):
|
|
|
757
757
|
if src_entry.is_dir:
|
|
758
758
|
raise pyfuse3.FUSEError(errno.EXDEV)
|
|
759
759
|
|
|
760
|
+
# Refresh the source's metadata from the server before copying. A file
|
|
761
|
+
# just created/written through this mount carries a locally-stamped
|
|
762
|
+
# st_mtime (the create()-time wall clock), which is earlier than the
|
|
763
|
+
# server's Last-Modified (assigned when the upload completed). The copy
|
|
764
|
+
# below sends that mtime as If-Unmodified-Since; a too-early value makes
|
|
765
|
+
# the server report 412 (file changed since) and the read fails with
|
|
766
|
+
# EIO, so `mv` of a freshly-written file breaks. getattr() refreshes the
|
|
767
|
+
# inode on the ordinary read path but rename() never does, so do it here.
|
|
768
|
+
try:
|
|
769
|
+
refreshed = await self.metadata_manager.get_attributes(src_entry, ctx)
|
|
770
|
+
except pyfuse3.FUSEError:
|
|
771
|
+
raise
|
|
772
|
+
except Exception as exc:
|
|
773
|
+
self._raise_fuse_error(exc, fs_path=old_fs_path, op="rename")
|
|
774
|
+
if refreshed is None:
|
|
775
|
+
raise pyfuse3.FUSEError(errno.ENOENT)
|
|
776
|
+
src_entry.attr.update(refreshed)
|
|
777
|
+
|
|
760
778
|
# Inspect the destination for NOREPLACE / file-over-dir rules.
|
|
761
779
|
try:
|
|
762
780
|
dst_attr = await self.metadata_manager.lookup_child(new_parent, new_name, ctx)
|
|
@@ -184,6 +184,31 @@ def test_profile_config_nothing_found(unified, tmp_path, monkeypatch):
|
|
|
184
184
|
assert (profile, config_file) == (None, None)
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
def test_missing_default_config_is_not_logged_as_error(unified, tmp_path, monkeypatch, caplog):
|
|
188
|
+
"""A missing ~/.databrickscfg is the normal state for a uid without a
|
|
189
|
+
Databricks setup (e.g. root probing the mount). It must not be logged at
|
|
190
|
+
error level, or a repeatedly-probing uid floods the journal with one line
|
|
191
|
+
per request."""
|
|
192
|
+
monkeypatch.setattr(unified, "_home_for_uid", lambda uid: str(tmp_path / "empty"))
|
|
193
|
+
with caplog.at_level("DEBUG", logger="fuse4dbricks.auth.provider"):
|
|
194
|
+
profile, config_file = unified._get_profile_and_config_file_name(env=None, uid=os.getuid())
|
|
195
|
+
assert (profile, config_file) == (None, None)
|
|
196
|
+
# The benign "does not exist" path logs at debug, never at error/warning.
|
|
197
|
+
assert not [r for r in caplog.records if r.levelname in ("ERROR", "WARNING")]
|
|
198
|
+
assert any(r.levelname == "DEBUG" for r in caplog.records)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_unreadable_config_is_logged_as_warning(unified, tmp_path, monkeypatch, caplog):
|
|
202
|
+
"""A stat failure that is NOT 'file missing' (e.g. permission denied) is
|
|
203
|
+
unexpected and stays visible at warning level."""
|
|
204
|
+
def _boom(path):
|
|
205
|
+
raise PermissionError("denied")
|
|
206
|
+
monkeypatch.setattr(provider_mod.os, "stat", _boom)
|
|
207
|
+
with caplog.at_level("DEBUG", logger="fuse4dbricks.auth.provider"):
|
|
208
|
+
assert unified._is_safe_config_file(str(tmp_path / "cfg"), os.getuid()) is False
|
|
209
|
+
assert any(r.levelname == "WARNING" for r in caplog.records)
|
|
210
|
+
|
|
211
|
+
|
|
187
212
|
# ---------------------------------------------------------------------------
|
|
188
213
|
# _get_token_from_config
|
|
189
214
|
# ---------------------------------------------------------------------------
|
|
@@ -1263,6 +1263,52 @@ async def test_rename_file_copies_then_deletes_and_moves_inode(
|
|
|
1263
1263
|
metadata_manager.invalidate.assert_any_call("/cat/sch/vol/renamed.txt", is_dir=False)
|
|
1264
1264
|
|
|
1265
1265
|
|
|
1266
|
+
@pytest.mark.trio
|
|
1267
|
+
async def test_rename_refreshes_source_mtime_before_copy(
|
|
1268
|
+
fs, inode_manager, uc_client, metadata_manager, data_manager, ctx
|
|
1269
|
+
):
|
|
1270
|
+
"""Regression: a file just written through the mount carries a locally
|
|
1271
|
+
stamped st_mtime (create()-time wall clock) that is earlier than the
|
|
1272
|
+
server's Last-Modified. rename() must refresh the source from the server
|
|
1273
|
+
before copying, so the If-Unmodified-Since the copy sends matches the
|
|
1274
|
+
server and does not 412. The copy read must use the refreshed mtime/size,
|
|
1275
|
+
not the stale inode values."""
|
|
1276
|
+
vol, f = _make_vol_file(inode_manager, size=512)
|
|
1277
|
+
# Stale, locally-stamped attributes left on the inode after create()/write.
|
|
1278
|
+
f.attr.st_mtime = 9999.0
|
|
1279
|
+
f.attr.st_size = 512
|
|
1280
|
+
# The server's authoritative view, returned by the refresh.
|
|
1281
|
+
server_attr = _make_attr(False, size=5) # _make_attr -> st_mtime=2000.0
|
|
1282
|
+
metadata_manager.get_attributes = AsyncMock(return_value=server_attr)
|
|
1283
|
+
metadata_manager.lookup_child = AsyncMock(return_value=None) # dest is free
|
|
1284
|
+
metadata_manager.invalidate = MagicMock()
|
|
1285
|
+
|
|
1286
|
+
await fs.rename(vol.inode, b"data.txt", vol.inode, b"renamed.txt", 0, ctx)
|
|
1287
|
+
|
|
1288
|
+
# Source was refreshed from the server before the copy.
|
|
1289
|
+
metadata_manager.get_attributes.assert_awaited_once()
|
|
1290
|
+
# The copy read used the server mtime (2000.0) and size (5), not the stale
|
|
1291
|
+
# local values (9999.0 / 512).
|
|
1292
|
+
read_args = data_manager.read.call_args.args
|
|
1293
|
+
assert read_args[3] == 2000.0 # mtime -> If-Unmodified-Since
|
|
1294
|
+
assert read_args[4] == 5 # file_size
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
@pytest.mark.trio
|
|
1298
|
+
async def test_rename_source_deleted_during_refresh_raises_enoent(
|
|
1299
|
+
fs, inode_manager, metadata_manager, ctx
|
|
1300
|
+
):
|
|
1301
|
+
"""If the source vanished by the time we refresh its metadata, rename
|
|
1302
|
+
surfaces ENOENT rather than copying with stale attributes."""
|
|
1303
|
+
vol, f = _make_vol_file(inode_manager, size=5)
|
|
1304
|
+
metadata_manager.get_attributes = AsyncMock(return_value=None) # gone
|
|
1305
|
+
metadata_manager.lookup_child = AsyncMock(return_value=None)
|
|
1306
|
+
|
|
1307
|
+
with pytest.raises(pyfuse3.FUSEError) as ei:
|
|
1308
|
+
await fs.rename(vol.inode, b"data.txt", vol.inode, b"renamed.txt", 0, ctx)
|
|
1309
|
+
assert ei.value.errno == errno.ENOENT
|
|
1310
|
+
|
|
1311
|
+
|
|
1266
1312
|
@pytest.mark.trio
|
|
1267
1313
|
async def test_rename_into_other_directory_reparents_inode(
|
|
1268
1314
|
fs, inode_manager, uc_client, metadata_manager, ctx
|
|
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
|
|
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
|