Skip to content

Commit d682ce4

Browse files
committed
migrate irods pyfilesystem2 filesource to fsspec backed filesource
1 parent bbac30d commit d682ce4

5 files changed

Lines changed: 140 additions & 104 deletions

File tree

lib/galaxy/dependencies/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def check_fs_basespace(self):
291291
def check_rspace_client(self):
292292
return "rspace" in self.file_sources
293293

294-
def check_fs_irods(self):
294+
def check_mangofs(self):
295295
return "irods" in self.file_sources
296296

297297
def check_watchdog(self):

lib/galaxy/dependencies/conditional-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ adlfs
3535
huggingface_hub
3636
omero-py #type: omero
3737
iiif-fsspec # type: iiif
38-
fs-irods # type: irods, depends on python-irodsclient
38+
mangofs @ git+https://gitlab.kuleuven.be/setit/rdm/mango-fsspec.git@1.1.0 # type: irods, depends on python-irodsclient
3939
mavedb-fsspec>=0.1.0 # type: mavedb
4040

4141
# Vault backend

lib/galaxy/files/sources/irods.py

Lines changed: 84 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,50 @@
11
import os
2-
from fnmatch import fnmatch
3-
4-
import fs
5-
import fs.errors
6-
7-
from galaxy.exceptions import (
8-
AuthenticationRequired,
9-
MessageException,
2+
import posixpath
3+
from typing import (
4+
Optional,
5+
Union,
106
)
7+
118
from galaxy.files.models import (
12-
AnyRemoteEntry,
13-
BaseFileSourceConfiguration,
14-
BaseFileSourceTemplateConfiguration,
159
FilesSourceRuntimeContext,
1610
)
11+
from galaxy.files.sources._fsspec import (
12+
CacheOptionsDictType,
13+
FsspecBaseFileSourceConfiguration,
14+
FsspecBaseFileSourceTemplateConfiguration,
15+
FsspecFilesSource,
16+
)
1717
from galaxy.util.config_templates import TemplateExpansion
18-
from ._pyfilesystem2 import PyFilesystem2FilesSource
1918

2019
try:
21-
from fs_irods import iRODSFS
2220
from irods.session import iRODSSession
21+
from mangofs import IRODSFileSystem
2322
except ImportError:
24-
iRODSFS = None
2523
iRODSSession = None
26-
27-
28-
class IrodsFileSourceTemplateConfiguration(BaseFileSourceTemplateConfiguration):
29-
host: str | TemplateExpansion
30-
port: int | TemplateExpansion = 1247
31-
username: str | TemplateExpansion
32-
password: str | TemplateExpansion
33-
zone: str | TemplateExpansion
34-
root: str | TemplateExpansion | None = None
35-
timeout: int | TemplateExpansion = 30
36-
refresh_time: int | TemplateExpansion = 300
37-
client_server_negotiation: str | TemplateExpansion | None = None
38-
client_server_policy: str | TemplateExpansion | None = None
39-
encryption_algorithm: str | TemplateExpansion | None = None
40-
encryption_key_size: int | TemplateExpansion | None = None
41-
encryption_num_hash_rounds: int | TemplateExpansion | None = None
42-
encryption_salt_size: int | TemplateExpansion | None = None
43-
ssl_verify_server: str | TemplateExpansion | None = None
44-
ssl_ca_certificate_file: str | TemplateExpansion | None = None
45-
resource: str | TemplateExpansion | None = None
46-
47-
48-
class IrodsFileSourceConfiguration(BaseFileSourceConfiguration):
24+
IRODSFileSystem = None
25+
26+
27+
class IrodsFsspecFileSourceTemplateConfiguration(FsspecBaseFileSourceTemplateConfiguration):
28+
host: Union[str, TemplateExpansion]
29+
port: Union[int, TemplateExpansion] = 1247
30+
username: Union[str, TemplateExpansion]
31+
password: Union[str, TemplateExpansion]
32+
zone: Union[str, TemplateExpansion]
33+
root: Optional[Union[str, TemplateExpansion]] = None
34+
timeout: Union[int, TemplateExpansion] = 30
35+
refresh_time: Union[int, TemplateExpansion] = 300
36+
client_server_negotiation: Optional[Union[str, TemplateExpansion]] = None
37+
client_server_policy: Optional[Union[str, TemplateExpansion]] = None
38+
encryption_algorithm: Optional[Union[str, TemplateExpansion]] = None
39+
encryption_key_size: Optional[Union[int, TemplateExpansion]] = None
40+
encryption_num_hash_rounds: Optional[Union[int, TemplateExpansion]] = None
41+
encryption_salt_size: Optional[Union[int, TemplateExpansion]] = None
42+
ssl_verify_server: Optional[Union[str, TemplateExpansion]] = None
43+
ssl_ca_certificate_file: Optional[Union[str, TemplateExpansion]] = None
44+
resource: Optional[Union[str, TemplateExpansion]] = None
45+
46+
47+
class IrodsFsspecFileSourceConfiguration(FsspecBaseFileSourceConfiguration):
4948
host: str
5049
port: int = 1247
5150
username: str
@@ -65,70 +64,28 @@ class IrodsFileSourceConfiguration(BaseFileSourceConfiguration):
6564
resource: str | None = None
6665

6766

68-
class IrodsFilesSource(PyFilesystem2FilesSource[IrodsFileSourceTemplateConfiguration, IrodsFileSourceConfiguration]):
67+
class IrodsFsspecFilesSource(
68+
FsspecFilesSource[IrodsFsspecFileSourceTemplateConfiguration, IrodsFsspecFileSourceConfiguration]
69+
):
6970
plugin_type = "irods"
70-
required_module = iRODSFS
71-
required_package = "fs-irods"
72-
73-
template_config_class = IrodsFileSourceTemplateConfiguration
74-
resolved_config_class = IrodsFileSourceConfiguration
75-
76-
def _iter_directory_entries(self, fs_handle, parent_path: str, normalized_query: str | None = None):
77-
for raw_name in fs_handle.listdir(parent_path):
78-
name = os.path.basename(str(raw_name).rstrip("/"))
79-
if not name:
80-
continue
81-
if normalized_query and not fnmatch(name.lower(), f"*{normalized_query}*"):
82-
continue
83-
entry_path = fs.path.join(parent_path, name)
84-
info = fs_handle.getinfo(entry_path, namespaces=["details"])
85-
yield entry_path, info
86-
87-
def _list_non_recursive(
88-
self,
89-
fs_handle,
90-
path: str,
91-
limit: int | None = None,
92-
offset: int | None = None,
93-
query: str | None = None,
94-
) -> tuple[list[AnyRemoteEntry], int]:
95-
normalized_query = query.lower() if query else None
96-
entries = []
97-
for _, info in self._iter_directory_entries(fs_handle, path, normalized_query):
98-
entries.append(self._resource_info_to_dict(path, info))
99-
count = len(entries)
100-
if (page := self._to_page(limit, offset)) is not None:
101-
entries = entries[page[0] : page[1]]
102-
return entries, count
103-
104-
def _list(
71+
required_module = IRODSFileSystem
72+
required_package = "mangofs"
73+
74+
template_config_class = IrodsFsspecFileSourceTemplateConfiguration
75+
resolved_config_class = IrodsFsspecFileSourceConfiguration
76+
77+
def _open_fs(
10578
self,
106-
context: FilesSourceRuntimeContext[IrodsFileSourceConfiguration],
107-
path="/",
108-
recursive=False,
109-
write_intent: bool = False,
110-
limit: int | None = None,
111-
offset: int | None = None,
112-
query: str | None = None,
113-
sort_by: str | None = None,
114-
) -> tuple[list[AnyRemoteEntry], int]:
115-
try:
116-
with self._open_fs(context) as fs_handle:
117-
if recursive:
118-
raise MessageException("Recursive listing is not supported for iRODS file sources.")
119-
return self._list_non_recursive(fs_handle, path, limit, offset, query)
120-
except fs.errors.PermissionDenied as e:
121-
raise AuthenticationRequired(
122-
f"Permission Denied. Reason: {e}. Please check your credentials in your preferences for {self.label}."
123-
) from e
124-
except fs.errors.FSError as e:
125-
raise MessageException(f"Problem listing file source path {path}. Reason: {e}") from e
126-
127-
def _open_fs(self, context: FilesSourceRuntimeContext[IrodsFileSourceConfiguration]):
128-
if iRODSFS is None or iRODSSession is None:
79+
context: FilesSourceRuntimeContext[IrodsFsspecFileSourceConfiguration],
80+
cache_options: CacheOptionsDictType,
81+
):
82+
if IRODSFileSystem is None or iRODSSession is None:
12983
raise self.required_package_exception
13084

131-
config = context.config
85+
session = self._open_session(context.config)
86+
return IRODSFileSystem(session=session, **cache_options)
87+
88+
def _open_session(self, config: IrodsFsspecFileSourceConfiguration):
13289
session_kwargs = {
13390
"host": config.host,
13491
"port": config.port,
@@ -152,8 +109,33 @@ def _open_fs(self, context: FilesSourceRuntimeContext[IrodsFileSourceConfigurati
152109
session.connection_timeout = config.timeout
153110
if config.resource:
154111
session.default_resource = config.resource
155-
156-
return iRODSFS(session=session, root=config.root)
157-
158-
159-
__all__ = ("IrodsFilesSource",)
112+
return session
113+
114+
def _to_filesystem_path(self, path: str, config: IrodsFsspecFileSourceConfiguration) -> str:
115+
root = self._normalized_root(config)
116+
path = path or "/"
117+
if path.startswith("irods://"):
118+
return path
119+
if path == "/":
120+
return root
121+
return posixpath.join(root, path.lstrip("/"))
122+
123+
def _adapt_entry_path(self, filesystem_path: str, config: IrodsFsspecFileSourceConfiguration) -> str:
124+
root = self._normalized_root(config)
125+
filesystem_path = filesystem_path or "/"
126+
if filesystem_path == root:
127+
return "/"
128+
root_prefix = root.rstrip("/") + "/"
129+
if filesystem_path.startswith(root_prefix):
130+
return "/" + filesystem_path[len(root_prefix) :]
131+
return filesystem_path if filesystem_path.startswith("/") else f"/{filesystem_path}"
132+
133+
@staticmethod
134+
def _normalized_root(config: IrodsFsspecFileSourceConfiguration) -> str:
135+
if not config.root:
136+
return "/"
137+
root = os.path.normpath(config.root)
138+
return root if root.startswith("/") else f"/{root}"
139+
140+
141+
__all__ = ("IrodsFsspecFilesSource",)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
- id: irods
2+
version: 0
3+
name: iRODS
4+
description: |
5+
Use this template to connect an iRODS collection as a Galaxy file source for
6+
importing and exporting datasets.
7+
8+
You need valid iRODS credentials and the target root collection path.
9+
variables:
10+
host:
11+
label: iRODS host
12+
type: string
13+
help: Hostname of the iRODS server.
14+
port:
15+
label: iRODS port
16+
type: integer
17+
help: Port of the iRODS server.
18+
default: 1247
19+
zone:
20+
label: iRODS zone
21+
type: string
22+
help: Zone used for authentication (for example teamZone).
23+
username:
24+
label: iRODS user
25+
type: string
26+
help: Username used to authenticate against iRODS.
27+
subpath:
28+
label: Subfolder path
29+
type: string
30+
optional: true
31+
help: Optional subfolder under your personal directory (for example projectName).
32+
writable:
33+
label: Writable?
34+
type: boolean
35+
optional: true
36+
help: Allow writing datasets back to iRODS.
37+
default: false
38+
secrets:
39+
password:
40+
label: Password
41+
help: Password used to authenticate against iRODS.
42+
configuration:
43+
type: irods
44+
host: "{{ variables.host }}"
45+
port: "{{ variables.port }}"
46+
username: "{{ variables.username }}"
47+
password: "{{ secrets.password }}"
48+
zone: "{{ variables.zone }}"
49+
root: "/{{ variables.zone }}/home/{{ variables.username }}{% if variables.get('subpath') %}/{{ variables.get('subpath') | ensure_path_component }}{% endif %}"
50+
timeout: 30
51+
refresh_time: 300
52+
writable: "{{ variables.writable }}"

test/unit/app/dependencies/test_deps.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
- type: dropbox
2727
- type: googledrive
2828
- type: irods
29+
- type: irods-fsspec
2930
"""
3031
JOB_CONF_YAML = """
3132
runners:
@@ -97,6 +98,7 @@ def test_fs_configured():
9798
assert cds.check_dropboxdrivefs()
9899
assert cds.check_webdav4()
99100
assert cds.check_fs_irods()
101+
assert cds.check_mangofs()
100102

101103

102104
def test_yaml_jobconf_runners():

0 commit comments

Comments
 (0)