10 Commits

29 changed files with 272 additions and 189 deletions

15
.cspell.json Normal file
View File

@ -0,0 +1,15 @@
{
"version": "0.2",
"language": "en,en-gb",
"ignoreWords": ["followlinks", "getframe", "pycache", "pydantic", "UNLCK"],
"words": [
"chacl",
"kikan",
"mdrs",
"mdrsclient",
"neurodata",
"Neuroinformatics",
"RIKEN"
],
"ignorePaths": [".env", "__pycache__"]
}

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
# editorconfig.org
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

19
.vscode/settings.json vendored
View File

@ -1,7 +1,7 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
"source.organizeImports": "explicit"
},
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
@ -15,9 +15,12 @@
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// Extensions - Black Formatter
"black-formatter.args": ["--line-length=120"],
// Extensions - Code Spell Checker
"cSpell.ignoreWords": ["getframe", "pydantic", "UNLCK"],
"cSpell.words": ["chacl", "mdrs", "mdrsclient", "neurodata", "Neuroinformatics", "RIKEN"],
// - see: .cspell.json
// Extensions - Flake8
"flake8.args": ["--max-line-length=120"],
// Extensions - isort
"isort.args": ["--profile=black"],
// Extensions - Prettier
@ -28,16 +31,6 @@
"prettier.trailingComma": "all",
// Extensions - Pylance
"python.analysis.typeCheckingMode": "strict",
"python.analysis.exclude": ["api/migrations/[0-9]*.py"],
// Extensions - Python:black
"python.formatting.blackArgs": ["--line-length=120"],
"python.formatting.provider": "black",
// Extensions - Python:Flake8
"python.linting.enabled": true,
"python.linting.flake8Enabled": true,
"python.linting.flake8Args": ["--max-line-length=120"],
"python.linting.ignorePatterns": ["**/site-packages/**/*.py", ".vscode/*.py"],
"python.linting.lintOnSave": true,
// Extensions - Python Docstring Generator configuration
"autoDocstring.docstringFormat": "google"
}

View File

@ -5,7 +5,7 @@ The mdrs-client-python is python library and a command-line client for up- and d
## Installing
```
pip install -e .
poetry install
```
## Example Usage

View File

@ -1 +1 @@
1.1.1
1.3.1

View File

@ -1,11 +1,11 @@
from mdrsclient.api.file import FileApi
from mdrsclient.api.folder import FolderApi
from mdrsclient.api.laboratory import LaboratoryApi
from mdrsclient.api.user import UserApi
from mdrsclient.api.files import FilesApi
from mdrsclient.api.folders import FoldersApi
from mdrsclient.api.laboratories import LaboratoriesApi
from mdrsclient.api.users import UsersApi
__all__ = [
"FileApi",
"FolderApi",
"LaboratoryApi",
"UserApi",
"FilesApi",
"FoldersApi",
"LaboratoriesApi",
"UsersApi",
]

View File

@ -1,3 +1,4 @@
import os
from typing import Any, Final
from pydantic import TypeAdapter
@ -10,12 +11,12 @@ from mdrsclient.models import File
@dataclass(frozen=True)
class FileCreateResponse:
class FilesApiCreateResponse:
id: str
class FileApi(BaseApi):
ENTRYPOINT: Final[str] = "v2/file/"
class FilesApi(BaseApi):
ENTRYPOINT: Final[str] = "v3/files/"
def retrieve(self, id: str) -> File:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
@ -31,10 +32,10 @@ class FileApi(BaseApi):
token_check(self.connection)
data: dict[str, str | int] = {"folder_id": folder_id}
try:
with open(path, mode="rb") as fp:
with open(os.path.realpath(path), mode="rb") as fp:
response = self.connection.post(url, data=data, files={"file": fp})
self._raise_response_error(response)
ret = TypeAdapter(FileCreateResponse).validate_python(response.json())
ret = TypeAdapter(FilesApiCreateResponse).validate_python(response.json())
except OSError:
raise UnexpectedException(f"Could not open `{path}` file.")
return ret.id
@ -46,7 +47,7 @@ class FileApi(BaseApi):
if path is not None:
# update file body
try:
with open(path, mode="rb") as fp:
with open(os.path.realpath(path), mode="rb") as fp:
response = self.connection.put(url, files={"file": fp})
except OSError:
raise UnexpectedException(f"Could not open `{path}` file.")
@ -93,7 +94,7 @@ class FileApi(BaseApi):
def download(self, file: File, path: str) -> bool:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = "v2/" + file.download_url
url = file.download_url
response = self.connection.get(url, stream=True)
self._raise_response_error(response)
try:

View File

@ -11,12 +11,12 @@ from mdrsclient.models import Folder, FolderSimple
@dataclass(frozen=True)
class FolderCreateResponse:
class FoldersApiCreateResponse:
id: str
class FolderApi(BaseApi):
ENTRYPOINT: Final[str] = "v2/folder/"
class FoldersApi(BaseApi):
ENTRYPOINT: Final[str] = "v3/folders/"
def list(self, laboratory_id: int, path: str) -> list[FolderSimple]:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
@ -46,7 +46,7 @@ class FolderApi(BaseApi):
token_check(self.connection)
response = self.connection.post(url, data=data)
self._raise_response_error(response)
ret = TypeAdapter(FolderCreateResponse).validate_python(response.json())
ret = TypeAdapter(FoldersApiCreateResponse).validate_python(response.json())
return ret.id
def update(self, folder: FolderSimple) -> bool:

View File

@ -7,8 +7,8 @@ from mdrsclient.api.utils import token_check
from mdrsclient.models import Laboratories, Laboratory
class LaboratoryApi(BaseApi):
ENTRYPOINT: Final[str] = "v2/laboratory/"
class LaboratoriesApi(BaseApi):
ENTRYPOINT: Final[str] = "v3/laboratories/"
def list(self) -> Laboratories:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)

View File

@ -1,46 +0,0 @@
from typing import Final
import requests
from pydantic import TypeAdapter
from pydantic.dataclasses import dataclass
from mdrsclient.api.base import BaseApi
from mdrsclient.exceptions import UnauthorizedException
from mdrsclient.models import Token, User
@dataclass(frozen=True)
class UserAuthResponse(Token):
is_reviewer: bool | None = None
laboratory: str | None = None
lab_id: int | None = None
class UserApi(BaseApi):
ENTRYPOINT: Final[str] = "v2/"
def auth(self, username: str, password: str) -> tuple[User, Token]:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + "auth/"
data: dict[str, str | int] = {"username": username, "password": password}
response = self.connection.post(url, data=data)
if response.status_code == requests.codes.unauthorized:
raise UnauthorizedException("Invalid username or password.")
self._raise_response_error(response)
obj = TypeAdapter(UserAuthResponse).validate_python(response.json())
token = Token(access=obj.access, refresh=obj.refresh)
laboratory_ids = [obj.lab_id] if obj.lab_id is not None else []
is_reviewer = obj.is_reviewer if obj.is_reviewer is not None else False
user = User(id=token.user_id, username=username, laboratory_ids=laboratory_ids, is_reviewer=is_reviewer)
return (user, token)
def refresh(self, token: Token) -> Token:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + "refresh/"
data: dict[str, str | int] = {"refresh": token.refresh}
response = self.connection.post(url, data=data)
if response.status_code == requests.codes.unauthorized:
raise UnauthorizedException("Token is invalid or expired.")
self._raise_response_error(response)
token = TypeAdapter(Token).validate_python(response.json())
return token

67
mdrsclient/api/users.py Normal file
View File

@ -0,0 +1,67 @@
from typing import Final
import requests
from pydantic import TypeAdapter
from pydantic.dataclasses import dataclass
from mdrsclient.api.base import BaseApi
from mdrsclient.exceptions import UnauthorizedException
from mdrsclient.models import Token, User
@dataclass(frozen=True)
class UsersCurrentResponseLaboratory:
id: int
name: str
role: int
@dataclass(frozen=True)
class UsersApiCurrentResponse:
id: int
username: str
full_name: str
email: str
laboratories: list[UsersCurrentResponseLaboratory]
is_staff: bool
is_active: bool
is_superuser: bool
is_reviewer: bool
last_login: str # ISO8601
date_joined: str # ISO8601
class UsersApi(BaseApi):
ENTRYPOINT: Final[str] = "v3/users/"
def current(self) -> User:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + "current/"
response = self.connection.get(url)
self._raise_response_error(response)
obj = TypeAdapter(UsersApiCurrentResponse).validate_python(response.json())
laboratory_ids = list(map(lambda x: x.id, obj.laboratories))
user = User(id=obj.id, username=obj.username, laboratory_ids=laboratory_ids, is_reviewer=obj.is_reviewer)
return user
def token(self, username: str, password: str) -> Token:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + "token/"
data: dict[str, str | int] = {"username": username, "password": password}
response = self.connection.post(url, data=data)
if response.status_code == requests.codes.unauthorized:
raise UnauthorizedException("Invalid username or password.")
self._raise_response_error(response)
token = TypeAdapter(Token).validate_python(response.json())
return token
def tokenRefresh(self, token: Token) -> Token:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + "token/refresh/"
data: dict[str, str | int] = {"refresh": token.refresh}
response = self.connection.post(url, data=data)
if response.status_code == requests.codes.unauthorized:
raise UnauthorizedException("Token is invalid or expired.")
self._raise_response_error(response)
token = TypeAdapter(Token).validate_python(response.json())
return token

View File

@ -1,4 +1,4 @@
from mdrsclient.api.user import UserApi
from mdrsclient.api.users import UsersApi
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import UnauthorizedException
@ -8,9 +8,9 @@ def token_check(connection: MDRSConnection) -> None:
connection.lock.acquire()
if connection.token is not None:
if connection.token.is_refresh_required:
user_api = UserApi(connection)
user_api = UsersApi(connection)
try:
connection.token = user_api.refresh(connection.token)
connection.token = user_api.tokenRefresh(connection.token)
except UnauthorizedException:
connection.logout()
elif connection.token.is_expired:

View File

@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
from typing import Any
from unicodedata import normalize
from mdrsclient.api import FolderApi, LaboratoryApi
from mdrsclient.api import FoldersApi, LaboratoriesApi
from mdrsclient.config import ConfigFile
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import (
@ -31,7 +31,7 @@ class BaseCommand(ABC):
@classmethod
def _find_laboratory(cls, connection: MDRSConnection, name: str) -> Laboratory:
if connection.laboratories.empty() or connection.token is not None and connection.token.is_expired:
laboratory_api = LaboratoryApi(connection)
laboratory_api = LaboratoriesApi(connection)
connection.laboratories = laboratory_api.list()
laboratory = connection.laboratories.find_by_name(name)
if laboratory is None:
@ -42,7 +42,7 @@ class BaseCommand(ABC):
def _find_folder(
cls, connection: MDRSConnection, laboratory: Laboratory, path: str, password: str | None = None
) -> Folder:
folder_api = FolderApi(connection)
folder_api = FoldersApi(connection)
folders = folder_api.list(laboratory.id, normalize("NFC", path))
if len(folders) != 1:
raise UnexpectedException(f"Folder `{path}` not found.")

View File

@ -1,7 +1,7 @@
from argparse import Namespace
from typing import Any
from mdrsclient.api import FolderApi
from mdrsclient.api import FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.models import FolderAccessLevel
@ -36,5 +36,5 @@ class ChaclCommand(BaseCommand):
connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name)
folder = cls._find_folder(connection, laboratory, r_path)
folder_api = FolderApi(connection)
folder_api = FoldersApi(connection)
folder_api.acl(folder.id, access_level, is_recursive, password)

View File

@ -3,7 +3,7 @@ from argparse import Namespace
from typing import Any
from unicodedata import normalize
from mdrsclient.api import FileApi, FolderApi
from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException
@ -56,7 +56,7 @@ class CpCommand(BaseCommand):
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 = FileApi(connection)
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:
@ -73,6 +73,6 @@ class CpCommand(BaseCommand):
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 = FolderApi(connection)
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))

View File

@ -5,7 +5,7 @@ from typing import Any
from pydantic.dataclasses import dataclass
from mdrsclient.api import FileApi, FolderApi
from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import IllegalArgumentException
@ -62,13 +62,13 @@ class DownloadCommand(BaseCommand):
raise IllegalArgumentException(f"File or folder `{r_path}` not found.")
if not is_recursive:
raise IllegalArgumentException(f"Cannot download `{r_path}`: Is a folder.")
folder_api = FolderApi(connection)
folder_api = FoldersApi(connection)
cls.__multiple_download_pickup_recursive_files(folder_api, download_files, folder.id, l_dirname)
cls.__multiple_download(connection, download_files)
@classmethod
def __multiple_download_pickup_recursive_files(
cls, folder_api: FolderApi, infolist: list[DownloadFileInfo], folder_id: str, basedir: str
cls, folder_api: FoldersApi, infolist: list[DownloadFileInfo], folder_id: str, basedir: str
) -> None:
folder = folder_api.retrieve(folder_id)
dirname = os.path.join(basedir, folder.name)
@ -83,11 +83,11 @@ class DownloadCommand(BaseCommand):
@classmethod
def __multiple_download(cls, connection: MDRSConnection, infolist: list[DownloadFileInfo]) -> None:
file_api = FileApi(connection)
file_api = FilesApi(connection)
with ThreadPoolExecutor(max_workers=CONCURRENT) as pool:
pool.map(lambda x: cls.__multiple_download_worker(file_api, x), infolist)
@classmethod
def __multiple_download_worker(cls, file_api: FileApi, info: DownloadFileInfo) -> None:
def __multiple_download_worker(cls, file_api: FilesApi, info: DownloadFileInfo) -> None:
file_api.download(info.file, info.path)
print(info.path)

View File

@ -3,7 +3,7 @@ import os
from argparse import Namespace
from typing import Any
from mdrsclient.api import FileApi
from mdrsclient.api import FilesApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException
@ -34,6 +34,6 @@ class FileMetadataCommand(BaseCommand):
file = folder.find_file(r_basename)
if file is None:
raise IllegalArgumentException(f"File `{r_basename}` not found.")
file_api = FileApi(connection)
file_api = FilesApi(connection)
metadata = file_api.metadata(file)
print(json.dumps(metadata, ensure_ascii=False))

View File

@ -1,7 +1,7 @@
from argparse import Namespace
from typing import Any
from mdrsclient.api import LaboratoryApi
from mdrsclient.api import LaboratoriesApi
from mdrsclient.commands.base import BaseCommand
@ -21,7 +21,7 @@ class LabsCommand(BaseCommand):
def labs(cls, remote: str) -> None:
remote = cls._parse_remote_host(remote)
connection = cls._create_connection(remote)
laboratory_api = LaboratoryApi(connection)
laboratory_api = LaboratoriesApi(connection)
laboratories = laboratory_api.list()
connection.laboratories = laboratories
label = {"id": "ID", "name": "Name", "pi_name": "PI", "full_name": "Laboratory"}

View File

@ -2,7 +2,7 @@ import getpass
from argparse import Namespace
from typing import Any
from mdrsclient.api import UserApi
from mdrsclient.api import UsersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.config import ConfigFile
from mdrsclient.connection import MDRSConnection
@ -30,8 +30,9 @@ class LoginCommand(BaseCommand):
if config.url is None:
raise MissingConfigurationException(f"Remote host `{remote}` is not found.")
connection = MDRSConnection(config.remote, config.url)
user_api = UserApi(connection)
(user, token) = user_api.auth(username, password)
print("Login Successful")
connection.user = user
user_api = UsersApi(connection)
token = user_api.token(username, password)
connection.token = token
user = user_api.current()
connection.user = user
print("Login Successful")

View File

@ -4,7 +4,7 @@ from typing import Any
from pydantic.dataclasses import dataclass
from mdrsclient.api import FolderApi
from mdrsclient.api import FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import UnauthorizedException
@ -82,7 +82,7 @@ class LsCommand(BaseCommand):
"type": "Type",
"acl": "Access",
"laboratory": "Laboratory",
"size": "Lock/Size",
"size": "Size",
"date": "Date",
"name": "Name",
}
@ -90,11 +90,11 @@ class LsCommand(BaseCommand):
for key in label.keys():
length[key] = len(label[key]) if not context.is_quick else 0
for sub_folder in folder.sub_folders:
sub_laboratory = context.connection.laboratories.find_by_id(sub_folder.lab_id)
sub_laboratory = context.connection.laboratories.find_by_id(sub_folder.laboratory_id)
sub_laboratory_name = sub_laboratory.name if sub_laboratory is not None else "(invalid)"
length["acl"] = max(length["acl"], len(sub_folder.access_level_name))
length["laboratory"] = max(length["laboratory"], len(sub_laboratory_name))
length["size"] = max(length["size"], len(sub_folder.lock_name))
length["size"] = max(length["size"], len(str(folder.size)))
length["date"] = max(length["date"], len(sub_folder.updated_at_name))
length["name"] = max(length["name"], len(sub_folder.name))
for file in folder.files:
@ -118,10 +118,11 @@ class LsCommand(BaseCommand):
print("-" * len(header.expandtabs()))
for sub_folder in sorted(folder.sub_folders, key=lambda x: x.name):
sub_laboratory_name = cls._laboratory_name(context, sub_folder.lab_id)
sub_laboratory_name = cls._laboratory_name(context, sub_folder.laboratory_id)
sub_folder_type = "[d]" if sub_folder.lock is False else "[l]"
print(
f"{'[d]':{length['type']}}\t{sub_folder.access_level_name:{length['acl']}}\t"
f"{sub_laboratory_name:{length['laboratory']}}\t{sub_folder.lock_name:{length['size']}}\t"
f"{sub_folder_type:{length['type']}}\t{sub_folder.access_level_name:{length['acl']}}\t"
f"{sub_laboratory_name:{length['laboratory']}}\t{sub_folder.size:{length['size']}}\t"
f"{sub_folder.updated_at_name:{length['date']}}\t{sub_folder.name:{length['name']}}"
)
for file in sorted(folder.files, key=lambda x: x.name):
@ -134,7 +135,7 @@ class LsCommand(BaseCommand):
if context.is_recursive:
print("")
for sub_folder in sorted(folder.sub_folders, key=lambda x: x.name):
folder_api = FolderApi(context.connection)
folder_api = FoldersApi(context.connection)
try:
if sub_folder.lock:
folder_api.auth(sub_folder.id, context.password)
@ -149,15 +150,16 @@ class LsCommand(BaseCommand):
"id": folder.id,
"pid": folder.pid,
"name": folder.name,
"size": folder.size,
"access_level": folder.access_level_name,
"lock": folder.lock,
"laboratory": cls._laboratory_name(context, folder.lab_id),
"laboratory": cls._laboratory_name(context, folder.laboratory_id),
"description": folder.description,
"created_at": folder.created_at,
"updated_at": folder.updated_at,
}
if isinstance(folder, Folder):
folder_api = FolderApi(context.connection)
folder_api = FoldersApi(context.connection)
data["metadata"] = folder_api.metadata(folder.id)
if context.is_recursive:
sub_folders: list[dict[str, Any]] = []
@ -187,7 +189,7 @@ class LsCommand(BaseCommand):
# "thumbnail": file.thumbnail,
"description": file.description,
"metadata": file.metadata,
"download_url": f"{context.connection.url}/v2/{file.download_url}",
"download_url": f"{context.connection.url}/{file.download_url}",
"created_at": file.created_at,
"updated_at": file.updated_at,
}

View File

@ -2,7 +2,7 @@ import json
from argparse import Namespace
from typing import Any
from mdrsclient.api import FolderApi
from mdrsclient.api import FoldersApi
from mdrsclient.commands.base import BaseCommand
@ -26,6 +26,6 @@ class MetadataCommand(BaseCommand):
connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name)
folder = cls._find_folder(connection, laboratory, r_path, password)
folder_api = FolderApi(connection)
folder_api = FoldersApi(connection)
metadata = folder_api.metadata(folder.id)
print(json.dumps(metadata, ensure_ascii=False))

View File

@ -3,7 +3,7 @@ from argparse import Namespace
from typing import Any
from unicodedata import normalize
from mdrsclient.api import FolderApi
from mdrsclient.api import FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException
@ -31,5 +31,5 @@ class MkdirCommand(BaseCommand):
parent_folder = cls._find_folder(connection, laboratory, r_dirname)
if parent_folder.find_sub_folder(r_basename) is not None or parent_folder.find_file(r_basename) is not None:
raise IllegalArgumentException(f"Cannot create folder `{r_path}`: File exists.")
folder_api = FolderApi(connection)
folder_api = FoldersApi(connection)
folder_api.create(normalize("NFC", r_basename), parent_folder.id)

View File

@ -3,7 +3,7 @@ from argparse import Namespace
from typing import Any
from unicodedata import normalize
from mdrsclient.api import FileApi, FolderApi
from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException
@ -52,7 +52,7 @@ class MvCommand(BaseCommand):
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 = FileApi(connection)
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:
@ -67,6 +67,6 @@ class MvCommand(BaseCommand):
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 = FolderApi(connection)
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))

View File

@ -2,7 +2,7 @@ import os
from argparse import Namespace
from typing import Any
from mdrsclient.api import FileApi, FolderApi
from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException
@ -34,7 +34,7 @@ class RmCommand(BaseCommand):
parent_folder = cls._find_folder(connection, laboratory, r_dirname)
file = parent_folder.find_file(r_basename)
if file is not None:
file_api = FileApi(connection)
file_api = FilesApi(connection)
file_api.destroy(file)
else:
folder = parent_folder.find_sub_folder(r_basename)
@ -42,5 +42,5 @@ class RmCommand(BaseCommand):
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 = FolderApi(connection)
folder_api = FoldersApi(connection)
folder_api.destroy(folder.id, True)

View File

@ -5,7 +5,7 @@ from typing import Any
from pydantic.dataclasses import dataclass
from mdrsclient.api import FileApi, FolderApi
from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import IllegalArgumentException, MDRSException
@ -40,7 +40,7 @@ class UploadCommand(BaseCommand):
@classmethod
def upload(cls, local_path: str, remote_path: str, is_recursive: bool) -> None:
(remote, laboratory_name, r_path) = cls._parse_remote_host_with_path(remote_path)
l_path = os.path.realpath(local_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)
@ -50,11 +50,11 @@ class UploadCommand(BaseCommand):
if os.path.isdir(l_path):
if not is_recursive:
raise IllegalArgumentException(f"Cannot upload `{local_path}`: Is a directory.")
folder_api = FolderApi(connection)
folder_api = FoldersApi(connection)
folder_map: dict[str, Folder] = {}
folder_map[r_path] = folder
l_basename = os.path.basename(l_path)
for dirpath, _, filenames in os.walk(l_path):
for dirpath, _, filenames in os.walk(l_path, followlinks=True):
sub = l_basename if dirpath == l_path else os.path.join(l_basename, os.path.relpath(dirpath, l_path))
d_dirname = os.path.join(r_path, sub)
d_basename = os.path.basename(d_dirname)
@ -82,12 +82,12 @@ class UploadCommand(BaseCommand):
@classmethod
def __multiple_upload(cls, connection: MDRSConnection, infos: list[UploadFileInfo]) -> None:
file_api = FileApi(connection)
file_api = FilesApi(connection)
with ThreadPoolExecutor(max_workers=CONCURRENT) as pool:
pool.map(lambda x: cls.__multiple_upload_worker(file_api, x), infos)
@classmethod
def __multiple_upload_worker(cls, file_api: FileApi, info: UploadFileInfo) -> None:
def __multiple_upload_worker(cls, file_api: FilesApi, info: UploadFileInfo) -> None:
basename = os.path.basename(info.path)
file = info.folder.find_file(basename)
try:

View File

@ -8,28 +8,37 @@ from mdrsclient.models.utils import iso8601_to_user_friendly
class FolderAccessLevelItem(NamedTuple):
id: int
mask: int
key: str
label: str
class FolderAccessLevel:
# Bit Mask
# - bit 0: Is Private
# - bit 1: Is Public
# - bit 2: With Password
# - bit 3-7: (Reserved)
# - bit 8-15: Restricted Open
ACCESS_LEVELS: Final[list[FolderAccessLevelItem]] = [
FolderAccessLevelItem(-1, "storage", "Storage"),
FolderAccessLevelItem(0, "private", "Private"),
FolderAccessLevelItem(1, "cbs_open", "CBS Open"),
FolderAccessLevelItem(2, "pw_open", "PW Open"),
FolderAccessLevelItem(3, "public", "Public"),
FolderAccessLevelItem(0x0204, "5kikan_or_pw_open", "5Kikan or PW Open"),
FolderAccessLevelItem(0x0104, "cbs_or_pw_open", "CBS or PW Open"),
FolderAccessLevelItem(0x0200, "5kikan_open", "5Kikan Open"),
FolderAccessLevelItem(0x0100, "cbs_open", "CBS Open"),
FolderAccessLevelItem(0x0004, "pw_open", "PW Open"),
FolderAccessLevelItem(0x0002, "public", "Public"),
FolderAccessLevelItem(0x0001, "private", "Private"),
FolderAccessLevelItem(0x0000, "storage", "Storage"),
]
@staticmethod
def key2id(key: str) -> int | None:
acl = next((x for x in FolderAccessLevel.ACCESS_LEVELS if x.key == key), None)
return acl.id if acl is not None else None
return acl.mask if acl is not None else None
@staticmethod
def id2label(id: int) -> str | None:
acl = next((x for x in FolderAccessLevel.ACCESS_LEVELS if x.id == id), None)
acl = next((x for x in FolderAccessLevel.ACCESS_LEVELS if (x.mask & id) == x.mask), None)
return acl.label if acl is not None else None
@ -40,7 +49,8 @@ class FolderSimple:
name: str
access_level: int
lock: bool
lab_id: int
size: int
laboratory_id: int
description: str
created_at: str
updated_at: str

74
pyproject.toml Normal file
View File

@ -0,0 +1,74 @@
[tool.poetry]
name = "mdrs-client-python"
version = "1.3.1"
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"
readme = "README.md"
classifiers=[
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"OSI Approved :: MIT License",
"Topic :: Utilities",
]
packages = [
{ include = "mdrsclient" }
]
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.31.0"
python-dotenv = "^1.0.0"
pydantic = "^2.5.2"
pydantic-settings = "^2.1.0"
PyJWT = "^2.8.0"
validators = "^0.22.0"
[tool.poetry.group.dev.dependencies]
black = "^23.12.0"
flake8 = "^6.1.0"
Flake8-pyproject = "^1.2.3"
isort = "^5.13.0"
pyright = "^1.1.339"
[tool.poetry.scripts]
mdrs = 'mdrsclient.__main__:main'
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 120
target-version = ['py310']
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.venv
| __pycache__
| dist
)/
'''
[tool.flake8]
exclude = ".git, .venv, __pycache__, dist"
max-complexity = 10
max-line-length = 120
[tool.isort]
profile = "black"
line_length = 120
[tool.mypy]
[tool.pyright]
typeCheckingMode = "basic"
exclude = ["**/__pycache__", "**/.*", "dist"]
#reportUnknownMemberType = "warning"
#reportUnknownVariableType = "warning"

View File

@ -1,6 +0,0 @@
requests
python-dotenv
pydantic
pydantic-settings
PyJWT
validators

View File

@ -1,40 +0,0 @@
import os
from typing import Final
from setuptools import find_packages, setup
from mdrsclient import __version__
BASE_DIR: Final[str] = os.path.realpath(os.path.dirname(__file__))
with open(os.path.join(BASE_DIR, "requirements.txt")) as f:
__requirements__ = f.read().splitlines()
with open(os.path.join(BASE_DIR, "README.md")) as f:
__readme__ = f.read()
setup(
name="mdrsclient",
version=__version__,
description="A MDRS command-line tool",
long_description=__readme__,
author="Neuroinformatics Unit, RIKEN CBS",
license="MIT",
classifiers=[
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"OSI Approved :: MIT License",
"Topic :: Utilities",
],
packages=find_packages(),
include_package_data=True,
package_data={
"mdrsclient": ["VERSION"],
},
install_requires=__requirements__,
entry_points={"console_scripts": ["mdrs=mdrsclient.__main__:main"]},
)