Skip to content

Add rspace_client.fs: the Gallery, Inventory and ELN Workspace as one fsspec filesystem - #68

Open
tilorspace wants to merge 29 commits into
mainfrom
feature/fsspec-filesystem
Open

tilorspace wants to merge 29 commits into
mainfrom
feature/fsspec-filesystem

Conversation

@tilorspace

@tilorspace tilorspace commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Stacked PR. Merge after #60 (client layer) and #61 (offline mock server); this one targets fs-stack/2-mock-server.

Replaces the PyFilesystem2 version of the unified filesystem (#62 to #64, closed). PyFilesystem2 has had no release since 2022, and Galaxy, the main consumer, is moving every file source to fsspec (galaxyproject/galaxy#21832, where rspace.py is on the list). Tracked in RSDEV-1519.

What it adds

  • rspace_client.fs, an fsspec filesystem. RSpaceFilesystem mounts gallery/, inventory/ and workspace/, sharing one ELN and one Inventory client. Each branch is also usable on its own (GalleryFilesystem, InventoryFilesystem, WorkspaceFilesystem).
    • Paths are record names (gallery/Images/microscope.png). Duplicates, and names that read as addresses, carry their global ID (data [GL112].csv). path_style="labelled" or "id" give paths that survive a rename.
    • Read-only by default (writable=True, allow_delete=True). Uploads into Inventory records and ELN fields go through the Gallery and are linked. cp_file from a Gallery-backed file links instead of copying bytes. Transactions defer uploads.
    • rspace://host/... URLs via fsspec.open, fsspec.filesystem("rspace") and UPath, registered as an fsspec.specs entry point. The key comes from RSPACE_API_KEY only, never from the URL.
    • RSpace errors surface as builtin OSError subclasses, so exists, walk, copy and Galaxy behave.
  • Deprecation of rspace_client.eln.fs.GalleryFilesystem and rspace_client.inv.attachment_fs.InventoryAttachmentFilesystem. Their behaviour is unchanged apart from the two Galaxy fixes below, they warn on construction, and they're removed in 3.0 together with fs, after Galaxy's shipped plugin has moved.
  • Fix for Galaxy 26.1 exports to RSpace. requests 2.34 (pinned by Galaxy 26.1) refuses a file part that forwards read through __getattr__. That's how Galaxy's FakedNameIO wrapper works, so exports through Galaxy's shipped rspace source fail on every released rspace-client. _post_multipart now wraps such objects. Verified inside a Galaxy 26.1.2 container: 2.7.4 raises the TypeError, this branch uploads.
  • Fix for Galaxy on Python 3.10. Galaxy's shipped rspace source parses a file's created date with datetime.fromisoformat, which rejects RSpace's trailing Z before Python 3.11, so listing any Gallery folder with files failed on Python 3.10 with every released rspace-client. GalleryFilesystem now gives the same UTC instant as +00:00. Verified with Galaxy's real plugin (galaxy-files 26.1.1) on Python 3.10.
  • galaxy_plugin_contract_test.py, which pins what Galaxy's shipped plugin relies on. A failure there is a release blocker.
  • tools/galaxy/rspace_fsspec_source.py, the reference Galaxy file source (type rspace_unified) to propose upstream once this is released.
  • Docs: docs/usage-guide.md and the design record docs/fsspec-filesystem-design.md, which explains why the package is shaped this way and what must not move.

Review order

  1. docs/fsspec-filesystem-design.md (the decisions)
  2. rspace_client/fs/paths.py, rspace_client/fs/base.py (shared behaviour)
  3. gallery.py, inventory.py, workspace.py, then rspace.py (the mount)
  4. Shims: rspace_client/eln/fs.py, rspace_client/inv/attachment_fs.py, and the contract test
  5. Tests (fs_*_test.py), tools/galaxy/

Testing

  • Offline suite: 305 passed (poetry run pytest -m "not integration"), using the mock server from Add an offline mock of the RSpace ELN and Inventory APIs for the test suite #61.
  • fs_integration_test.py against a docker RSpace 2.27, plus a record-type matrix covering Gallery, documents, samples, subsamples, containers, instruments, and the bench and template refusals.
  • Galaxy 25.1 and 26.1 containers with the reference plugin: listing, search (including bracketed names, CJK and %), pagination, import from Gallery, Inventory and Workspace, and export into a sample, a field and the Gallery.

New dependency: fsspec>=2024.3.0. fs and the setuptools<82 pin stay until 3.0.

🤖 Generated with Claude Code

tilorspace and others added 19 commits September 21, 2026 14:25
…s the filesystems need

URLs the server advertises in `_links` are rebased onto the configured host so paging and
download links work behind a proxy or port-forward. The session never follows a redirect,
because `requests` would carry the `apiKey` header to the new host. `doDelete` tolerates a
leading slash. `upload_file` and `upload_attachment_by_global_id` take `filename=`,
`list_folder_tree` takes `page_size=`, and the Inventory client gains `get_workbench_by_id`
and `attach_gallery_file_by_global_id`. Inventory CSV import goes through the shared session.
The existing filesystem tests now expect the redirect flag on every request.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… suite

A standard-library HTTP server covering the endpoints the PyFilesystem classes use, backed
by an in-memory dataset with the awkward cases a filesystem has to handle (duplicate names,
system folders, paginated sections, documents with mixed field types). It counts requests so
tests can assert what an operation costs. One server runs for the whole pytest session and
`MockServerTestCase` resets its fixtures before each test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s of the PyFilesystem work

RSDEV-1519: the unified filesystem is being built on fsspec rather than PyFilesystem2.
This branch starts from the mock-server stage (fs-stack/2) and takes from
review/unified-pyfilesystem only what does not depend on PyFilesystem:

- docs/unified-pyfilesystem-design.md, the browsing-model record, with a status note
  saying which sections describe what to build and which glue is replaced
- rspace_client/fs/paths.py, the segment and path-style helpers, now importing from
  posixpath instead of fs.path
- tools/galaxy/check_plugin.py and its README, to be re-pointed at the fsspec plugin
- tools/fsspec_spike/, the 2026-09-21 prototype that proved the Gallery branch, the
  mount dispatcher, transactions and Galaxy's FsspecFilesSource end to end; it documents
  the fsspec traps found and is to be deleted once the real package exists

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e plugin

Galaxy 25.1 already ships _fsspec.py (fsspec 2025.9.0) but its path hooks take no
config argument and _open_fs no cache_options, so the prototype plugin accepts both
signatures. rspace-client 2.7.4 in that venv has no page_size on list_folder_tree, so
the prototype falls back. check_in_galaxy_container.py drives the shipped PyFilesystem
plugin and the fsspec one through ConfiguredFileSources against the mock server: same
listings, fsspec adds name-based URIs, glob search and disambiguated duplicates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ce files

Live RSpace 2.26 file records carry created but no lastModified, and Galaxy's fsspec base
reads only mtime, modified or LastModified for the time column.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…override for bracketed names

Exploratory testing in Galaxy 25.1 on 2026-10-06 showed that a clicked row is re-resolved
from its stored URI string in a later request, so the appended-ID rule is load-bearing in a
pure UI consumer too, and that Galaxy's bracket escaping defeats fsspec's glob, which an
override of glob in our class fixes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rspace_client.fs presents RSpace as an fsspec filesystem (RSDEV-1519). RSpaceFilesystem
mounts gallery/, inventory/ and workspace/; the three branches keep the browsing model of
the PyFilesystem design record unchanged (directory = contains files, documents as folders
of text fields, Inventory records as folders of records and attachments, attachment fields
as one-file folders, link instead of copy, read-only by default, signed-document lock).

Framework glue, new for fsspec:
- base.py: entries are dicts (name = full path, type, size, mtime, created, globalId,
  rspace, writable); client exceptions become OSError subclasses at every public method
  (FileNotFoundError, PermissionError, ConnectionError, RemoteApiError; a ReadOnlyError
  for the posture); the listing keeps segments unique and self-addressing (duplicates and
  names that read as addresses get their real ID, as the design requires, because a
  consumer such as Galaxy stores the segment and re-resolves it later); reads buffer the
  file, writes buffer until close or transaction commit; glob answers the
  "<dir>/*<text>*" pattern Galaxy's search sends with the brackets it escapes.
- rspace.py: the mount dispatcher fsspec does not ship, plus rspace:// URL handling
  (host and options from the URL, the key only from RSPACE_API_KEY, http for loopback
  hosts only, credentials in the URL refused). Registered as the "rspace" protocol via
  fsspec.specs; UPath works.
- tree.py: the tree view over entries.

The deprecated PyFilesystem classes stay as they are for Galaxy's shipped plugin and now
warn; they and the fs dependency leave in 3.0. tools/galaxy/rspace_fsspec_source.py is the
reference Galaxy file source for the new filesystem. The prototype under tools/fsspec_spike
is gone. Tests: the PyFilesystem suites ported to the fsspec API, 139 tests over the mock
server, plus the live integration module.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Resolving a bare name listed the parent with the Gallery's per-file size fetches, so
  info() of one file by name cost 6 requests; lookups and emptiness checks now read the
  listing in a names-only mode (4 requests, sizes still fetched for real listings).
- glob("<dir>/**") was answered as a search of <dir> rather than a recursive glob.
- invalidate_cache() left fsspec's listings cache untouched (the base does nothing), so a
  write with use_listings_cache=True left the old listing in place; the cache is dropped.
- mv() out of the Gallery copied the file and only then failed on rm_file, leaving a
  duplicate the API cannot delete; a branch that cannot delete files refuses before copying,
  both directly and through the mount.
- _strip_protocol() rejected the list form fsspec passes to cat()/rm()/expand_path().

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fsspec's put() passes directories through put_file and expects the backend to make them
before their files arrive; returning early left 'Documents/sub/b.txt' with no parent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… a RemoteApiError

fsspec.core.url_to_fs, get_mapper and UPath hand the mount paths such as
'host:port/gallery/Images?scheme=http'; only the fixed mount names may appear at the top
level, so a first segment that looks like a host is dropped with its query string, and
find()/keys now line up with the root path those consumers use. RSpace answers 500 for a
request it understood but could not carry out (an invalid image), with the reason in the
message; only 502/503/504 are mapped to ConnectionError now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reuse, simplification, efficiency and altitude pass over rspace_client.fs:

- the deprecated eln.fs Gallery module imports the section policy, the
  classification table, GallerySectionMismatch, Placement and the
  placement helpers from fs.gallery instead of carrying a copy
- one translate_class helper replaces four hand-written translation loops
- RSpaceFilesystem delegates its simple methods through a factory, strips
  host and query once in _strip_protocol by splitting the URL, and derives
  mount_names from the branch dict
- shared branch code moves into the base: file download, file-bearing
  field helpers, a default link() behind CAN_LINK, _require_movable
- the _names_only instance flag becomes a names_only argument
- a listing records every child in the name cache, so browsing a
  directory and then its children no longer re-lists the parent per child
- Inventory field lookups fetch the record without container contents;
  get_file and rm resolve the path instead of an extra info request
- the Galaxy search glob regex states what it accepts; the mismatch
  message is built in one function; dead helpers and duplicate checks go
- tests share one names()/segments() helper and one lazily started mock
  server; fixture builders call the mutation methods with a chosen id
- universal-pathlib is declared as a dev dependency (the mount tests
  import it)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…in contract

requests 2.34 checks that a multipart file part defines read on its class, which on
Python 3.12+ skips __getattr__. Galaxy's shipped rspace file source passes exactly such a
wrapper (FakedNameIO) to ELNClient.upload_file, so exports from Galaxy 26.1, which pins
requests 2.34.2, fail with a TypeError on any released rspace-client. _post_multipart now
wraps such objects in one that defines read and keeps the name.

Ports galaxy_plugin_contract_test.py from the closed PyFilesystem stack, adapted to the
shim as it is here (the 2.7 class, not a subclass of the new one), and restores
MISCELLANEOUS_SECTION, the one 2.7.4 shim export the move to rspace_client.fs.gallery dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fsspec.filesystem("rspace") and rspace:// URLs now resolve without importing
rspace_client.fs first, as the package docstring already said they did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renamed to docs/fsspec-filesystem-design.md. The sections on mounting, write posture,
listings, errors and the opener described PyFilesystem glue that no longer exists; the
Galaxy section now lists the five things the shipped plugin relies on, the requests 2.34
breakage, and what the upstream file source pull request has to carry. DEVELOPING points
at the contract test, CHANGELOG records the fix and the entry point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
That module never shipped; #57 was a docs error, fixed by pointing the usage guide at
rspace_client.inv.attachment_fs. Say which shim Galaxy imports and why the other stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tilorspace and others added 4 commits October 12, 2026 08:17
_rebase_url swapped scheme and host but kept the server's path, so a client
configured with a path prefix (https://proxy/rspace) followed next-page and
download links to https://proxy/api/v1/... and lost /rspace. Re-anchor the
part from /api/ on under our own base path, the same base relative requests
use. Links without /api/ keep their path. Raised in review of PR #60.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers the review's test gaps: doDelete single-slash URLs (regression for
delete_document), upload_file and upload_attachment_by_global_id filename=,
list_folder_tree page_size=, get_workbench_by_id id forms and includeContent,
attach_gallery_file_by_global_id body, and CSV parse through the shared
session with redirects off and the client timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tilorspace added a commit that referenced this pull request Oct 12, 2026
…e filesystems need (#60)

Client layer: rebase advertised links, refuse redirects, add calls the filesystems need (#60)

Changes to the ELN and Inventory REST clients that the filesystem work
needs, plus fixes that stand on their own:

- Links the server advertises in `_links` are rebased onto the configured
  base URL, path prefix included, so next-page and download links work
  behind a port-forward, in a container, or behind a proxy that serves
  RSpace under a sub-path such as /rspace.
- The shared session never follows an HTTP redirect, because requests
  would carry the apiKey header to the new host.
- doDelete tolerates a leading slash, which fixes delete_document.
- ELNClient.upload_file and InventoryClient.upload_attachment_by_global_id
  take an optional filename=; list_folder_tree takes page_size=.
- New InventoryClient.get_workbench_by_id (/containers answers 422 for a
  bench) and attach_gallery_file_by_global_id (link an existing Gallery
  file to an Inventory record).
- Inventory CSV import goes through the shared session, so it gets the
  same timeout, retries and error handling as every other request.
- A stray debug print is removed from upload_attachment_by_global_id.

Review follow-up: the path-prefix handling in link rebasing, and unit
tests for each changed or new client method (client_methods_test.py).

Stack: this was part 1 of five PRs towards a unified PyFilesystem (#58).
On 2026-10-06 the filesystem was moved to fsspec instead, so parts 3 to 5
(#62, #63, #64) were closed without merging. This client layer and the
offline mock server (#61) are independent of the filesystem framework and
are kept. The fsspec filesystem itself is #68, which builds on #61. Merge
#61 next, then retarget #68 to main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… suite

A standard-library HTTP server covering the endpoints the PyFilesystem classes use, backed
by an in-memory dataset with the awkward cases a filesystem has to handle (duplicate names,
system folders, paginated sections, documents with mixed field types). It counts requests so
tests can assert what an operation costs. One server runs for the whole pytest session and
`MockServerTestCase` resets its fixtures before each test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fs-stack/2 was rebased onto main after #60 was squash-merged; its content is unchanged, so the feature branch keeps its own tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tilorspace
tilorspace changed the base branch from fs-stack/2-mock-server to main October 12, 2026 08:21
tilorspace added a commit that referenced this pull request Oct 12, 2026
… suite (#61)

A standard-library mock of the ELN (/api/v1) and Inventory
(/api/inventory/v1) endpoints the filesystem classes use, under
rspace_client/tests/mock_rspace. Nothing here is installed with the
package.

- server.py serves an in-memory dataset from fixtures.py and counts
  requests, so a test can assert what a listing costs.
- The dataset carries the cases a filesystem has to get right: duplicate
  names, names that look like global IDs, system folders in Home,
  sections large enough to paginate, a document with text and non-text
  fields, a signed document, a bench with attachments.
- conftest.py starts one server for the whole pytest session;
  MockServerTestCase resets its fixtures before every test.

No test uses it yet; #68 adds the fsspec filesystem tests, the Galaxy
contract test and the plugin check tool that run against it.

Stack: part 2 of the original PyFilesystem stack (#58). Parts 3 to 5
(#62, #63, #64) were closed when the filesystem moved to fsspec; the
client layer (#60) and this mock are framework-neutral and were kept.
#68, the fsspec filesystem, is now based on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tilorspace and others added 4 commits October 12, 2026 10:22
#61 was squash-merged into main; main now holds the same content this branch already had, so the branch keeps its own tree (conftest.py stays deleted, the feature's mock fixtures stay).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests were gated on RSPACE_SUPPORTS_LINK_FIELDS while server PR
rspace-os/rspace-web#803 was unreleased. It has since shipped, and CI runs
against the latest rspace-web release (2.27.0), so the gate only made CI
skip them. All five pass against RSpace 2.27.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RSpace dates end in Z for UTC. Galaxy's shipped rspace file source parses
the shim's rspace.created with datetime.fromisoformat, which rejects Z
before Python 3.11, so on Python 3.10 (still supported by Galaxy) listing
any Gallery folder that holds files failed with ValueError. That is true of
every released rspace-client; the contract test surfaced it when CI ran it
on 3.9 and 3.10.

GalleryFilesystem now spells the same instant as +00:00 in the created
field and passes the rest of the record through. A new contract test pins
the form on every Python version. Checked with Galaxy's real plugin
(galaxy-files 26.1.1) on Python 3.10 against the mock: the listing fails
before and succeeds after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Inventory attachment tests borrowed the first listed sample with an
attachment field. CI runs as a sysadmin, who sees samples it may not edit,
so the first match (SA4) answered 422 "cannot be edited by current User".
Each test now creates a sample from a template with one attachment field
and one subsample, and deletes both afterwards. The tests no longer skip on
an account without such a sample either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant