monkeyfs 0.2.2__tar.gz → 0.2.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.
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/PKG-INFO +2 -2
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/README.md +1 -1
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/virtual.py +30 -5
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs.egg-info/PKG-INFO +2 -2
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/pyproject.toml +1 -1
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_metadata.py +67 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/LICENSE +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/__init__.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/base.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/conformance.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/context.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/isolated.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/mount.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/patching/__init__.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/patching/core.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/patching/fdtable.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/patching/install.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/patching/patches.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/py.typed +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/readonly.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/virtualfile.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs.egg-info/SOURCES.txt +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs.egg-info/dependency_links.txt +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs.egg-info/requires.txt +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs.egg-info/top_level.txt +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/setup.cfg +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_bulk_operations.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_conformance.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_directory_ops.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_expanduser.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_fd_emulation.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_finalization_and_invalidate.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_isolated.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_lazy_binary_read.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_mount.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_open_optional.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_patching.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_patching_pathlib.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_patching_safe_paths.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_protocol.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_ranged_read.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_readonly.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_symlink_entries.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_vfs_optional.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_vfs_size_limit.py +0 -0
- {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_virtual.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: monkeyfs
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Transparent filesystem interception via monkey-patching.
|
|
5
5
|
Author: ashenfad
|
|
6
6
|
License-Expression: MIT
|
|
@@ -142,7 +142,7 @@ It raises `AssertionError` naming the method and what was expected of it -- incl
|
|
|
142
142
|
|
|
143
143
|
## Part of the agex stack
|
|
144
144
|
|
|
145
|
-
monkeyfs provides filesystem interception for [sandtrap](https://github.com/ashenfad/sandtrap) and [agex](https://github.com/ashenfad/agex), giving sandboxed agent code an isolated virtual filesystem. `VirtualFS` accepts any dict-like backing store -- including [kvgit](https://github.com/ashenfad/kvgit)
|
|
145
|
+
monkeyfs provides filesystem interception for [sandtrap](https://github.com/ashenfad/sandtrap) and [agex](https://github.com/ashenfad/agex), giving sandboxed agent code an isolated virtual filesystem. `VirtualFS` accepts any dict-like backing store -- including a [kvgit](https://github.com/ashenfad/kvgit) worktree for a versioned filesystem with commit/rollback. A store that offers `get_many(*keys)`, as kvgit worktrees do, has its metadata read in one batched call rather than one per file.
|
|
146
146
|
|
|
147
147
|
## Documentation
|
|
148
148
|
|
|
@@ -108,7 +108,7 @@ It raises `AssertionError` naming the method and what was expected of it -- incl
|
|
|
108
108
|
|
|
109
109
|
## Part of the agex stack
|
|
110
110
|
|
|
111
|
-
monkeyfs provides filesystem interception for [sandtrap](https://github.com/ashenfad/sandtrap) and [agex](https://github.com/ashenfad/agex), giving sandboxed agent code an isolated virtual filesystem. `VirtualFS` accepts any dict-like backing store -- including [kvgit](https://github.com/ashenfad/kvgit)
|
|
111
|
+
monkeyfs provides filesystem interception for [sandtrap](https://github.com/ashenfad/sandtrap) and [agex](https://github.com/ashenfad/agex), giving sandboxed agent code an isolated virtual filesystem. `VirtualFS` accepts any dict-like backing store -- including a [kvgit](https://github.com/ashenfad/kvgit) worktree for a versioned filesystem with commit/rollback. A store that offers `get_many(*keys)`, as kvgit worktrees do, has its metadata read in one batched call rather than one per file.
|
|
112
112
|
|
|
113
113
|
## Documentation
|
|
114
114
|
|
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import os
|
|
11
11
|
from collections.abc import MutableMapping
|
|
12
12
|
from datetime import datetime, timezone
|
|
13
|
+
from typing import Any
|
|
13
14
|
|
|
14
15
|
from .base import FileInfo, FileMetadata
|
|
15
16
|
from .virtualfile import LazyBinaryFile, VirtualFile, open_file
|
|
@@ -19,7 +20,8 @@ class VirtualFS:
|
|
|
19
20
|
"""State-backed virtual filesystem with metadata tracking.
|
|
20
21
|
|
|
21
22
|
Provides file operations backed by agent state. Each file is stored
|
|
22
|
-
as a separate state key, enabling granular versioning with
|
|
23
|
+
as a separate state key, enabling granular versioning with a versioned
|
|
24
|
+
state such as a kvgit worktree.
|
|
23
25
|
|
|
24
26
|
File metadata (size, creation time, modification time) is automatically
|
|
25
27
|
tracked for all files and can be accessed via stat() or list_detailed().
|
|
@@ -77,6 +79,11 @@ class VirtualFS:
|
|
|
77
79
|
|
|
78
80
|
Args:
|
|
79
81
|
state: State backend for file storage. Defaults to an empty dict.
|
|
82
|
+
A state may also offer ``get_many(*keys)``, returning a
|
|
83
|
+
mapping of the keys that exist to their values (as kvgit
|
|
84
|
+
worktrees do). Scans that read many keys then ask for them
|
|
85
|
+
in one call, which over a networked store is one round
|
|
86
|
+
trip instead of one per key.
|
|
80
87
|
max_size_mb: Maximum total size of all files in megabytes.
|
|
81
88
|
None means unlimited.
|
|
82
89
|
"""
|
|
@@ -417,10 +424,10 @@ class VirtualFS:
|
|
|
417
424
|
return self._all_meta
|
|
418
425
|
|
|
419
426
|
rows: dict[str, FileMetadata] = {}
|
|
420
|
-
for key in list(self._state.keys())
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
raw =
|
|
427
|
+
keys = [key for key in list(self._state.keys()) if self.is_metadata_key(key)]
|
|
428
|
+
found = self._read_many(keys)
|
|
429
|
+
for key in keys:
|
|
430
|
+
raw = found.get(key)
|
|
424
431
|
if raw is None:
|
|
425
432
|
continue
|
|
426
433
|
try:
|
|
@@ -436,6 +443,24 @@ class VirtualFS:
|
|
|
436
443
|
self._all_meta = rows
|
|
437
444
|
return rows
|
|
438
445
|
|
|
446
|
+
def _read_many(self, keys: list[str]) -> dict[str, Any]:
|
|
447
|
+
"""The values of ``keys`` that exist in the state.
|
|
448
|
+
|
|
449
|
+
One ``get_many`` call when the state offers it, else one ``get``
|
|
450
|
+
per key.
|
|
451
|
+
"""
|
|
452
|
+
if not keys:
|
|
453
|
+
return {}
|
|
454
|
+
get_many = getattr(self._state, "get_many", None)
|
|
455
|
+
if callable(get_many):
|
|
456
|
+
return dict(get_many(*keys))
|
|
457
|
+
found: dict[str, Any] = {}
|
|
458
|
+
for key in keys:
|
|
459
|
+
value = self._state.get(key)
|
|
460
|
+
if value is not None:
|
|
461
|
+
found[key] = value
|
|
462
|
+
return found
|
|
463
|
+
|
|
439
464
|
def _get_current_size(self) -> int:
|
|
440
465
|
"""Get total size of all files in the VFS.
|
|
441
466
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: monkeyfs
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Transparent filesystem interception via monkey-patching.
|
|
5
5
|
Author: ashenfad
|
|
6
6
|
License-Expression: MIT
|
|
@@ -142,7 +142,7 @@ It raises `AssertionError` naming the method and what was expected of it -- incl
|
|
|
142
142
|
|
|
143
143
|
## Part of the agex stack
|
|
144
144
|
|
|
145
|
-
monkeyfs provides filesystem interception for [sandtrap](https://github.com/ashenfad/sandtrap) and [agex](https://github.com/ashenfad/agex), giving sandboxed agent code an isolated virtual filesystem. `VirtualFS` accepts any dict-like backing store -- including [kvgit](https://github.com/ashenfad/kvgit)
|
|
145
|
+
monkeyfs provides filesystem interception for [sandtrap](https://github.com/ashenfad/sandtrap) and [agex](https://github.com/ashenfad/agex), giving sandboxed agent code an isolated virtual filesystem. `VirtualFS` accepts any dict-like backing store -- including a [kvgit](https://github.com/ashenfad/kvgit) worktree for a versioned filesystem with commit/rollback. A store that offers `get_many(*keys)`, as kvgit worktrees do, has its metadata read in one batched call rather than one per file.
|
|
146
146
|
|
|
147
147
|
## Documentation
|
|
148
148
|
|
|
@@ -707,3 +707,70 @@ class TestLegacyTableMigration:
|
|
|
707
707
|
assert vfs.isdir("d")
|
|
708
708
|
assert set(rows(state)) == {"d/a.txt"}
|
|
709
709
|
assert set(legacy_table(state)) == {"d"}
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
class BatchingState(dict):
|
|
713
|
+
"""A dict that also answers ``get_many``, counting how it is read."""
|
|
714
|
+
|
|
715
|
+
def __init__(self, *args, **kwargs):
|
|
716
|
+
super().__init__(*args, **kwargs)
|
|
717
|
+
self.gets: list[str] = []
|
|
718
|
+
self.batches: list[tuple[str, ...]] = []
|
|
719
|
+
|
|
720
|
+
def get(self, key, default=None):
|
|
721
|
+
self.gets.append(key)
|
|
722
|
+
return super().get(key, default)
|
|
723
|
+
|
|
724
|
+
def get_many(self, *keys):
|
|
725
|
+
self.batches.append(keys)
|
|
726
|
+
return {k: dict.__getitem__(self, k) for k in keys if k in self}
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _tree(vfs):
|
|
730
|
+
vfs.mkdir("/empty")
|
|
731
|
+
for i in range(30):
|
|
732
|
+
vfs.write(f"/d{i % 3}/f{i}.txt", b"x" * i)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
class TestBatchedMetadataScan:
|
|
736
|
+
def test_a_scan_reads_every_row_in_one_call(self):
|
|
737
|
+
state = BatchingState()
|
|
738
|
+
_tree(VirtualFS(state))
|
|
739
|
+
state.gets.clear()
|
|
740
|
+
|
|
741
|
+
fresh = VirtualFS(state) # nothing cached
|
|
742
|
+
fresh.list("/", recursive=True)
|
|
743
|
+
|
|
744
|
+
assert len(state.batches) == 1
|
|
745
|
+
assert set(state.batches[0]) == {
|
|
746
|
+
k for k in state if VirtualFS.is_metadata_key(k)
|
|
747
|
+
}
|
|
748
|
+
assert not [k for k in state.gets if VirtualFS.is_metadata_key(k)]
|
|
749
|
+
|
|
750
|
+
def test_a_batching_state_answers_as_a_plain_one_does(self):
|
|
751
|
+
plain: dict = {}
|
|
752
|
+
_tree(VirtualFS(plain))
|
|
753
|
+
batching = BatchingState(plain)
|
|
754
|
+
|
|
755
|
+
def listing(state):
|
|
756
|
+
vfs = VirtualFS(state)
|
|
757
|
+
return [
|
|
758
|
+
(info.path, info.size, info.is_dir)
|
|
759
|
+
for info in vfs.list_detailed("/", recursive=True)
|
|
760
|
+
]
|
|
761
|
+
|
|
762
|
+
assert listing(batching) == listing(plain)
|
|
763
|
+
assert (
|
|
764
|
+
VirtualFS(batching)._get_current_size()
|
|
765
|
+
== VirtualFS(plain)._get_current_size()
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
def test_the_legacy_table_still_fills_paths_without_rows(self):
|
|
769
|
+
state = BatchingState(
|
|
770
|
+
_legacy_state({"/old.txt": _entry(3)}, blobs={"/old.txt": b"abc"})
|
|
771
|
+
)
|
|
772
|
+
vfs = VirtualFS(state)
|
|
773
|
+
vfs.write("/new.txt", b"hello")
|
|
774
|
+
fresh = VirtualFS(state)
|
|
775
|
+
assert fresh.stat("/old.txt").size == 3
|
|
776
|
+
assert {i.path for i in fresh.list_detailed("/")} == {"/old.txt", "/new.txt"}
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|