1 Commits

Author SHA1 Message Date
orrisroot 36cad6db52 refactor: extract MdrsClient service layer for library portability
To improve the tool's portability as a Python library, the core logic
has been decoupled from the CLI interface. This allows developers to
programmatically interact with MDRS without relying on CLI-specific
argument parsing or local file-based caches.

- Introduce `MdrsClient` service layer to handle core operations.
- Abstract authentication state using `CacheInterface` and `InMemoryCache`.
- Migrate all CLI commands to utilize `MdrsClient` for execution.
- Separate `Doi` data model from API responses and move to `models/doi.py`.
- Update `README.md` to include Python API usage examples.
- Bump package version to 1.3.17.
2026-07-02 13:07:18 +09:00
28 changed files with 736 additions and 215 deletions
+26 -1
View File
@@ -8,7 +8,7 @@ The mdrs-client-python is python library and a command-line client for up- and d
poetry install poetry install
``` ```
## Example Usage ## CLI Usage
### config create ### config create
@@ -205,3 +205,28 @@ Show the help message and exit
```shell ```shell
mdrs -h mdrs -h
``` ```
## Python API Usage
You can also use this package as a Python library to programmatically interact with MDRS repositories.
```python
from mdrsclient.client import MdrsClient
from mdrsclient.cache import InMemoryCache
# 1. Setup client with an in-memory cache to avoid local `.mdrsclient` state files
cache = InMemoryCache()
client = MdrsClient.from_remote("neurodata", cache=cache)
# 2. Login to the remote server
client.login("username", "password")
# 3. Use service methods
labs = client.get_laboratories()
metadata = client.metadata("neurodata:/NIU/Repository/")
# Transfer files programmatically
client.upload("/path/to/local/data", "neurodata:/NIU/Repository/TEST/", is_recursive=True)
client.download("neurodata:/NIU/Repository/TEST/data", "/path/to/local", is_recursive=True)
```
+4
View File
@@ -1,3 +1,7 @@
from mdrsclient.__version__ import __version__ from mdrsclient.__version__ import __version__
__all__ = ["__version__"] __all__ = ["__version__"]
from mdrsclient.client import MdrsClient
__all__ = ["MdrsClient"]
+6 -2
View File
@@ -1,5 +1,9 @@
from importlib.metadata import version import importlib.metadata
try:
__version__ = importlib.metadata.version("mdrs-client-python")
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0-dev"
__version__ = version("mdrs-client-python")
__all__ = ["__version__"] __all__ = ["__version__"]
+7 -14
View File
@@ -5,36 +5,29 @@ from pydantic.dataclasses import dataclass
from mdrsclient.api.base import BaseApi from mdrsclient.api.base import BaseApi
from mdrsclient.api.utils import token_check from mdrsclient.api.utils import token_check
from mdrsclient.models.doi import Doi
@dataclass(frozen=True) @dataclass(frozen=True)
class DoiFolderRef: class DoiRetrieveFolderRef:
"""Nested folder reference returned inside a DOI response.
The DOI endpoint only returns the folder ``id``; ``laboratory_id`` must be
obtained by subsequently calling the folder retrieve endpoint.
"""
id: str id: str
@dataclass(frozen=True) @dataclass(frozen=True)
class DoiResponse: class DoiRetrieveResponse:
"""Response from GET v3/doi/{id}/."""
# The internal DOI suffix ID returned as a string (e.g. "20260429-001").
id: str id: str
doi: str doi: str
folder: DoiFolderRef folder: DoiRetrieveFolderRef
class DoiApi(BaseApi): class DoiApi(BaseApi):
ENTRYPOINT: Final[str] = "v3/doi/" ENTRYPOINT: Final[str] = "v3/doi/"
def retrieve(self, doi_id: str) -> DoiResponse: def retrieve(self, doi_id: str) -> Doi:
"""Retrieve the folder associated with a DOI suffix ID (GET v3/doi/{id}/).""" """Retrieve the folder associated with a DOI suffix ID (GET v3/doi/{id}/)."""
url = self.ENTRYPOINT + doi_id + "/" url = self.ENTRYPOINT + doi_id + "/"
token_check(self.connection) token_check(self.connection)
response = self.connection.get(url) response = self.connection.get(url)
self._raise_response_error(response) self._raise_response_error(response)
return TypeAdapter(DoiResponse).validate_python(response.json()) api_resp = TypeAdapter(DoiRetrieveResponse).validate_python(response.json())
return Doi(id=api_resp.id, doi=api_resp.doi, folder_id=api_resp.folder.id)
+63 -1
View File
@@ -2,6 +2,7 @@ import dataclasses
import hashlib import hashlib
import json import json
import os import os
from typing import Protocol, runtime_checkable
from pydantic import TypeAdapter, ValidationError from pydantic import TypeAdapter, ValidationError
from pydantic.dataclasses import dataclass from pydantic.dataclasses import dataclass
@@ -16,7 +17,7 @@ from mdrsclient.utils import FileLock
class CacheData: class CacheData:
user: User | None = None user: User | None = None
token: Token | None = None token: Token | None = None
laboratories: Laboratories = Laboratories() laboratories: Laboratories = dataclasses.field(default_factory=Laboratories)
digest: str = "" digest: str = ""
def clear(self) -> None: def clear(self) -> None:
@@ -43,6 +44,67 @@ class CacheData:
).hexdigest() ).hexdigest()
@runtime_checkable
class CacheInterface(Protocol):
@property
def token(self) -> Token | None: ...
@token.setter
def token(self, token: Token) -> None: ...
@token.deleter
def token(self) -> None: ...
@property
def user(self) -> User | None: ...
@user.setter
def user(self, user: User) -> None: ...
@user.deleter
def user(self) -> None: ...
@property
def laboratories(self) -> Laboratories: ...
@laboratories.setter
def laboratories(self, laboratories: Laboratories) -> None: ...
class InMemoryCache:
def __init__(self) -> None:
self.__data = CacheData()
@property
def token(self) -> Token | None:
return self.__data.token
@token.setter
def token(self, token: Token) -> None:
self.__data.token = token
@token.deleter
def token(self) -> None:
if self.__data.token is not None:
self.__data.token = None
@property
def user(self) -> User | None:
return self.__data.user
@user.setter
def user(self, user: User) -> None:
self.__data.user = user
@user.deleter
def user(self) -> None:
if self.__data.user is not None:
self.__data.user = None
@property
def laboratories(self) -> Laboratories:
return self.__data.laboratories
@laboratories.setter
def laboratories(self, laboratories: Laboratories) -> None:
self.__data.laboratories = laboratories
class CacheFile: class CacheFile:
__serial: int __serial: int
__cache_dir: str __cache_dir: str
+266
View File
@@ -0,0 +1,266 @@
import os
from concurrent.futures import ThreadPoolExecutor
from typing import Any
from unicodedata import normalize
from mdrsclient.api import DoiApi, FilesApi, FoldersApi, LaboratoriesApi, UsersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import IllegalArgumentException, MDRSException, UnauthorizedException, UnexpectedException
from mdrsclient.models import File, Folder, Laboratory, Token, User
from mdrsclient.models.file import find_file
from mdrsclient.settings import CONCURRENT
class MdrsClient:
"""Service layer client for MDRS."""
def __init__(self, connection: MDRSConnection):
self.connection = connection
@classmethod
def from_remote(cls, remote: str) -> "MdrsClient":
return cls(BaseCommand._create_connection(remote))
def login(self, username: str, password: str) -> tuple[Token, User]:
user_api = UsersApi(self.connection)
token = user_api.token(username, password)
self.connection.token = token
user = user_api.current()
self.connection.user = user
return token, user
def logout(self) -> None:
self.connection.logout()
def whoami(self) -> User:
user_api = UsersApi(self.connection)
return user_api.current()
def get_laboratories(self) -> list[Laboratory]:
laboratory_api = LaboratoriesApi(self.connection)
labs = laboratory_api.list()
self.connection.laboratories = labs
return list(labs)
def mkdir(self, remote_path: str) -> None:
remote, laboratory_name, r_path = BaseCommand._parse_remote_host_with_path(remote_path)
r_path = r_path.rstrip("/")
r_dirname = os.path.dirname(r_path)
r_basename = os.path.basename(r_path)
laboratory = BaseCommand._find_laboratory(self.connection, laboratory_name)
parent_folder = BaseCommand._find_folder(self.connection, laboratory, r_dirname)
files = BaseCommand._find_files(self.connection, parent_folder.id)
if parent_folder.find_sub_folder(r_basename) is not None or find_file(files, r_basename) is not None:
raise IllegalArgumentException(f"Cannot create folder `{r_path}`: File exists.")
folder_api = FoldersApi(self.connection)
folder_api.create(normalize("NFC", r_basename), parent_folder.id)
def rm(self, remote_path: str, is_recursive: bool = False) -> None:
remote, laboratory_name, r_path = BaseCommand._parse_remote_host_with_path(remote_path)
r_path = r_path.rstrip("/")
r_dirname = os.path.dirname(r_path)
r_basename = os.path.basename(r_path)
laboratory = BaseCommand._find_laboratory(self.connection, laboratory_name)
parent_folder = BaseCommand._find_folder(self.connection, laboratory, r_dirname)
parent_files = BaseCommand._find_files(self.connection, parent_folder.id)
file = find_file(parent_files, r_basename)
if file is not None:
file_api = FilesApi(self.connection)
file_api.destroy(file)
else:
folder = parent_folder.find_sub_folder(r_basename)
if folder is None:
raise IllegalArgumentException(f"Cannot remove `{r_path}`: No such file or folder.")
if not is_recursive:
raise IllegalArgumentException(f"Cannot remove `{r_path}`: Is a folder.")
folder_api = FoldersApi(self.connection)
folder_api.destroy(folder.id, True)
def ls(self, remote_path: str, password: str | None = None) -> tuple[Folder, list[File]]:
folder, laboratory = BaseCommand._resolve_folder(self.connection, remote_path, password)
files = BaseCommand._find_files(self.connection, folder.id)
return folder, files
def cp(self, src_path: str, dest_path: str, is_recursive: bool = False) -> None:
s_remote, s_laboratory_name, s_path = BaseCommand._parse_remote_host_with_path(src_path)
d_remote, d_laboratory_name, d_path = BaseCommand._parse_remote_host_with_path(dest_path)
if s_remote != d_remote:
raise IllegalArgumentException("Remote host mismatched.")
if s_laboratory_name != d_laboratory_name:
raise IllegalArgumentException("Laboratory mismatched.")
s_path = s_path.rstrip("/")
s_dirname = os.path.dirname(s_path)
s_basename = os.path.basename(s_path)
if d_path.endswith("/"):
d_dirname = d_path
d_basename = s_basename
else:
d_dirname = os.path.dirname(d_path)
d_basename = os.path.basename(d_path)
laboratory = BaseCommand._find_laboratory(self.connection, s_laboratory_name)
s_parent_folder = BaseCommand._find_folder(self.connection, laboratory, s_dirname)
s_parent_files = BaseCommand._find_files(self.connection, s_parent_folder.id)
d_parent_folder = BaseCommand._find_folder(self.connection, laboratory, d_dirname)
d_parent_files = BaseCommand._find_files(self.connection, d_parent_folder.id)
s_file = find_file(s_parent_files, s_basename)
if s_file is not None:
d_file = find_file(d_parent_files, d_basename)
if d_file is not None:
raise IllegalArgumentException(f"File `{d_basename}` already exists.")
d_sub_folder = d_parent_folder.find_sub_folder(d_basename)
if d_sub_folder is not None:
raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{d_path}`.")
file_api = FilesApi(self.connection)
if s_parent_folder.id != d_parent_folder.id or d_basename != s_basename:
file_api.copy(s_file, d_parent_folder.id, normalize("NFC", d_basename))
else:
s_folder = s_parent_folder.find_sub_folder(s_basename)
if s_folder is None:
raise IllegalArgumentException(f"File or folder `{s_basename}` not found.")
if not is_recursive:
raise IllegalArgumentException(f"Cannot copy `{s_path}`: Is a folder.")
if find_file(d_parent_files, d_basename) is not None:
raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{s_path}`.")
d_folder = d_parent_folder.find_sub_folder(d_basename)
if d_folder is not None:
if d_folder.id == s_folder.id:
raise IllegalArgumentException(f"`{s_path}` and `{s_path}` are the same folder.")
raise IllegalArgumentException(f"Cannot move `{s_path}` to `{d_path}`: Folder not empty.")
folder_api = FoldersApi(self.connection)
if s_parent_folder.id != d_parent_folder.id or s_basename != d_basename:
folder_api.copy(s_folder, d_parent_folder.id, normalize("NFC", d_basename))
def mv(self, src_path: str, dest_path: str) -> None:
s_remote, s_laboratory_name, s_path = BaseCommand._parse_remote_host_with_path(src_path)
d_remote, d_laboratory_name, d_path = BaseCommand._parse_remote_host_with_path(dest_path)
if s_remote != d_remote:
raise IllegalArgumentException("Remote host mismatched.")
if s_laboratory_name != d_laboratory_name:
raise IllegalArgumentException("Laboratory mismatched.")
s_path = s_path.rstrip("/")
s_dirname = os.path.dirname(s_path)
s_basename = os.path.basename(s_path)
if d_path.endswith("/"):
d_dirname = d_path
d_basename = s_basename
else:
d_dirname = os.path.dirname(d_path)
d_basename = os.path.basename(d_path)
laboratory = BaseCommand._find_laboratory(self.connection, s_laboratory_name)
s_parent_folder = BaseCommand._find_folder(self.connection, laboratory, s_dirname)
s_parent_files = BaseCommand._find_files(self.connection, s_parent_folder.id)
d_parent_folder = BaseCommand._find_folder(self.connection, laboratory, d_dirname)
d_parent_files = BaseCommand._find_files(self.connection, d_parent_folder.id)
s_file = find_file(s_parent_files, s_basename)
if s_file is not None:
d_file = find_file(d_parent_files, d_basename)
if d_file is not None:
raise IllegalArgumentException(f"File `{d_basename}` already exists.")
d_sub_folder = d_parent_folder.find_sub_folder(d_basename)
if d_sub_folder is not None:
raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{d_path}`.")
file_api = FilesApi(self.connection)
if s_parent_folder.id != d_parent_folder.id or d_basename != s_basename:
file_api.move(s_file, d_parent_folder.id, normalize("NFC", d_basename))
else:
s_folder = s_parent_folder.find_sub_folder(s_basename)
if s_folder is None:
raise IllegalArgumentException(f"File or folder `{s_basename}` not found.")
if find_file(d_parent_files, d_basename) is not None:
raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{s_path}`.")
d_folder = d_parent_folder.find_sub_folder(d_basename)
if d_folder is not None:
if d_folder.id == s_folder.id:
raise IllegalArgumentException(f"`{s_path}` and `{s_path}` are the same folder.")
raise IllegalArgumentException(f"Cannot move `{s_path}` to `{d_path}`: Folder not empty.")
folder_api = FoldersApi(self.connection)
if s_parent_folder.id != d_parent_folder.id or d_basename != s_basename:
folder_api.move(s_folder, d_parent_folder.id, normalize("NFC", d_basename))
def chacl(
self, remote_path: str, access_level: int, is_recursive: bool = False, password: str | None = None
) -> None:
remote, laboratory_name, r_path = BaseCommand._parse_remote_host_with_path(remote_path)
r_path = r_path.rstrip("/")
laboratory = BaseCommand._find_laboratory(self.connection, laboratory_name)
folder = BaseCommand._find_folder(self.connection, laboratory, r_path)
folder_api = FoldersApi(self.connection)
folder_api.acl(folder.id, access_level, is_recursive, password)
def metadata(self, remote_path: str, password: str | None = None) -> dict:
folder, laboratory = BaseCommand._resolve_folder(self.connection, remote_path, password)
folder_api = FoldersApi(self.connection)
return folder_api.metadata(folder.id)
def file_metadata(self, remote_path: str, password: str | None = None) -> dict:
folder, laboratory, r_basename = BaseCommand._resolve_file(self.connection, remote_path, password)
files = BaseCommand._find_files(self.connection, folder.id)
file = find_file(files, r_basename)
if file is None:
raise IllegalArgumentException(f"File `{r_basename}` not found.")
file_api = FilesApi(self.connection)
return file_api.metadata(file)
def _create_connection(self, remote: str):
return self.connection
def upload(
self, local_path: str, remote_path: str, is_recursive: bool = False, is_skip_if_exists: bool = False
) -> None:
from mdrsclient.commands.upload import UploadCommand
UploadCommand._upload_logic(self.connection, local_path, remote_path, is_recursive, is_skip_if_exists)
def download(
self,
remote_path: str,
local_path: str,
is_recursive: bool = False,
is_skip_if_exists: bool = False,
password: str | None = None,
excludes: list[str] | None = None,
) -> None:
from mdrsclient.commands.download import DownloadCommand
DownloadCommand._download_logic(
self.connection, remote_path, local_path, is_recursive, is_skip_if_exists, password, excludes or []
)
def ls_command(
self,
remote_path: str,
password: str | None = None,
is_json: bool = False,
is_recursive: bool = False,
is_quiet: bool = False,
) -> None:
from mdrsclient.commands.ls import LsCommand
LsCommand._ls_logic(self.connection, remote_path, password, is_json, is_recursive, is_quiet)
def version(self) -> str:
from mdrsclient.__version__ import __version__
return f"mdrs {__version__}"
def config_create(self, remote: str, url: str) -> None:
from mdrsclient.commands.config import ConfigCommand
ConfigCommand.create(remote, url)
def config_update(self, remote: str, url: str) -> None:
from mdrsclient.commands.config import ConfigCommand
ConfigCommand.update(remote, url)
def config_list(self) -> list:
from mdrsclient.config import ConfigFile
config = ConfigFile("")
return config.list()
def config_delete(self, remote: str) -> None:
from mdrsclient.commands.config import ConfigCommand
ConfigCommand.delete(remote)
+1 -1
View File
@@ -196,7 +196,7 @@ class BaseCommand(ABC):
folder_api = FoldersApi(connection) folder_api = FoldersApi(connection)
# Retrieve full folder detail directly by ID; laboratory_id is here. # Retrieve full folder detail directly by ID; laboratory_id is here.
folder = folder_api.retrieve(doi_resp.folder.id) folder = folder_api.retrieve(doi_resp.folder_id)
if folder.lock: if folder.lock:
if password is None: if password is None:
+5 -7
View File
@@ -31,10 +31,8 @@ class ChaclCommand(BaseCommand):
@classmethod @classmethod
def chacl(cls, remote_path: str, access_level: int, is_recursive: bool, password: str | None) -> None: def chacl(cls, remote_path: str, access_level: int, is_recursive: bool, password: str | None) -> None:
remote, laboratory_name, r_path = cls._parse_remote_host_with_path(remote_path) remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
r_path = r_path.rstrip("/") from mdrsclient.client import MdrsClient
connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name) client = MdrsClient.from_remote(remote)
folder = cls._find_folder(connection, laboratory, r_path) client.chacl(remote_path, access_level, is_recursive, password)
folder_api = FoldersApi(connection)
folder_api.acl(folder.id, access_level, is_recursive, password)
+13 -5
View File
@@ -38,13 +38,17 @@ class ConfigCommand(BaseCommand):
def func_create(cls, args: Namespace) -> None: def func_create(cls, args: Namespace) -> None:
remote = str(args.remote) remote = str(args.remote)
url = str(args.url) url = str(args.url)
cls.create(remote, url) from mdrsclient.client import MdrsClient
MdrsClient(None).config_create(remote, url)
@classmethod @classmethod
def func_update(cls, args: Namespace) -> None: def func_update(cls, args: Namespace) -> None:
remote = str(args.remote) remote = str(args.remote)
url = str(args.url) url = str(args.url)
cls.update(remote, url) from mdrsclient.client import MdrsClient
MdrsClient(None).config_update(remote, url)
@classmethod @classmethod
def func_list(cls, args: Namespace) -> None: def func_list(cls, args: Namespace) -> None:
@@ -53,7 +57,9 @@ class ConfigCommand(BaseCommand):
@classmethod @classmethod
def func_delete(cls, args: Namespace) -> None: def func_delete(cls, args: Namespace) -> None:
remote = str(args.remote) remote = str(args.remote)
cls.delete(remote) from mdrsclient.client import MdrsClient
MdrsClient(None).config_delete(remote)
@classmethod @classmethod
def create(cls, remote: str, url: str) -> None: def create(cls, remote: str, url: str) -> None:
@@ -75,8 +81,10 @@ class ConfigCommand(BaseCommand):
@classmethod @classmethod
def list(cls) -> None: def list(cls) -> None:
config = ConfigFile("") from mdrsclient.client import MdrsClient
for remote, url in config.list():
client = MdrsClient(None)
for remote, url in client.config_list():
print(f"{remote}:\t{url}") print(f"{remote}:\t{url}")
@classmethod @classmethod
+5 -50
View File
@@ -29,53 +29,8 @@ class CpCommand(BaseCommand):
@classmethod @classmethod
def cp(cls, src_path: str, dest_path: str, is_recursive: bool) -> None: def cp(cls, src_path: str, dest_path: str, is_recursive: bool) -> None:
s_remote, s_laboratory_name, s_path = cls._parse_remote_host_with_path(src_path) remote = src_path.split(":", 1)[0] if ":" in src_path else ""
d_remote, d_laboratory_name, d_path = cls._parse_remote_host_with_path(dest_path) from mdrsclient.client import MdrsClient
if s_remote != d_remote:
raise IllegalArgumentException("Remote host mismatched.") client = MdrsClient.from_remote(remote)
if s_laboratory_name != d_laboratory_name: client.cp(src_path, dest_path, is_recursive)
raise IllegalArgumentException("Laboratory mismatched.")
s_path = s_path.rstrip("/")
s_dirname = os.path.dirname(s_path)
s_basename = os.path.basename(s_path)
if d_path.endswith("/"):
d_dirname = d_path
d_basename = s_basename
else:
d_dirname = os.path.dirname(d_path)
d_basename = os.path.basename(d_path)
connection = cls._create_connection(s_remote)
laboratory = cls._find_laboratory(connection, s_laboratory_name)
s_parent_folder = cls._find_folder(connection, laboratory, s_dirname)
s_parent_files = cls._find_files(connection, s_parent_folder.id)
d_parent_folder = cls._find_folder(connection, laboratory, d_dirname)
d_parent_files = cls._find_files(connection, d_parent_folder.id)
s_file = find_file(s_parent_files, s_basename)
if s_file is not None:
# source is file
d_file = find_file(d_parent_files, d_basename)
if d_file is not None:
raise IllegalArgumentException(f"File `{d_basename}` already exists.")
d_sub_folder = d_parent_folder.find_sub_folder(d_basename)
if d_sub_folder is not None:
raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{d_path}`.")
file_api = FilesApi(connection)
if s_parent_folder.id != d_parent_folder.id or d_basename != s_basename:
file_api.copy(s_file, d_parent_folder.id, normalize("NFC", d_basename))
else:
s_folder = s_parent_folder.find_sub_folder(s_basename)
if s_folder is None:
raise IllegalArgumentException(f"File or folder `{s_basename}` not found.")
# source is folder
if not is_recursive:
raise IllegalArgumentException(f"Cannot copy `{s_path}`: Is a folder.")
if find_file(d_parent_files, d_basename) is not None:
raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{s_path}`.")
d_folder = d_parent_folder.find_sub_folder(d_basename)
if d_folder is not None:
if d_folder.id == s_folder.id:
raise IllegalArgumentException(f"`{s_path}` and `{s_path}` are the same folder.")
raise IllegalArgumentException(f"Cannot move `{s_path}` to `{d_path}`: Folder not empty.")
folder_api = FoldersApi(connection)
if s_parent_folder.id != d_parent_folder.id or s_basename != d_basename:
folder_api.copy(s_folder, d_parent_folder.id, normalize("NFC", d_basename))
+20 -2
View File
@@ -67,12 +67,30 @@ class DownloadCommand(BaseCommand):
is_skip_if_exists: bool, is_skip_if_exists: bool,
password: str | None, password: str | None,
excludes: list[str], excludes: list[str],
) -> None:
remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
from mdrsclient.client import MdrsClient
client = MdrsClient.from_remote(remote)
client.download(remote_path, local_path, is_recursive, is_skip_if_exists, password, excludes)
return
@classmethod
def _download_logic(
cls,
connection: MDRSConnection,
remote_path: str,
local_path: str,
is_recursive: bool,
is_skip_if_exists: bool,
password: str | None,
excludes: list[str],
) -> None: ) -> None:
# Detect DOI path: "remote:10.xxxx/prefix.ID[/optional/sub/path]" # Detect DOI path: "remote:10.xxxx/prefix.ID[/optional/sub/path]"
path_component = remote_path.split(":", 1)[1] if ":" in remote_path else "" path_component = remote_path.split(":", 1)[1] if ":" in remote_path else ""
if cls._is_doi(path_component): if cls._is_doi(path_component):
remote, doi, subpath = cls._parse_doi_remote_host(remote_path) remote, doi, subpath = cls._parse_doi_remote_host(remote_path)
connection = cls._create_connection(remote)
l_dirname = os.path.realpath(local_path) l_dirname = os.path.realpath(local_path)
if not os.path.isdir(l_dirname): if not os.path.isdir(l_dirname):
raise IllegalArgumentException(f"Local directory `{local_path}` not found.") raise IllegalArgumentException(f"Local directory `{local_path}` not found.")
@@ -126,7 +144,7 @@ class DownloadCommand(BaseCommand):
r_path = r_path.rstrip("/") r_path = r_path.rstrip("/")
r_dirname = os.path.dirname(r_path) r_dirname = os.path.dirname(r_path)
r_basename = os.path.basename(r_path) r_basename = os.path.basename(r_path)
connection = cls._create_connection(remote)
l_dirname = os.path.realpath(local_path) l_dirname = os.path.realpath(local_path)
if not os.path.isdir(l_dirname): if not os.path.isdir(l_dirname):
raise IllegalArgumentException(f"Local directory `{local_path}` not found.") raise IllegalArgumentException(f"Local directory `{local_path}` not found.")
+4 -8
View File
@@ -26,12 +26,8 @@ class FileMetadataCommand(BaseCommand):
@classmethod @classmethod
def file_metadata(cls, remote_path: str, password: str | None) -> None: def file_metadata(cls, remote_path: str, password: str | None) -> None:
remote = remote_path.split(":", 1)[0] if ":" in remote_path else "" remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
connection = cls._create_connection(remote) from mdrsclient.client import MdrsClient
folder, laboratory, r_basename = cls._resolve_file(connection, remote_path, password)
files = cls._find_files(connection, folder.id) client = MdrsClient.from_remote(remote)
file = find_file(files, r_basename) metadata = client.file_metadata(remote_path, password)
if file is None:
raise IllegalArgumentException(f"File `{r_basename}` not found.")
file_api = FilesApi(connection)
metadata = file_api.metadata(file)
print(json.dumps(metadata, ensure_ascii=False)) print(json.dumps(metadata, ensure_ascii=False))
+5 -7
View File
@@ -19,11 +19,11 @@ class LabsCommand(BaseCommand):
@classmethod @classmethod
def labs(cls, remote: str) -> None: def labs(cls, remote: str) -> None:
remote = cls._parse_remote_host(remote) remote_host = cls._parse_remote_host(remote)
connection = cls._create_connection(remote) from mdrsclient.client import MdrsClient
laboratory_api = LaboratoriesApi(connection)
laboratories = laboratory_api.list() client = MdrsClient.from_remote(remote_host)
connection.laboratories = laboratories laboratories = client.get_laboratories()
label = {"id": "ID", "name": "Name", "pi_name": "PI", "full_name": "Laboratory"} label = {"id": "ID", "name": "Name", "pi_name": "PI", "full_name": "Laboratory"}
length: dict[str, int] = {} length: dict[str, int] = {}
for key in label.keys(): for key in label.keys():
@@ -34,7 +34,6 @@ class LabsCommand(BaseCommand):
length["pi_name"] = max(length["pi_name"], len(laboratory.pi_name)) length["pi_name"] = max(length["pi_name"], len(laboratory.pi_name))
length["full_name"] = max(length["full_name"], len(laboratory.full_name)) length["full_name"] = max(length["full_name"], len(laboratory.full_name))
header = ( header = (
# f"{label['id']:{length['id']}}\t{label['name']:{length['name']}}\t"
f"{label['name']:{length['name']}}\t" f"{label['name']:{length['name']}}\t"
f"{label['pi_name']:{length['pi_name']}}\t{label['full_name']:{length['full_name']}}" f"{label['pi_name']:{length['pi_name']}}\t{label['full_name']:{length['full_name']}}"
) )
@@ -42,7 +41,6 @@ class LabsCommand(BaseCommand):
print("-" * len(header.expandtabs())) print("-" * len(header.expandtabs()))
for laboratory in laboratories: for laboratory in laboratories:
print( print(
# f"{laboratory.id:{length['id']}}\t{laboratory.name:{length['name']}}\t"
f"{laboratory.name:{length['name']}}\t" f"{laboratory.name:{length['name']}}\t"
f"{laboratory.pi_name:{length['pi_name']}}\t{laboratory.full_name:{length['full_name']}}" f"{laboratory.pi_name:{length['pi_name']}}\t{laboratory.full_name:{length['full_name']}}"
) )
+6 -11
View File
@@ -21,20 +21,15 @@ class LoginCommand(BaseCommand):
@classmethod @classmethod
def func(cls, args: Namespace) -> None: def func(cls, args: Namespace) -> None:
remote = str(args.remote) remote = str(args.remote)
username = str(args.username) if args.password else input("Username: ").strip() username = str(args.username) if args.username else input("Username: ").strip()
password = str(args.password) if args.password else getpass.getpass("Password: ").strip() password = str(args.password) if args.password else getpass.getpass("Password: ").strip()
cls.login(remote, username, password) cls.login(remote, username, password)
@classmethod @classmethod
def login(cls, remote: str, username: str, password: str) -> None: def login(cls, remote: str, username: str, password: str) -> None:
remote = cls._parse_remote_host(remote) remote_host = cls._parse_remote_host(remote)
config = ConfigFile(remote) from mdrsclient.client import MdrsClient
if config.url is None:
raise MissingConfigurationException(f"Remote host `{remote}` is not found.") client = MdrsClient.from_remote(remote_host)
connection = MDRSConnection(config.remote, config.url) client.login(username, password)
user_api = UsersApi(connection)
token = user_api.token(username, password)
connection.token = token
user = user_api.current()
connection.user = user
print("Login Successful") print("Login Successful")
+5 -6
View File
@@ -21,9 +21,8 @@ class LogoutCommand(BaseCommand):
@classmethod @classmethod
def logout(cls, remote: str) -> None: def logout(cls, remote: str) -> None:
remote = cls._parse_remote_host(remote) remote_host = cls._parse_remote_host(remote)
config = ConfigFile(remote) from mdrsclient.client import MdrsClient
if config.url is None:
raise MissingConfigurationException(f"Remote host `{remote}` is not found.") client = MdrsClient.from_remote(remote_host)
connection = MDRSConnection(config.remote, config.url) client.logout()
connection.logout()
+12 -1
View File
@@ -55,7 +55,18 @@ class LsCommand(BaseCommand):
@classmethod @classmethod
def ls(cls, remote_path: str, password: str | None, is_json: bool, is_recursive: bool, is_quiet: bool) -> None: def ls(cls, remote_path: str, password: str | None, is_json: bool, is_recursive: bool, is_quiet: bool) -> None:
remote = remote_path.split(":", 1)[0] if ":" in remote_path else "" remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
connection = cls._create_connection(remote) from mdrsclient.client import MdrsClient
client = MdrsClient.from_remote(remote)
client.ls_command(remote_path, password, is_json, is_recursive, is_quiet)
return
@classmethod
def _ls_logic(
cls, connection, remote_path: str, password: str | None, is_json: bool, is_recursive: bool, is_quiet: bool
) -> None:
remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
folder, laboratory = cls._resolve_folder(connection, remote_path, password) folder, laboratory = cls._resolve_folder(connection, remote_path, password)
laboratory_name = laboratory.name laboratory_name = laboratory.name
files = cls._find_files(connection, folder.id) files = cls._find_files(connection, folder.id)
+4 -4
View File
@@ -23,8 +23,8 @@ class MetadataCommand(BaseCommand):
@classmethod @classmethod
def metadata(cls, remote_path: str, password: str | None) -> None: def metadata(cls, remote_path: str, password: str | None) -> None:
remote = remote_path.split(":", 1)[0] if ":" in remote_path else "" remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
connection = cls._create_connection(remote) from mdrsclient.client import MdrsClient
folder, laboratory = cls._resolve_folder(connection, remote_path, password)
folder_api = FoldersApi(connection) client = MdrsClient.from_remote(remote)
metadata = folder_api.metadata(folder.id) metadata = client.metadata(remote_path, password)
print(json.dumps(metadata, ensure_ascii=False)) print(json.dumps(metadata, ensure_ascii=False))
+5 -12
View File
@@ -23,15 +23,8 @@ class MkdirCommand(BaseCommand):
@classmethod @classmethod
def mkdir(cls, remote_path: str) -> None: def mkdir(cls, remote_path: str) -> None:
remote, laboratory_name, r_path = cls._parse_remote_host_with_path(remote_path) remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
r_path = r_path.rstrip("/") from mdrsclient.client import MdrsClient
r_dirname = os.path.dirname(r_path)
r_basename = os.path.basename(r_path) client = MdrsClient.from_remote(remote)
connection = cls._create_connection(remote) client.mkdir(remote_path)
laboratory = cls._find_laboratory(connection, laboratory_name)
parent_folder = cls._find_folder(connection, laboratory, r_dirname)
files = cls._find_files(connection, parent_folder.id)
if parent_folder.find_sub_folder(r_basename) is not None or find_file(files, r_basename) is not None:
raise IllegalArgumentException(f"Cannot create folder `{r_path}`: File exists.")
folder_api = FoldersApi(connection)
folder_api.create(normalize("NFC", r_basename), parent_folder.id)
+5 -48
View File
@@ -25,51 +25,8 @@ class MvCommand(BaseCommand):
@classmethod @classmethod
def mv(cls, src_path: str, dest_path: str) -> None: def mv(cls, src_path: str, dest_path: str) -> None:
s_remote, s_laboratory_name, s_path = cls._parse_remote_host_with_path(src_path) remote = src_path.split(":", 1)[0] if ":" in src_path else ""
d_remote, d_laboratory_name, d_path = cls._parse_remote_host_with_path(dest_path) from mdrsclient.client import MdrsClient
if s_remote != d_remote:
raise IllegalArgumentException("Remote host mismatched.") client = MdrsClient.from_remote(remote)
if s_laboratory_name != d_laboratory_name: client.mv(src_path, dest_path)
raise IllegalArgumentException("Laboratory mismatched.")
s_path = s_path.rstrip("/")
s_dirname = os.path.dirname(s_path)
s_basename = os.path.basename(s_path)
if d_path.endswith("/"):
d_dirname = d_path
d_basename = s_basename
else:
d_dirname = os.path.dirname(d_path)
d_basename = os.path.basename(d_path)
connection = cls._create_connection(s_remote)
laboratory = cls._find_laboratory(connection, s_laboratory_name)
s_parent_folder = cls._find_folder(connection, laboratory, s_dirname)
s_parent_files = cls._find_files(connection, s_parent_folder.id)
d_parent_folder = cls._find_folder(connection, laboratory, d_dirname)
d_parent_files = cls._find_files(connection, d_parent_folder.id)
s_file = find_file(s_parent_files, s_basename)
if s_file is not None:
# source is file
d_file = find_file(d_parent_files, d_basename)
if d_file is not None:
raise IllegalArgumentException(f"File `{d_basename}` already exists.")
d_sub_folder = d_parent_folder.find_sub_folder(d_basename)
if d_sub_folder is not None:
raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{d_path}`.")
file_api = FilesApi(connection)
if s_parent_folder.id != d_parent_folder.id or d_basename != s_basename:
file_api.move(s_file, d_parent_folder.id, normalize("NFC", d_basename))
else:
s_folder = s_parent_folder.find_sub_folder(s_basename)
if s_folder is None:
raise IllegalArgumentException(f"File or folder `{s_basename}` not found.")
# source is folder
if find_file(d_parent_files, d_basename) is not None:
raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{s_path}`.")
d_folder = d_parent_folder.find_sub_folder(d_basename)
if d_folder is not None:
if d_folder.id == s_folder.id:
raise IllegalArgumentException(f"`{s_path}` and `{s_path}` are the same folder.")
raise IllegalArgumentException(f"Cannot move `{s_path}` to `{d_path}`: Folder not empty.")
folder_api = FoldersApi(connection)
if s_parent_folder.id != d_parent_folder.id or d_basename != s_basename:
folder_api.move(s_folder, d_parent_folder.id, normalize("NFC", d_basename))
+5 -20
View File
@@ -26,23 +26,8 @@ class RmCommand(BaseCommand):
@classmethod @classmethod
def rm(cls, remote_path: str, is_recursive: bool) -> None: def rm(cls, remote_path: str, is_recursive: bool) -> None:
remote, laboratory_name, r_path = cls._parse_remote_host_with_path(remote_path) remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
r_path = r_path.rstrip("/") from mdrsclient.client import MdrsClient
r_dirname = os.path.dirname(r_path)
r_basename = os.path.basename(r_path) client = MdrsClient.from_remote(remote)
connection = cls._create_connection(remote) client.rm(remote_path, is_recursive)
laboratory = cls._find_laboratory(connection, laboratory_name)
parent_folder = cls._find_folder(connection, laboratory, r_dirname)
parent_files = cls._find_files(connection, parent_folder.id)
file = find_file(parent_files, r_basename)
if file is not None:
file_api = FilesApi(connection)
file_api.destroy(file)
else:
folder = parent_folder.find_sub_folder(r_basename)
if folder is None:
raise IllegalArgumentException(f"Cannot remove `{r_path}`: No such file or folder.")
if not is_recursive:
raise IllegalArgumentException(f"Cannot remove `{r_path}`: Is a folder.")
folder_api = FoldersApi(connection)
folder_api.destroy(folder.id, True)
+12 -1
View File
@@ -49,11 +49,22 @@ class UploadCommand(BaseCommand):
@classmethod @classmethod
def upload(cls, local_path: str, remote_path: str, is_recursive: bool, is_skip_if_exists: bool) -> None: def upload(cls, local_path: str, remote_path: str, is_recursive: bool, is_skip_if_exists: bool) -> None:
remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
from mdrsclient.client import MdrsClient
client = MdrsClient.from_remote(remote)
client.upload(local_path, remote_path, is_recursive, is_skip_if_exists)
return
@classmethod
def _upload_logic(
cls, connection, local_path: str, remote_path: str, is_recursive: bool, is_skip_if_exists: bool
) -> None:
remote, laboratory_name, r_path = cls._parse_remote_host_with_path(remote_path) remote, laboratory_name, r_path = cls._parse_remote_host_with_path(remote_path)
l_path = os.path.abspath(local_path) l_path = os.path.abspath(local_path)
if not os.path.exists(l_path): if not os.path.exists(l_path):
raise IllegalArgumentException(f"File or directory `{local_path}` not found.") raise IllegalArgumentException(f"File or directory `{local_path}` not found.")
connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name) laboratory = cls._find_laboratory(connection, laboratory_name)
folder = cls._find_folder(connection, laboratory, r_path) folder = cls._find_folder(connection, laboratory, r_path)
files = cls._find_files(connection, folder.id) files = cls._find_files(connection, folder.id)
+5 -1
View File
@@ -17,4 +17,8 @@ class VersionCommand(BaseCommand):
@classmethod @classmethod
def version(cls) -> None: def version(cls) -> None:
print(f"mdrs {__version__}") from mdrsclient.client import MdrsClient
# Client initialization is not strictly needed for version, but for consistency:
client = MdrsClient(None)
print(client.version())
+11 -8
View File
@@ -23,12 +23,15 @@ class WhoamiCommand(BaseCommand):
@classmethod @classmethod
def whoami(cls, remote: str) -> None: def whoami(cls, remote: str) -> None:
remote = cls._parse_remote_host(remote) remote_host = cls._parse_remote_host(remote)
config = ConfigFile(remote) from mdrsclient.client import MdrsClient
if config.url is None:
raise MissingConfigurationException(f"Remote host `{remote}` is not found.") client = MdrsClient.from_remote(remote_host)
connection = MDRSConnection(config.remote, config.url) if client.connection.token is not None and client.connection.token.is_expired:
if connection.token is not None and connection.token.is_expired: client.logout()
connection.logout() try:
username = connection.user.username if connection.user is not None else cls.ANONYMOUS_USERNAME user = client.whoami()
username = user.username if user is not None else cls.ANONYMOUS_USERNAME
except Exception:
username = cls.ANONYMOUS_USERNAME
print(username) print(username)
+4 -4
View File
@@ -9,7 +9,7 @@ from requests_toolbelt.multipart.encoder import MultipartEncoder
from typing_extensions import Unpack from typing_extensions import Unpack
from mdrsclient.__version__ import __version__ from mdrsclient.__version__ import __version__
from mdrsclient.cache import CacheFile from mdrsclient.cache import CacheFile, CacheInterface
from mdrsclient.exceptions import MissingConfigurationException from mdrsclient.exceptions import MissingConfigurationException
from mdrsclient.models import Laboratories, Token, User from mdrsclient.models import Laboratories, Token, User
@@ -39,14 +39,14 @@ class MDRSConnection:
url: str url: str
session: Session session: Session
lock: threading.Lock lock: threading.Lock
__cache: CacheFile __cache: CacheInterface
def __init__(self, remote: str, url: str) -> None: def __init__(self, remote: str, url: str, cache: CacheInterface | None = None) -> None:
super().__init__() super().__init__()
self.url = url self.url = url
self.session = Session() self.session = Session()
self.lock = threading.Lock() self.lock = threading.Lock()
self.__cache = CacheFile(remote) self.__cache = cache if cache is not None else CacheFile(remote)
self.__prepare_headers() self.__prepare_headers()
def get(self, url: str, **kwargs: Unpack[_KwArgsMDRSConnectionGet]) -> Response: def get(self, url: str, **kwargs: Unpack[_KwArgsMDRSConnectionGet]) -> Response:
+2
View File
@@ -1,3 +1,4 @@
from mdrsclient.models.doi import Doi
from mdrsclient.models.error import DRFStandardizedErrors from mdrsclient.models.error import DRFStandardizedErrors
from mdrsclient.models.file import File from mdrsclient.models.file import File
from mdrsclient.models.folder import Folder, FolderAccessLevel, FolderSimple from mdrsclient.models.folder import Folder, FolderAccessLevel, FolderSimple
@@ -6,6 +7,7 @@ from mdrsclient.models.user import Token, User
__all__ = [ __all__ = [
"DRFStandardizedErrors", "DRFStandardizedErrors",
"Doi",
"File", "File",
"Folder", "Folder",
"FolderAccessLevel", "FolderAccessLevel",
+11
View File
@@ -0,0 +1,11 @@
from pydantic.dataclasses import dataclass
@dataclass(frozen=True)
class Doi:
"""Model representing a DOI entity (Response from GET v3/doi/{id}/)."""
# The internal DOI suffix ID returned as a string (e.g. "20260429-001").
id: str
doi: str
folder_id: str
+223
View File
@@ -0,0 +1,223 @@
from typing import Any
import os
import re
from unicodedata import normalize
from mdrsclient.api import DoiApi, FilesApi, FoldersApi, LaboratoriesApi, UsersApi
from mdrsclient.config import ConfigFile
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import (
IllegalArgumentException,
MissingConfigurationException,
UnauthorizedException,
UnexpectedException,
)
from mdrsclient.models import File, Folder, Laboratory, Token, User
from mdrsclient.utils import page_num_from_url
class MdrsService:
def __init__(self, connection: MDRSConnection):
self.connection = connection
@classmethod
def create_connection(cls, remote: str) -> MDRSConnection:
config = ConfigFile(remote)
if config.url is None:
raise MissingConfigurationException(f"Remote host `{remote}` is not found.")
return MDRSConnection(config.remote, config.url)
def login(self, username: str, password: str) -> tuple[Token, User]:
user_api = UsersApi(self.connection)
token = user_api.token(username, password)
self.connection.token = token
user = user_api.current()
self.connection.user = user
return token, user
def logout(self) -> None:
self.connection.logout()
def whoami(self) -> User:
user_api = UsersApi(self.connection)
return user_api.current()
def get_laboratories(self) -> list[Laboratory]:
laboratory_api = LaboratoriesApi(self.connection)
labs = laboratory_api.list()
self.connection.laboratories = labs
return list(labs)
def find_laboratory(self, name: str) -> Laboratory:
if self.connection.laboratories.empty() or (self.connection.token and self.connection.token.is_expired):
self.get_laboratories()
laboratory = self.connection.laboratories.find_by_name(name)
if laboratory is None:
raise IllegalArgumentException(f"Laboratory `{name}` not found.")
return laboratory
def find_folder(self, laboratory: Laboratory, path: str, password: str | None = None) -> Folder:
folder_api = FoldersApi(self.connection)
folders = folder_api.list(laboratory.id, normalize("NFC", path))
if len(folders) != 1:
raise UnexpectedException(f"Folder `{path}` not found.")
if folders[0].lock:
if password is None:
raise UnauthorizedException(f"Folder `{path}` is locked.")
folder_api.auth(folders[0].id, password)
return folder_api.retrieve(folders[0].id)
def find_files(self, folder_id: str) -> list[File]:
files_api = FilesApi(self.connection)
page = 1
results_file = []
while page:
result = files_api.list(folder_id, page)
results_file.extend(result.results)
page = 0
if result.next:
page = page_num_from_url(result.next)
return results_file
@staticmethod
def is_doi(path_component: str) -> bool:
return path_component.startswith("10.") and "/" in path_component
@staticmethod
def doi_suffix_id(doi: str) -> str:
doi = doi.rstrip("/")
slash_pos = doi.find("/")
if slash_pos == -1:
return doi
suffix = doi[slash_pos + 1 :]
dot_pos = suffix.rfind(".")
return suffix[dot_pos + 1 :] if dot_pos != -1 else suffix
@staticmethod
def split_doi_and_subpath(doi_with_path: str) -> tuple[str, str]:
first_slash = doi_with_path.find("/")
if first_slash != -1:
after_suffix_start = first_slash + 1
after_first = doi_with_path[after_suffix_start:]
second_slash = after_first.find("/")
if second_slash != -1:
doi_end = after_suffix_start + second_slash
doi = doi_with_path[:doi_end]
subpath = doi_with_path[doi_end:]
if subpath == "/":
return (doi, "")
else:
return (doi, subpath)
else:
return (doi_with_path, "")
else:
return (doi_with_path, "")
@classmethod
def parse_remote_host(cls, path: str) -> str:
path_array = path.split(":")
remote_host = path_array[0]
if len(path_array) == 2 and path_array[1] != "" or len(path_array) > 2:
raise IllegalArgumentException("Invalid remote host")
return remote_host
@classmethod
def parse_remote_host_with_path(cls, path: str) -> tuple[str, str, str]:
path = re.sub(r"//+|/\./+|/\.$", "/", path)
if re.search(r"/\.\./|/\.\.$", path) is not None:
raise IllegalArgumentException("Path traversal found.")
path_array = path.split(":")
if len(path_array) != 2:
raise IllegalArgumentException("Invalid remote host.")
remote_host = path_array[0]
folder_array = path_array[1].split("/")
is_absolute_path = folder_array[0] == ""
if not is_absolute_path:
raise IllegalArgumentException("Must be absolute paths.")
del folder_array[0]
if len(folder_array) == 0:
laboratory = ""
folder = ""
else:
laboratory = folder_array.pop(0)
folder = "/" + "/".join(folder_array)
return (remote_host, laboratory, folder)
@classmethod
def parse_doi_remote_host(cls, path: str) -> tuple[str, str, str]:
parts = path.split(":", 1)
if len(parts) != 2:
raise IllegalArgumentException("remote_path must be in the form 'remote:10.xxxx/prefix.ID'")
remote, doi_with_path = parts
if not cls.is_doi(doi_with_path):
raise IllegalArgumentException(f"Path `{doi_with_path}` does not look like a DOI.")
doi, subpath = cls.split_doi_and_subpath(doi_with_path)
return (remote, doi, subpath)
def find_folder_by_doi(self, doi: str, password: str | None = None) -> tuple[Folder, Laboratory]:
doi_clean = doi.rstrip("/")
doi_id = self.doi_suffix_id(doi_clean)
doi_api = DoiApi(self.connection)
doi_resp = doi_api.retrieve(doi_id)
returned_doi = doi_resp.doi.rstrip("/")
if returned_doi.lower() != doi_clean.lower():
raise IllegalArgumentException(
f"DOI mismatch: requested `{doi_clean}` but server returned `{returned_doi}`."
)
folder_api = FoldersApi(self.connection)
folder = folder_api.retrieve(doi_resp.folder_id)
if folder.lock:
if password is None:
raise UnauthorizedException(f"Folder for DOI `{doi_clean}` is locked.")
folder_api.auth(doi_resp.folder.id, password)
lab_api = LaboratoriesApi(self.connection)
labs = lab_api.list()
lab = labs.find_by_id(folder.laboratory_id)
if lab is None:
raise UnexpectedException(f"Laboratory with id {folder.laboratory_id} not found.")
self.connection.laboratories = labs
return (folder, lab)
def resolve_folder(self, remote_path: str, password: str | None = None) -> tuple[Folder, Laboratory]:
path_component = remote_path.split(":", 1)[1] if ":" in remote_path else ""
if self.is_doi(path_component):
remote, doi, subpath = self.parse_doi_remote_host(remote_path)
doi_folder, laboratory = self.find_folder_by_doi(doi, password)
if not subpath:
return (doi_folder, laboratory)
else:
abs_path = doi_folder.path.rstrip("/") + subpath
folder = self.find_folder(laboratory, abs_path, password)
return (folder, laboratory)
else:
remote, laboratory_name, r_path = self.parse_remote_host_with_path(remote_path)
laboratory = self.find_laboratory(laboratory_name)
folder = self.find_folder(laboratory, r_path, password)
return (folder, laboratory)
def resolve_file(self, remote_path: str, password: str | None = None) -> tuple[Folder, Laboratory, str]:
path_component = remote_path.split(":", 1)[1] if ":" in remote_path else ""
if self.is_doi(path_component):
remote, doi, subpath = self.parse_doi_remote_host(remote_path)
doi_folder, laboratory = self.find_folder_by_doi(doi, password)
subpath_clean = subpath.rstrip("/")
if not subpath_clean:
raise IllegalArgumentException("DOI path must point to a file, not a folder.")
r_dirname = os.path.dirname(subpath_clean)
r_basename = os.path.basename(subpath_clean)
abs_path = doi_folder.path.rstrip("/") + r_dirname
parent_folder = self.find_folder(laboratory, abs_path, password)
return (parent_folder, laboratory, r_basename)
else:
remote, laboratory_name, r_path = self.parse_remote_host_with_path(remote_path)
r_path = r_path.rstrip("/")
r_dirname = os.path.dirname(r_path)
r_basename = os.path.basename(r_path)
laboratory = self.find_laboratory(laboratory_name)
parent_folder = self.find_folder(laboratory, r_dirname, password)
return (parent_folder, laboratory, r_basename)
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "mdrs-client-python" name = "mdrs-client-python"
version = "1.3.16" version = "1.3.17"
description = "The mdrs-client-python is python library and a command-line client for up- and downloading files to and from MDRS based repository." description = "The mdrs-client-python is python library and a command-line client for up- and downloading files to and from MDRS based repository."
authors = ["Yoshihiro OKUMURA <yoshihiro.okumura@riken.jp>"] authors = ["Yoshihiro OKUMURA <yoshihiro.okumura@riken.jp>"]
license = "MIT" license = "MIT"