Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
36cad6db52
|
@@ -8,7 +8,7 @@ The mdrs-client-python is python library and a command-line client for up- and d
|
||||
poetry install
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
## CLI Usage
|
||||
|
||||
### config create
|
||||
|
||||
@@ -205,3 +205,28 @@ Show the help message and exit
|
||||
```shell
|
||||
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)
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from mdrsclient.__version__ import __version__
|
||||
|
||||
__all__ = ["__version__"]
|
||||
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
__all__ = ["MdrsClient"]
|
||||
|
||||
@@ -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__"]
|
||||
|
||||
+7
-14
@@ -5,36 +5,29 @@ from pydantic.dataclasses import dataclass
|
||||
|
||||
from mdrsclient.api.base import BaseApi
|
||||
from mdrsclient.api.utils import token_check
|
||||
from mdrsclient.models.doi import Doi
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DoiFolderRef:
|
||||
"""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.
|
||||
"""
|
||||
|
||||
class DoiRetrieveFolderRef:
|
||||
id: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DoiResponse:
|
||||
"""Response from GET v3/doi/{id}/."""
|
||||
|
||||
# The internal DOI suffix ID returned as a string (e.g. "20260429-001").
|
||||
class DoiRetrieveResponse:
|
||||
id: str
|
||||
doi: str
|
||||
folder: DoiFolderRef
|
||||
folder: DoiRetrieveFolderRef
|
||||
|
||||
|
||||
class DoiApi(BaseApi):
|
||||
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}/)."""
|
||||
url = self.ENTRYPOINT + doi_id + "/"
|
||||
token_check(self.connection)
|
||||
response = self.connection.get(url)
|
||||
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
@@ -2,6 +2,7 @@ import dataclasses
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
from pydantic.dataclasses import dataclass
|
||||
@@ -16,7 +17,7 @@ from mdrsclient.utils import FileLock
|
||||
class CacheData:
|
||||
user: User | None = None
|
||||
token: Token | None = None
|
||||
laboratories: Laboratories = Laboratories()
|
||||
laboratories: Laboratories = dataclasses.field(default_factory=Laboratories)
|
||||
digest: str = ""
|
||||
|
||||
def clear(self) -> None:
|
||||
@@ -43,6 +44,67 @@ class CacheData:
|
||||
).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:
|
||||
__serial: int
|
||||
__cache_dir: str
|
||||
|
||||
@@ -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)
|
||||
@@ -196,7 +196,7 @@ class BaseCommand(ABC):
|
||||
folder_api = FoldersApi(connection)
|
||||
|
||||
# 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 password is None:
|
||||
|
||||
@@ -31,10 +31,8 @@ class ChaclCommand(BaseCommand):
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
r_path = r_path.rstrip("/")
|
||||
connection = cls._create_connection(remote)
|
||||
laboratory = cls._find_laboratory(connection, laboratory_name)
|
||||
folder = cls._find_folder(connection, laboratory, r_path)
|
||||
folder_api = FoldersApi(connection)
|
||||
folder_api.acl(folder.id, access_level, is_recursive, password)
|
||||
remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
client = MdrsClient.from_remote(remote)
|
||||
client.chacl(remote_path, access_level, is_recursive, password)
|
||||
|
||||
@@ -38,13 +38,17 @@ class ConfigCommand(BaseCommand):
|
||||
def func_create(cls, args: Namespace) -> None:
|
||||
remote = str(args.remote)
|
||||
url = str(args.url)
|
||||
cls.create(remote, url)
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
MdrsClient(None).config_create(remote, url)
|
||||
|
||||
@classmethod
|
||||
def func_update(cls, args: Namespace) -> None:
|
||||
remote = str(args.remote)
|
||||
url = str(args.url)
|
||||
cls.update(remote, url)
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
MdrsClient(None).config_update(remote, url)
|
||||
|
||||
@classmethod
|
||||
def func_list(cls, args: Namespace) -> None:
|
||||
@@ -53,7 +57,9 @@ class ConfigCommand(BaseCommand):
|
||||
@classmethod
|
||||
def func_delete(cls, args: Namespace) -> None:
|
||||
remote = str(args.remote)
|
||||
cls.delete(remote)
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
MdrsClient(None).config_delete(remote)
|
||||
|
||||
@classmethod
|
||||
def create(cls, remote: str, url: str) -> None:
|
||||
@@ -75,8 +81,10 @@ class ConfigCommand(BaseCommand):
|
||||
|
||||
@classmethod
|
||||
def list(cls) -> None:
|
||||
config = ConfigFile("")
|
||||
for remote, url in config.list():
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
client = MdrsClient(None)
|
||||
for remote, url in client.config_list():
|
||||
print(f"{remote}:\t{url}")
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -29,53 +29,8 @@ class CpCommand(BaseCommand):
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
d_remote, d_laboratory_name, d_path = cls._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)
|
||||
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))
|
||||
remote = src_path.split(":", 1)[0] if ":" in src_path else ""
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
client = MdrsClient.from_remote(remote)
|
||||
client.cp(src_path, dest_path, is_recursive)
|
||||
|
||||
@@ -67,12 +67,30 @@ class DownloadCommand(BaseCommand):
|
||||
is_skip_if_exists: bool,
|
||||
password: str | None,
|
||||
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:
|
||||
# Detect DOI path: "remote:10.xxxx/prefix.ID[/optional/sub/path]"
|
||||
path_component = remote_path.split(":", 1)[1] if ":" in remote_path else ""
|
||||
if cls._is_doi(path_component):
|
||||
remote, doi, subpath = cls._parse_doi_remote_host(remote_path)
|
||||
connection = cls._create_connection(remote)
|
||||
|
||||
l_dirname = os.path.realpath(local_path)
|
||||
if not os.path.isdir(l_dirname):
|
||||
raise IllegalArgumentException(f"Local directory `{local_path}` not found.")
|
||||
@@ -126,7 +144,7 @@ class DownloadCommand(BaseCommand):
|
||||
r_path = r_path.rstrip("/")
|
||||
r_dirname = os.path.dirname(r_path)
|
||||
r_basename = os.path.basename(r_path)
|
||||
connection = cls._create_connection(remote)
|
||||
|
||||
l_dirname = os.path.realpath(local_path)
|
||||
if not os.path.isdir(l_dirname):
|
||||
raise IllegalArgumentException(f"Local directory `{local_path}` not found.")
|
||||
|
||||
@@ -26,12 +26,8 @@ class FileMetadataCommand(BaseCommand):
|
||||
@classmethod
|
||||
def file_metadata(cls, remote_path: str, password: str | None) -> None:
|
||||
remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
|
||||
connection = cls._create_connection(remote)
|
||||
folder, laboratory, r_basename = cls._resolve_file(connection, remote_path, password)
|
||||
files = cls._find_files(connection, folder.id)
|
||||
file = find_file(files, r_basename)
|
||||
if file is None:
|
||||
raise IllegalArgumentException(f"File `{r_basename}` not found.")
|
||||
file_api = FilesApi(connection)
|
||||
metadata = file_api.metadata(file)
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
client = MdrsClient.from_remote(remote)
|
||||
metadata = client.file_metadata(remote_path, password)
|
||||
print(json.dumps(metadata, ensure_ascii=False))
|
||||
|
||||
@@ -19,11 +19,11 @@ class LabsCommand(BaseCommand):
|
||||
|
||||
@classmethod
|
||||
def labs(cls, remote: str) -> None:
|
||||
remote = cls._parse_remote_host(remote)
|
||||
connection = cls._create_connection(remote)
|
||||
laboratory_api = LaboratoriesApi(connection)
|
||||
laboratories = laboratory_api.list()
|
||||
connection.laboratories = laboratories
|
||||
remote_host = cls._parse_remote_host(remote)
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
client = MdrsClient.from_remote(remote_host)
|
||||
laboratories = client.get_laboratories()
|
||||
label = {"id": "ID", "name": "Name", "pi_name": "PI", "full_name": "Laboratory"}
|
||||
length: dict[str, int] = {}
|
||||
for key in label.keys():
|
||||
@@ -34,7 +34,6 @@ class LabsCommand(BaseCommand):
|
||||
length["pi_name"] = max(length["pi_name"], len(laboratory.pi_name))
|
||||
length["full_name"] = max(length["full_name"], len(laboratory.full_name))
|
||||
header = (
|
||||
# f"{label['id']:{length['id']}}\t{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']}}"
|
||||
)
|
||||
@@ -42,7 +41,6 @@ class LabsCommand(BaseCommand):
|
||||
print("-" * len(header.expandtabs()))
|
||||
for laboratory in laboratories:
|
||||
print(
|
||||
# f"{laboratory.id:{length['id']}}\t{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']}}"
|
||||
)
|
||||
|
||||
@@ -21,20 +21,15 @@ class LoginCommand(BaseCommand):
|
||||
@classmethod
|
||||
def func(cls, args: Namespace) -> None:
|
||||
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()
|
||||
cls.login(remote, username, password)
|
||||
|
||||
@classmethod
|
||||
def login(cls, remote: str, username: str, password: str) -> None:
|
||||
remote = cls._parse_remote_host(remote)
|
||||
config = ConfigFile(remote)
|
||||
if config.url is None:
|
||||
raise MissingConfigurationException(f"Remote host `{remote}` is not found.")
|
||||
connection = MDRSConnection(config.remote, config.url)
|
||||
user_api = UsersApi(connection)
|
||||
token = user_api.token(username, password)
|
||||
connection.token = token
|
||||
user = user_api.current()
|
||||
connection.user = user
|
||||
remote_host = cls._parse_remote_host(remote)
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
client = MdrsClient.from_remote(remote_host)
|
||||
client.login(username, password)
|
||||
print("Login Successful")
|
||||
|
||||
@@ -21,9 +21,8 @@ class LogoutCommand(BaseCommand):
|
||||
|
||||
@classmethod
|
||||
def logout(cls, remote: str) -> None:
|
||||
remote = cls._parse_remote_host(remote)
|
||||
config = ConfigFile(remote)
|
||||
if config.url is None:
|
||||
raise MissingConfigurationException(f"Remote host `{remote}` is not found.")
|
||||
connection = MDRSConnection(config.remote, config.url)
|
||||
connection.logout()
|
||||
remote_host = cls._parse_remote_host(remote)
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
client = MdrsClient.from_remote(remote_host)
|
||||
client.logout()
|
||||
|
||||
@@ -55,7 +55,18 @@ class LsCommand(BaseCommand):
|
||||
@classmethod
|
||||
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 ""
|
||||
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)
|
||||
laboratory_name = laboratory.name
|
||||
files = cls._find_files(connection, folder.id)
|
||||
|
||||
@@ -23,8 +23,8 @@ class MetadataCommand(BaseCommand):
|
||||
@classmethod
|
||||
def metadata(cls, remote_path: str, password: str | None) -> None:
|
||||
remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
|
||||
connection = cls._create_connection(remote)
|
||||
folder, laboratory = cls._resolve_folder(connection, remote_path, password)
|
||||
folder_api = FoldersApi(connection)
|
||||
metadata = folder_api.metadata(folder.id)
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
client = MdrsClient.from_remote(remote)
|
||||
metadata = client.metadata(remote_path, password)
|
||||
print(json.dumps(metadata, ensure_ascii=False))
|
||||
|
||||
@@ -23,15 +23,8 @@ class MkdirCommand(BaseCommand):
|
||||
|
||||
@classmethod
|
||||
def mkdir(cls, remote_path: str) -> None:
|
||||
remote, laboratory_name, r_path = cls._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)
|
||||
connection = cls._create_connection(remote)
|
||||
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)
|
||||
remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
client = MdrsClient.from_remote(remote)
|
||||
client.mkdir(remote_path)
|
||||
|
||||
@@ -25,51 +25,8 @@ class MvCommand(BaseCommand):
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
d_remote, d_laboratory_name, d_path = cls._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)
|
||||
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))
|
||||
remote = src_path.split(":", 1)[0] if ":" in src_path else ""
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
client = MdrsClient.from_remote(remote)
|
||||
client.mv(src_path, dest_path)
|
||||
|
||||
@@ -26,23 +26,8 @@ class RmCommand(BaseCommand):
|
||||
|
||||
@classmethod
|
||||
def rm(cls, remote_path: str, is_recursive: bool) -> None:
|
||||
remote, laboratory_name, r_path = cls._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)
|
||||
connection = cls._create_connection(remote)
|
||||
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)
|
||||
remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
client = MdrsClient.from_remote(remote)
|
||||
client.rm(remote_path, is_recursive)
|
||||
|
||||
@@ -49,11 +49,22 @@ class UploadCommand(BaseCommand):
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
l_path = os.path.abspath(local_path)
|
||||
if not os.path.exists(l_path):
|
||||
raise IllegalArgumentException(f"File or directory `{local_path}` not found.")
|
||||
connection = cls._create_connection(remote)
|
||||
|
||||
laboratory = cls._find_laboratory(connection, laboratory_name)
|
||||
folder = cls._find_folder(connection, laboratory, r_path)
|
||||
files = cls._find_files(connection, folder.id)
|
||||
|
||||
@@ -17,4 +17,8 @@ class VersionCommand(BaseCommand):
|
||||
|
||||
@classmethod
|
||||
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())
|
||||
|
||||
@@ -23,12 +23,15 @@ class WhoamiCommand(BaseCommand):
|
||||
|
||||
@classmethod
|
||||
def whoami(cls, remote: str) -> None:
|
||||
remote = cls._parse_remote_host(remote)
|
||||
config = ConfigFile(remote)
|
||||
if config.url is None:
|
||||
raise MissingConfigurationException(f"Remote host `{remote}` is not found.")
|
||||
connection = MDRSConnection(config.remote, config.url)
|
||||
if connection.token is not None and connection.token.is_expired:
|
||||
connection.logout()
|
||||
username = connection.user.username if connection.user is not None else cls.ANONYMOUS_USERNAME
|
||||
remote_host = cls._parse_remote_host(remote)
|
||||
from mdrsclient.client import MdrsClient
|
||||
|
||||
client = MdrsClient.from_remote(remote_host)
|
||||
if client.connection.token is not None and client.connection.token.is_expired:
|
||||
client.logout()
|
||||
try:
|
||||
user = client.whoami()
|
||||
username = user.username if user is not None else cls.ANONYMOUS_USERNAME
|
||||
except Exception:
|
||||
username = cls.ANONYMOUS_USERNAME
|
||||
print(username)
|
||||
|
||||
@@ -9,7 +9,7 @@ from requests_toolbelt.multipart.encoder import MultipartEncoder
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from mdrsclient.__version__ import __version__
|
||||
from mdrsclient.cache import CacheFile
|
||||
from mdrsclient.cache import CacheFile, CacheInterface
|
||||
from mdrsclient.exceptions import MissingConfigurationException
|
||||
from mdrsclient.models import Laboratories, Token, User
|
||||
|
||||
@@ -39,14 +39,14 @@ class MDRSConnection:
|
||||
url: str
|
||||
session: Session
|
||||
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__()
|
||||
self.url = url
|
||||
self.session = Session()
|
||||
self.lock = threading.Lock()
|
||||
self.__cache = CacheFile(remote)
|
||||
self.__cache = cache if cache is not None else CacheFile(remote)
|
||||
self.__prepare_headers()
|
||||
|
||||
def get(self, url: str, **kwargs: Unpack[_KwArgsMDRSConnectionGet]) -> Response:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from mdrsclient.models.doi import Doi
|
||||
from mdrsclient.models.error import DRFStandardizedErrors
|
||||
from mdrsclient.models.file import File
|
||||
from mdrsclient.models.folder import Folder, FolderAccessLevel, FolderSimple
|
||||
@@ -6,6 +7,7 @@ from mdrsclient.models.user import Token, User
|
||||
|
||||
__all__ = [
|
||||
"DRFStandardizedErrors",
|
||||
"Doi",
|
||||
"File",
|
||||
"Folder",
|
||||
"FolderAccessLevel",
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
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."
|
||||
authors = ["Yoshihiro OKUMURA <yoshihiro.okumura@riken.jp>"]
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user