5 Commits

Author SHA1 Message Date
orrisroot ddb4300d85 feat(config): simplify list command and add subcommand aliases
- config list: remove -l/--long option, always display URL
- config list: add ls alias (already existed, kept)
- config delete: add rm alias (alongside existing remove alias)
- README: add config update, config list, config delete sections

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 18:59:07 +09:00
orrisroot 68670a6588 fix(ls): rename --quick to --quiet; add version command; bump to 1.3.14
- Fix ls -q long option name: --quick → --quiet (typo fix)
- Remove mdrsclient/VERSION file; read version via importlib.metadata
- Bump version 1.3.13 → 1.3.14
- Add Python 3.14 to supported classifiers; promote to Development Status 4 - Beta
- Add `version` subcommand (prints "mdrs <version>")
- Document `version` command in README

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 17:41:37 +09:00
orrisroot 6d8fd0a598 update version to 1.3.13 2025-10-28 11:10:09 +09:00
pw-serizawa 634b5f1a13 update version to 1.3.12 2025-07-02 18:34:11 +09:00
pw-serizawa 95f22ea5f9 fix for file.list api pagination 2025-06-26 17:13:06 +09:00
21 changed files with 182 additions and 63 deletions
+34
View File
@@ -18,6 +18,32 @@ Create remote host configuration
mdrs config create neurodata https://neurodata.riken.jp/api
```
### config update
Update the URL of a registered remote host.
```shell
mdrs config update neurodata https://neurodata.riken.jp/api
```
### config list
List registered remote hosts.
```shell
mdrs config list
mdrs config ls
```
### config delete
Remove a registered remote host.
```shell
mdrs config delete neurodata
mdrs config rm neurodata
```
### login
Login to remote host
@@ -150,6 +176,14 @@ mdrs file-metadata neurodata:/NIU/Repository/TEST/dataset/sample.dat
mdrs file-metadata -p SHARING_PASSWORD neurodata:/NIU/Repository/PW_Open/Readme.txt
```
### version
Show the tool name and version number
```shell
mdrs version
```
### help
Show the help message and exit
-1
View File
@@ -1 +0,0 @@
1.3.12
+2
View File
@@ -17,6 +17,7 @@ from mdrsclient.commands import (
MvCommand,
RmCommand,
UploadCommand,
VersionCommand,
WhoamiCommand,
)
from mdrsclient.exceptions import MDRSException
@@ -29,6 +30,7 @@ def main() -> None:
parsers = parser.add_subparsers(title="subcommands")
ConfigCommand.register(parsers)
VersionCommand.register(parsers)
LoginCommand.register(parsers)
LogoutCommand.register(parsers)
WhoamiCommand.register(parsers)
+2 -5
View File
@@ -1,8 +1,5 @@
import os
from importlib.metadata import version
here = os.path.realpath(os.path.dirname(__file__))
with open(os.path.join(here, "VERSION")) as version_file:
__version__ = version_file.read().strip()
__version__ = version("mdrs-client-python")
__all__ = ["__version__"]
+16
View File
@@ -17,10 +17,26 @@ class FilesApiCreateResponse:
id: str
@dataclass(frozen=True)
class FilesApiListResponse:
count: int
next: str | None
previous: str | None
results: list[File]
class FilesApi(BaseApi):
ENTRYPOINT: Final[str] = "v3/files/"
FALLBACK_MIMETYPE: Final[str] = "application/octet-stream"
def list(self, folder_id: str, page_num: int) -> FilesApiListResponse:
url = self.ENTRYPOINT
token_check(self.connection)
params: dict[str, str | int] = {"folder_id": folder_id, "page": page_num}
response = self.connection.get(url, params=params)
self._raise_response_error(response)
return TypeAdapter(FilesApiListResponse).validate_python(response.json())
def retrieve(self, id: str) -> File:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + id + "/"
+2
View File
@@ -12,6 +12,7 @@ from mdrsclient.commands.mkdir import MkdirCommand
from mdrsclient.commands.mv import MvCommand
from mdrsclient.commands.rm import RmCommand
from mdrsclient.commands.upload import UploadCommand
from mdrsclient.commands.version import VersionCommand
from mdrsclient.commands.whoami import WhoamiCommand
__all__ = [
@@ -29,5 +30,6 @@ __all__ = [
"MvCommand",
"RmCommand",
"UploadCommand",
"VersionCommand",
"WhoamiCommand",
]
+16 -2
View File
@@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
from typing import Any
from unicodedata import normalize
from mdrsclient.api import FoldersApi, LaboratoriesApi
from mdrsclient.api import FilesApi, FoldersApi, LaboratoriesApi
from mdrsclient.config import ConfigFile
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import (
@@ -12,7 +12,8 @@ from mdrsclient.exceptions import (
UnauthorizedException,
UnexpectedException,
)
from mdrsclient.models import Folder, Laboratory
from mdrsclient.models import File, Folder, Laboratory
from mdrsclient.utils import page_num_from_url
class BaseCommand(ABC):
@@ -52,6 +53,19 @@ class BaseCommand(ABC):
folder_api.auth(folders[0].id, password)
return folder_api.retrieve(folders[0].id)
@classmethod
def _find_files(cls, connection: MDRSConnection, folder_id: str) -> list[File]:
files_api = FilesApi(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
@classmethod
def _parse_remote_host(cls, path: str) -> str:
path_array = path.split(":")
+4 -9
View File
@@ -26,10 +26,9 @@ class ConfigCommand(BaseCommand):
update_parser.set_defaults(func=cls.func_update)
# config list
list_parser = config_parsers.add_parser("list", help="list all the remote hosts", aliases=["ls"])
list_parser.add_argument("-l", "--long", help="show the api url", action="store_true")
list_parser.set_defaults(func=cls.func_list)
# config delete
delete_parser = config_parsers.add_parser("delete", help="delete an existing remote host", aliases=["remove"])
delete_parser = config_parsers.add_parser("delete", help="delete an existing remote host", aliases=["remove", "rm"])
delete_parser.add_argument("remote", help="label of remote host")
delete_parser.set_defaults(func=cls.func_delete)
@@ -47,8 +46,7 @@ class ConfigCommand(BaseCommand):
@classmethod
def func_list(cls, args: Namespace) -> None:
is_long = bool(args.long)
cls.list(is_long)
cls.list()
@classmethod
def func_delete(cls, args: Namespace) -> None:
@@ -74,13 +72,10 @@ class ConfigCommand(BaseCommand):
config.url = url
@classmethod
def list(cls, is_long: bool) -> None:
def list(cls) -> None:
config = ConfigFile("")
for remote, url in config.list():
line = f"{remote}:"
if is_long:
line += f"\t{url}"
print(line)
print(f"{remote}:\t{url}")
@classmethod
def delete(cls, remote: str) -> None:
+6 -3
View File
@@ -6,6 +6,7 @@ from unicodedata import normalize
from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.models.file import find_file
class CpCommand(BaseCommand):
@@ -46,11 +47,13 @@ class CpCommand(BaseCommand):
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)
s_file = s_parent_folder.find_file(s_basename)
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 = d_parent_folder.find_file(d_basename)
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)
@@ -66,7 +69,7 @@ class CpCommand(BaseCommand):
# source is folder
if not is_recursive:
raise IllegalArgumentException(f"Cannot copy `{s_path}`: Is a folder.")
if d_parent_folder.find_file(d_basename) is not None:
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:
+5 -2
View File
@@ -10,6 +10,7 @@ from mdrsclient.commands.base import BaseCommand
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import IllegalArgumentException, UnexpectedException
from mdrsclient.models import File, Folder, Laboratory
from mdrsclient.models.file import find_file
from mdrsclient.settings import CONCURRENT
@@ -77,7 +78,8 @@ class DownloadCommand(BaseCommand):
raise IllegalArgumentException(f"Local directory `{local_path}` not found.")
laboratory = cls._find_laboratory(connection, laboratory_name)
r_parent_folder = cls._find_folder(connection, laboratory, r_dirname, password)
file = r_parent_folder.find_file(r_basename)
r_parent_files = cls._find_files(connection, r_parent_folder.id)
file = find_file(r_parent_files, r_basename)
if file is not None:
if cls.__check_excludes(excludes, laboratory, r_parent_folder, file):
return
@@ -109,13 +111,14 @@ class DownloadCommand(BaseCommand):
) -> None:
context = DownloadContext(False, is_skip_if_exists, [])
folder = folder_api.retrieve(folder_id)
files = cls._find_files(connection, folder.id)
dirname = os.path.join(basedir, folder.name)
if cls.__check_excludes(excludes, laboratory, folder, None):
return
if not os.path.exists(dirname):
os.makedirs(dirname)
print(dirname)
for file in folder.files:
for file in files:
if cls.__check_excludes(excludes, laboratory, folder, file):
continue
path = os.path.join(dirname, file.name)
+3 -1
View File
@@ -6,6 +6,7 @@ from typing import Any
from mdrsclient.api import FilesApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.models.file import find_file
class FileMetadataCommand(BaseCommand):
@@ -31,7 +32,8 @@ class FileMetadataCommand(BaseCommand):
connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name)
folder = cls._find_folder(connection, laboratory, r_dirname, password)
file = folder.find_file(r_basename)
files = cls._find_files(connection, folder.id)
file = find_file(files, r_basename)
if file is None:
raise IllegalArgumentException(f"File `{r_basename}` not found.")
file_api = FilesApi(connection)
+27 -22
View File
@@ -4,7 +4,7 @@ from typing import Any
from pydantic.dataclasses import dataclass
from mdrsclient.api import FoldersApi
from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import UnauthorizedException
@@ -23,7 +23,7 @@ class LsCommandContext:
laboratory: Laboratory
password: str
is_json: bool
is_quick: bool
is_quiet: bool
is_recursive: bool
@@ -35,7 +35,7 @@ class LsCommand(BaseCommand):
ls_parser.add_argument("-J", "--json", help="turn on json output", action="store_true")
ls_parser.add_argument(
"-q",
"--quick",
"--quiet",
help="don't output header row. this option is forced if the -r option is specified",
action="store_true",
)
@@ -49,11 +49,11 @@ class LsCommand(BaseCommand):
password = str(args.password) if args.password else None
is_json = bool(args.json)
is_recursive = bool(args.recursive)
is_quick = bool(args.quick) if not is_recursive else True
cls.ls(remote_path, password, is_json, is_recursive, is_quick)
is_quiet = bool(args.quiet) if not is_recursive else True
cls.ls(remote_path, password, is_json, is_recursive, is_quiet)
@classmethod
def ls(cls, remote_path: str, password: str | None, is_json: bool, is_recursive: bool, is_quick: bool) -> None:
def ls(cls, remote_path: str, password: str | None, is_json: bool, is_recursive: bool, is_quiet: bool) -> None:
(remote, laboratory_name, r_path) = cls._parse_remote_host_with_path(remote_path)
connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name)
@@ -63,21 +63,22 @@ class LsCommand(BaseCommand):
laboratory,
password if password is not None else "",
is_json,
is_quick,
is_quiet,
is_recursive,
)
folder = cls._find_folder(connection, laboratory, r_path, password)
files = cls._find_files(connection, folder.id)
if context.is_json:
cls._ls_json(context, folder)
cls._ls_json(context, folder, files)
else:
cls._ls_plain(context, folder)
cls._ls_plain(context, folder, files)
@classmethod
def _ls_json(cls, context: LsCommandContext, folder: Folder) -> None:
print(json.dumps(cls._folder2dict(context, folder), ensure_ascii=False))
def _ls_json(cls, context: LsCommandContext, folder: Folder, files: list[File]) -> None:
print(json.dumps(cls._folder2dict(context, folder, files), ensure_ascii=False))
@classmethod
def _ls_plain(cls, context: LsCommandContext, folder: Folder) -> None:
def _ls_plain(cls, context: LsCommandContext, folder: Folder, files: list[File]) -> None:
label = {
"type": "Type",
"acl": "Access",
@@ -88,7 +89,7 @@ class LsCommand(BaseCommand):
}
length: dict[str, int] = {}
for key in label.keys():
length[key] = len(label[key]) if not context.is_quick else 0
length[key] = len(label[key]) if not context.is_quiet else 0
for sub_folder in folder.sub_folders:
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)"
@@ -97,7 +98,7 @@ class LsCommand(BaseCommand):
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:
for file in files:
length["size"] = max(length["size"], len(str(file.size)))
length["date"] = max(length["date"], len(file.updated_at_name))
length["name"] = max(length["name"], len(file.name))
@@ -111,9 +112,9 @@ class LsCommand(BaseCommand):
if context.is_recursive:
print(f"{context.prefix}{folder.path}:")
print(f"total {sum(f.size for f in folder.files)}")
print(f"total {sum(f.size for f in files)}")
if not context.is_quick:
if not context.is_quiet:
print(header)
print("-" * len(header.expandtabs()))
@@ -125,7 +126,7 @@ class LsCommand(BaseCommand):
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):
for file in sorted(files, key=lambda x: x.name):
print(
f"{'[f]':{length['type']}}\t{folder.access_level_name:{length['acl']}}\t"
f"{context.laboratory.name:{length['laboratory']}}\t{file.size:{length['size']}}\t"
@@ -140,12 +141,15 @@ class LsCommand(BaseCommand):
if sub_folder.lock:
folder_api.auth(sub_folder.id, context.password)
folder = folder_api.retrieve(sub_folder.id)
cls._ls_plain(context, folder)
files = cls._find_files(context.connection, sub_folder.id)
cls._ls_plain(context, folder, files)
except UnauthorizedException:
pass
@classmethod
def _folder2dict(cls, context: LsCommandContext, folder: Folder | FolderSimple) -> dict[str, Any]:
def _folder2dict(
cls, context: LsCommandContext, folder: Folder | FolderSimple, files: list[File]
) -> dict[str, Any]:
data: dict[str, Any] = {
"id": folder.id,
"pid": folder.pid,
@@ -168,15 +172,16 @@ class LsCommand(BaseCommand):
if sub_folder.lock:
folder_api.auth(sub_folder.id, context.password)
folder2 = folder_api.retrieve(sub_folder.id)
sub_folders.append(cls._folder2dict(context, folder2))
files2 = cls._find_files(context.connection, sub_folder.id)
sub_folders.append(cls._folder2dict(context, folder2, files2))
except UnauthorizedException:
pass
data["sub_folders"] = sub_folders
else:
data["sub_folders"] = list(
map(lambda x: cls._folder2dict(context, x), sorted(folder.sub_folders, key=lambda x: x.name))
map(lambda x: cls._folder2dict(context, x, []), sorted(folder.sub_folders, key=lambda x: x.name))
)
data["files"] = list(map(lambda x: cls._file2dict(context, x), sorted(folder.files, key=lambda x: x.name)))
data["files"] = list(map(lambda x: cls._file2dict(context, x), sorted(files, key=lambda x: x.name)))
return data
@classmethod
+3 -1
View File
@@ -6,6 +6,7 @@ from unicodedata import normalize
from mdrsclient.api import FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.models.file import find_file
class MkdirCommand(BaseCommand):
@@ -29,7 +30,8 @@ class MkdirCommand(BaseCommand):
connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name)
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:
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)
+6 -3
View File
@@ -6,6 +6,7 @@ from unicodedata import normalize
from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.models.file import find_file
class MvCommand(BaseCommand):
@@ -42,11 +43,13 @@ class MvCommand(BaseCommand):
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)
s_file = s_parent_folder.find_file(s_basename)
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 = d_parent_folder.find_file(d_basename)
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)
@@ -60,7 +63,7 @@ class MvCommand(BaseCommand):
if s_folder is None:
raise IllegalArgumentException(f"File or folder `{s_basename}` not found.")
# source is folder
if d_parent_folder.find_file(d_basename) is not None:
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:
+3 -1
View File
@@ -5,6 +5,7 @@ from typing import Any
from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.models.file import find_file
class RmCommand(BaseCommand):
@@ -32,7 +33,8 @@ class RmCommand(BaseCommand):
connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name)
parent_folder = cls._find_folder(connection, laboratory, r_dirname)
file = parent_folder.find_file(r_basename)
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)
+16 -5
View File
@@ -9,13 +9,15 @@ from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import IllegalArgumentException, MDRSException
from mdrsclient.models import Folder
from mdrsclient.models import File, Folder
from mdrsclient.models.file import find_file
from mdrsclient.settings import CONCURRENT
@dataclass(frozen=True)
class UploadFileInfo:
folder: Folder
files: list[File]
path: str
@@ -53,6 +55,7 @@ class UploadCommand(BaseCommand):
connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name)
folder = cls._find_folder(connection, laboratory, r_path)
files = cls._find_files(connection, folder.id)
infos: list[UploadFileInfo] = []
if os.path.isdir(l_path):
if not is_recursive:
@@ -60,6 +63,8 @@ class UploadCommand(BaseCommand):
folder_api = FoldersApi(connection)
folder_map: dict[str, Folder] = {}
folder_map[r_path] = folder
files_map: dict[str, list[File]] = {}
files_map[r_path] = files
l_basename = os.path.basename(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))
@@ -68,7 +73,10 @@ class UploadCommand(BaseCommand):
# prepare destination parent path
d_parent_dirname = os.path.dirname(d_dirname)
if folder_map.get(d_parent_dirname) is None:
folder_map[d_parent_dirname] = cls._find_folder(connection, laboratory, d_parent_dirname)
parent_folder = cls._find_folder(connection, laboratory, d_parent_dirname)
folder_map[d_parent_dirname] = parent_folder
parent_files = cls._find_files(connection, parent_folder.id)
files_map[d_parent_dirname] = parent_files
# prepare destination path
if folder_map.get(d_dirname) is None:
d_folder = folder_map[d_parent_dirname].find_sub_folder(d_basename)
@@ -78,13 +86,16 @@ class UploadCommand(BaseCommand):
d_folder_id = d_folder.id
print(d_dirname)
folder_map[d_dirname] = folder_api.retrieve(d_folder_id)
files_map[d_dirname] = cls._find_files(connection, d_folder_id)
if d_folder is None:
folder_map[d_parent_dirname].sub_folders.append(folder_map[d_dirname])
# register upload file list
for filename in filenames:
infos.append(UploadFileInfo(folder_map[d_dirname], os.path.join(dirpath, filename)))
infos.append(
UploadFileInfo(folder_map[d_dirname], files_map[d_dirname], os.path.join(dirpath, filename))
)
else:
infos.append(UploadFileInfo(folder, l_path))
infos.append(UploadFileInfo(folder, files, l_path))
cls.__multiple_upload(connection, infos, is_skip_if_exists)
@classmethod
@@ -98,7 +109,7 @@ class UploadCommand(BaseCommand):
@classmethod
def __multiple_upload_worker(cls, file_api: FilesApi, info: UploadFileInfo, is_skip_if_exists: bool) -> None:
basename = os.path.basename(info.path)
file = info.folder.find_file(basename)
file = find_file(info.files, basename)
try:
if file is None:
file_api.create(info.folder.id, info.path)
+20
View File
@@ -0,0 +1,20 @@
from argparse import Namespace
from typing import Any
from mdrsclient.__version__ import __version__
from mdrsclient.commands.base import BaseCommand
class VersionCommand(BaseCommand):
@classmethod
def register(cls, parsers: Any) -> None:
version_parser = parsers.add_parser("version", help="show the version of this tool")
version_parser.set_defaults(func=cls.func)
@classmethod
def func(cls, args: Namespace) -> None:
cls.version()
@classmethod
def version(cls) -> None:
print(f"mdrs {__version__}")
+6
View File
@@ -1,4 +1,5 @@
from typing import Any
from unicodedata import normalize
from pydantic.dataclasses import dataclass
@@ -25,3 +26,8 @@ class File:
@property
def updated_at_name(self) -> str:
return iso8601_to_user_friendly(self.updated_at)
def find_file(files: list[File], name: str) -> File | None:
_name = normalize("NFC", name).lower()
return next((x for x in files if x.name.lower() == _name), None)
-6
View File
@@ -3,7 +3,6 @@ from unicodedata import normalize
from pydantic.dataclasses import dataclass
from mdrsclient.models.file import File
from mdrsclient.models.utils import iso8601_to_user_friendly
@@ -78,13 +77,8 @@ class FolderSimple:
class Folder(FolderSimple):
metadata: list[dict[str, Any]]
sub_folders: list[FolderSimple]
files: list[File]
path: str
def find_sub_folder(self, name: str) -> FolderSimple | None:
_name = normalize("NFC", name).lower()
return next((x for x in self.sub_folders if x.name.lower() == _name), None)
def find_file(self, name: str) -> File | None:
_name = normalize("NFC", name).lower()
return next((x for x in self.files if x.name.lower() == _name), None)
+8
View File
@@ -1,5 +1,6 @@
import os
from typing import IO, Any
from urllib.parse import parse_qs, urlparse
if os.name == "nt":
import msvcrt
@@ -21,3 +22,10 @@ class FileLock:
msvcrt.locking(file.fileno(), msvcrt.LK_UNLCK, 1)
elif os.name == "posix":
fcntl.flock(file.fileno(), fcntl.LOCK_UN)
def page_num_from_url(url: str) -> int | None:
parsed_url = urlparse(url)
params = parse_qs(parsed_url.query)
page = params.get("page", [None])[0]
return int(page) if page is not None else None
+3 -2
View File
@@ -1,12 +1,12 @@
[tool.poetry]
name = "mdrs-client-python"
version = "1.3.12"
version = "1.3.14"
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",
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
@@ -14,6 +14,7 @@ classifiers=[
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"OSI Approved :: MIT License",
"Topic :: Utilities",
]