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
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
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:
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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']}}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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()
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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))
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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]
|
[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"
|
||||||
|
|||||||
Reference in New Issue
Block a user