Compare commits

..

18 Commits
v1.3.3 ... main

Author SHA1 Message Date
7b3f1f2d09
update version to 1.3.11 2025-01-21 18:33:41 +09:00
8a5e1b68b7
follow-up fixes due to recent User API specification changes. 2025-01-21 18:30:22 +09:00
dd00973bea
update version number to 1.3.10. 2024-12-23 20:43:57 +09:00
0e5685d5ea
update libraries and version number. 2024-12-23 14:28:25 +09:00
6a2810f603
show summary when file download fails and delete broken files. 2024-12-23 13:58:14 +09:00
d5ac5cd427
fixed compatibility with python 3.10. 2024-10-23 18:33:12 +09:00
ab7cd1b885
update version to 1.3.8. 2024-09-18 11:24:06 +09:00
f2f898c263
check the exception to unexpected responses from the server. 2024-09-18 11:22:03 +09:00
24172dc65c
implemented -s --skip-if-file-exists option for download commnad. 2024-09-18 10:56:40 +09:00
f2c5a06cb4
implemented --exclude argument for download subcommand. 2024-07-22 14:35:34 +09:00
49cb411af4
update version to 1.3.6. 2024-07-08 21:18:47 +09:00
3e8ab8de3a
update version to 1.3.6. 2024-07-08 20:37:28 +09:00
d392379235
implemented to cancel to recursive download if some files failed to download. 2024-07-08 20:35:48 +09:00
c8b16939d7
update version to 1.3.5. 2024-07-08 18:13:09 +09:00
0d8deb02d7
add token check for file download operation. 2024-07-08 18:07:26 +09:00
c2a67aa861
removed debug code. 2024-07-04 15:21:45 +09:00
4696b9799c
add some aliases for config sub command. 2024-07-04 12:35:59 +09:00
020ef8835a
fixed bug to upload large file. 2024-07-04 12:33:57 +09:00
14 changed files with 162 additions and 51 deletions

View File

@ -1,7 +1,14 @@
{ {
"version": "0.2", "version": "0.2",
"language": "en,en-gb", "language": "en,en-gb",
"ignoreWords": ["followlinks", "getframe", "pycache", "pydantic", "UNLCK"], "ignoreWords": [
"followlinks",
"getframe",
"pycache",
"pydantic",
"toolbelt",
"UNLCK"
],
"words": [ "words": [
"chacl", "chacl",
"kikan", "kikan",
@ -9,7 +16,11 @@
"mdrsclient", "mdrsclient",
"neurodata", "neurodata",
"Neuroinformatics", "Neuroinformatics",
"orcid",
"RIKEN" "RIKEN"
], ],
"ignorePaths": [".env", "__pycache__"] "ignorePaths": [
".env",
"__pycache__"
]
} }

3
.gitignore vendored
View File

@ -160,4 +160,5 @@ cython_debug/
.idea/ .idea/
# mdrs-cli # mdrs-cli
.neurodatacli.config .neurodatacli.config
poetry.toml

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2023 Neuroinformatics Unit, RIKEN CBS Copyright (c) 2023- Neuroinformatics Unit, RIKEN CBS
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -60,7 +60,7 @@ List the folder contents
```shell ```shell
mdrs ls neurodata:/NIU/Repository/ mdrs ls neurodata:/NIU/Repository/
mdrs ls -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open/ mdrs ls -p SHARING_PASSWORD neurodata:/NIU/Repository/PW_Open/
mdrs ls -r neurodata:/NIU/Repository/Dataset1/ mdrs ls -r neurodata:/NIU/Repository/Dataset1/
mdrs ls -J -r neurodata:/NIU/Repository/Dataset1/ mdrs ls -J -r neurodata:/NIU/Repository/Dataset1/
``` ```
@ -80,6 +80,7 @@ Upload the file or directory
```shell ```shell
mdrs upload ./sample.dat neurodata:/NIU/Repository/TEST/ mdrs upload ./sample.dat neurodata:/NIU/Repository/TEST/
mdrs upload -r ./dataset neurodata:/NIU/Repository/TEST/ mdrs upload -r ./dataset neurodata:/NIU/Repository/TEST/
mdrs upload -r --skip-if-exists ./dataset neurodata:/NIU/Repository/TEST/
``` ```
### download ### download
@ -89,7 +90,9 @@ Download the file or folder
```shell ```shell
mdrs download neurodata:/NIU/Repository/TEST/sample.dat ./ mdrs download neurodata:/NIU/Repository/TEST/sample.dat ./
mdrs download -r neurodata:/NIU/Repository/TEST/dataset/ ./ mdrs download -r neurodata:/NIU/Repository/TEST/dataset/ ./
mdrs download -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open/Readme.dat ./ mdrs download -p SHARING_PASSWORD neurodata:/NIU/Repository/PW_Open/Readme.dat ./
mdrs download -r --exclude /NIU/Repository/TEST/dataset/skip neurodata:/NIU/Repository/TEST/dataset/ ./
mdrs download -r --skip-if-exists neurodata:/NIU/Repository/TEST/dataset/ ./
``` ```
### mv ### mv
@ -115,8 +118,8 @@ mdrs cp -r neurodata:/NIU/Repository/TEST/dataset neurodata:/NIU/Repository/TEST
Remove the file or folder Remove the file or folder
```shell ```shell
mdrs rm neurodata:/NIU/Repository/TEST2/sample2.dat mdrs rm neurodata:/NIU/Repository/TEST/sample.dat
mdrs rm -r neurodata:/NIU/Repository/TEST2/dataset mdrs rm -r neurodata:/NIU/Repository/TEST/dataset
``` ```
### chacl ### chacl
@ -126,7 +129,7 @@ Change the folder access level
```shell ```shell
mdrs chacl private neurodata:/NIU/Repository/Private mdrs chacl private neurodata:/NIU/Repository/Private
mdrs chacl cbs_open -r neurodata:/NIU/Repository/CBS_Open mdrs chacl cbs_open -r neurodata:/NIU/Repository/CBS_Open
mdrs chacl pw_open -r -p FOLDER_PASSWORD neurodata:/NIU/Repository/PW_Open mdrs chacl pw_open -r -p SHARING_PASSWORD neurodata:/NIU/Repository/PW_Open
``` ```
### metadata ### metadata
@ -135,7 +138,7 @@ Get a folder metadata
```shell ```shell
mdrs metadata neurodata:/NIU/Repository/TEST/ mdrs metadata neurodata:/NIU/Repository/TEST/
mdrs metadata -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open/ mdrs metadata -p SHARING_PASSWORD neurodata:/NIU/Repository/PW_Open/
``` ```
### file-metadata ### file-metadata
@ -144,7 +147,7 @@ Get the file metadata
```shell ```shell
mdrs file-metadata neurodata:/NIU/Repository/TEST/dataset/sample.dat mdrs file-metadata neurodata:/NIU/Repository/TEST/dataset/sample.dat
mdrs file-metadata -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open/Readme.txt mdrs file-metadata -p SHARING_PASSWORD neurodata:/NIU/Repository/PW_Open/Readme.txt
``` ```
### help ### help

View File

@ -1 +1 @@
1.3.1 1.3.11

View File

@ -1,5 +1,6 @@
import argparse import argparse
import sys import sys
from json import JSONDecodeError
from mdrsclient.commands import ( from mdrsclient.commands import (
ChaclCommand, ChaclCommand,
@ -52,6 +53,9 @@ def main() -> None:
except MDRSException as e: except MDRSException as e:
print(f"Error: {e}") print(f"Error: {e}")
sys.exit(2) sys.exit(2)
except JSONDecodeError:
print("Unexpected response returned. Please check the configuration or the server's operational status.")
sys.exit(2)
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit(130) sys.exit(130)

View File

@ -1,8 +1,10 @@
import mimetypes
import os import os
from typing import Any, Final from typing import Any, Final
from pydantic import TypeAdapter from pydantic import TypeAdapter
from pydantic.dataclasses import dataclass from pydantic.dataclasses import dataclass
from requests_toolbelt.multipart.encoder import MultipartEncoder
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
@ -17,6 +19,7 @@ class FilesApiCreateResponse:
class FilesApi(BaseApi): class FilesApi(BaseApi):
ENTRYPOINT: Final[str] = "v3/files/" ENTRYPOINT: Final[str] = "v3/files/"
FALLBACK_MIMETYPE: Final[str] = "application/octet-stream"
def retrieve(self, id: str) -> File: def retrieve(self, id: str) -> File:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name) # print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
@ -30,30 +33,43 @@ class FilesApi(BaseApi):
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name) # print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT url = self.ENTRYPOINT
token_check(self.connection) token_check(self.connection)
data: dict[str, str | int] = {"folder_id": folder_id} data: dict[str, str | int] | MultipartEncoder = {}
try: try:
with open(os.path.realpath(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}) data = MultipartEncoder(
fields={"folder_id": folder_id, "file": (os.path.basename(path), fp, self._get_mime_type(path))}
)
response = self.connection.post(url, data=data, headers={"Content-Type": data.content_type})
self._raise_response_error(response) self._raise_response_error(response)
ret = TypeAdapter(FilesApiCreateResponse).validate_python(response.json()) ret = TypeAdapter(FilesApiCreateResponse).validate_python(response.json())
except OSError: except OSError:
raise UnexpectedException(f"Could not open `{path}` file.") raise UnexpectedException(f"Could not open `{path}` file.")
except MemoryError:
raise UnexpectedException("Out of memory.")
except Exception as e:
raise UnexpectedException("Unspecified error.") from e
return ret.id return ret.id
def update(self, file: File, path: str | None) -> bool: def update(self, file: File, path: str | None) -> bool:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name) # print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + file.id + "/" url = self.ENTRYPOINT + file.id + "/"
token_check(self.connection) token_check(self.connection)
data: dict[str, str | int] | MultipartEncoder = {}
if path is not None: if path is not None:
# update file body # update file body
try: try:
with open(os.path.realpath(path), mode="rb") as fp: with open(os.path.realpath(path), mode="rb") as fp:
response = self.connection.put(url, files={"file": fp}) data = MultipartEncoder(fields={"file": (os.path.basename(path), fp, self._get_mime_type(path))})
response = self.connection.put(url, data=data, headers={"Content-Type": data.content_type})
except OSError: except OSError:
raise UnexpectedException(f"Could not open `{path}` file.") raise UnexpectedException(f"Could not open `{path}` file.")
except MemoryError:
raise UnexpectedException("Out of memory.")
except Exception as e:
raise UnexpectedException("Unspecified error.") from e
else: else:
# update metadata # update metadata
data: dict[str, str | int] = {"name": file.name, "description": file.description} data = {"name": file.name, "description": file.description}
response = self.connection.put(url, data=data) response = self.connection.put(url, data=data)
self._raise_response_error(response) self._raise_response_error(response)
return True return True
@ -95,6 +111,7 @@ class FilesApi(BaseApi):
def download(self, file: File, path: str) -> bool: def download(self, file: File, path: str) -> bool:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name) # print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = file.download_url url = file.download_url
token_check(self.connection)
response = self.connection.get(url, stream=True) response = self.connection.get(url, stream=True)
self._raise_response_error(response) self._raise_response_error(response)
try: try:
@ -106,3 +123,9 @@ class FilesApi(BaseApi):
except PermissionError: except PermissionError:
print(f"Cannot create file `{path}`: Permission denied.") print(f"Cannot create file `{path}`: Permission denied.")
return True return True
def _get_mime_type(self, path: str) -> str:
mt = mimetypes.guess_type(path)
if mt:
return mt[0] or self.FALLBACK_MIMETYPE
return self.FALLBACK_MIMETYPE

View File

@ -20,8 +20,10 @@ class UsersCurrentResponseLaboratory:
class UsersApiCurrentResponse: class UsersApiCurrentResponse:
id: int id: int
username: str username: str
full_name: str first_name: str
last_name: str
email: str email: str
orcid_id: str
laboratories: list[UsersCurrentResponseLaboratory] laboratories: list[UsersCurrentResponseLaboratory]
is_staff: bool is_staff: bool
is_active: bool is_active: bool

View File

@ -25,11 +25,11 @@ class ConfigCommand(BaseCommand):
update_parser.add_argument("url", help="API entrypoint url of remote host") update_parser.add_argument("url", help="API entrypoint url of remote host")
update_parser.set_defaults(func=cls.func_update) update_parser.set_defaults(func=cls.func_update)
# config list # config list
list_parser = config_parsers.add_parser("list", help="list all the remote hosts") 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.add_argument("-l", "--long", help="show the api url", action="store_true")
list_parser.set_defaults(func=cls.func_list) list_parser.set_defaults(func=cls.func_list)
# config delete # config delete
delete_parser = config_parsers.add_parser("delete", help="delete an existing remote host") delete_parser = config_parsers.add_parser("delete", help="delete an existing remote host", aliases=["remove"])
delete_parser.add_argument("remote", help="label of remote host") delete_parser.add_argument("remote", help="label of remote host")
delete_parser.set_defaults(func=cls.func_delete) delete_parser.set_defaults(func=cls.func_delete)

View File

@ -8,8 +8,8 @@ from pydantic.dataclasses import dataclass
from mdrsclient.api import FilesApi, FoldersApi from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand from mdrsclient.commands.base import BaseCommand
from mdrsclient.connection import MDRSConnection from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import IllegalArgumentException from mdrsclient.exceptions import IllegalArgumentException, UnexpectedException
from mdrsclient.models import File from mdrsclient.models import File, Folder, Laboratory
from mdrsclient.settings import CONCURRENT from mdrsclient.settings import CONCURRENT
@ -19,6 +19,13 @@ class DownloadFileInfo:
path: str path: str
@dataclass
class DownloadContext:
hasError: bool
isSkipIfExists: bool
files: list[DownloadFileInfo]
class DownloadCommand(BaseCommand): class DownloadCommand(BaseCommand):
@classmethod @classmethod
def register(cls, parsers: Any) -> None: def register(cls, parsers: Any) -> None:
@ -26,6 +33,15 @@ class DownloadCommand(BaseCommand):
download_parser.add_argument( download_parser.add_argument(
"-r", "--recursive", help="download folders and their contents recursive", action="store_true" "-r", "--recursive", help="download folders and their contents recursive", action="store_true"
) )
download_parser.add_argument(
"-s",
"--skip-if-exists",
help="skip the download if file is already downloaded and file size is the same",
action="store_true",
)
download_parser.add_argument(
"-e", "--exclude", help="exclude to download path matched file or folders", action="append"
)
download_parser.add_argument("-p", "--password", help="password to use when open locked folder") download_parser.add_argument("-p", "--password", help="password to use when open locked folder")
download_parser.add_argument("remote_path", help="remote file path (remote:/lab/path/file)") download_parser.add_argument("remote_path", help="remote file path (remote:/lab/path/file)")
download_parser.add_argument("local_path", help="local folder path (/foo/bar/)") download_parser.add_argument("local_path", help="local folder path (/foo/bar/)")
@ -36,11 +52,21 @@ class DownloadCommand(BaseCommand):
remote_path = str(args.remote_path) remote_path = str(args.remote_path)
local_path = str(args.local_path) local_path = str(args.local_path)
is_recursive = bool(args.recursive) is_recursive = bool(args.recursive)
is_skip_if_exists = bool(args.skip_if_exists)
password = str(args.password) if args.password else None password = str(args.password) if args.password else None
cls.download(remote_path, local_path, is_recursive, password) excludes = list(map(lambda x: str(x).rstrip("/").lower(), args.exclude)) if args.exclude is not None else []
cls.download(remote_path, local_path, is_recursive, is_skip_if_exists, password, excludes)
@classmethod @classmethod
def download(cls, remote_path: str, local_path: str, is_recursive: bool, password: str | None) -> None: def download(
cls,
remote_path: str,
local_path: str,
is_recursive: bool,
is_skip_if_exists: bool,
password: str | None,
excludes: list[str],
) -> 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)
r_path = r_path.rstrip("/") r_path = r_path.rstrip("/")
r_dirname = os.path.dirname(r_path) r_dirname = os.path.dirname(r_path)
@ -52,10 +78,13 @@ class DownloadCommand(BaseCommand):
laboratory = cls._find_laboratory(connection, laboratory_name) laboratory = cls._find_laboratory(connection, laboratory_name)
r_parent_folder = cls._find_folder(connection, laboratory, r_dirname, password) r_parent_folder = cls._find_folder(connection, laboratory, r_dirname, password)
file = r_parent_folder.find_file(r_basename) file = r_parent_folder.find_file(r_basename)
download_files: list[DownloadFileInfo] = []
if file is not None: if file is not None:
if cls.__check_excludes(excludes, laboratory, r_parent_folder, file):
return
context = DownloadContext(False, is_skip_if_exists, [])
l_path = os.path.join(l_dirname, r_basename) l_path = os.path.join(l_dirname, r_basename)
download_files.append(DownloadFileInfo(file, l_path)) context.files.append(DownloadFileInfo(file, l_path))
cls.__multiple_download(connection, context)
else: else:
folder = r_parent_folder.find_sub_folder(r_basename) folder = r_parent_folder.find_sub_folder(r_basename)
if folder is None: if folder is None:
@ -63,31 +92,67 @@ class DownloadCommand(BaseCommand):
if not is_recursive: if not is_recursive:
raise IllegalArgumentException(f"Cannot download `{r_path}`: Is a folder.") raise IllegalArgumentException(f"Cannot download `{r_path}`: Is a folder.")
folder_api = FoldersApi(connection) folder_api = FoldersApi(connection)
cls.__multiple_download_pickup_recursive_files(folder_api, download_files, folder.id, l_dirname) cls.__multiple_download_pickup_recursive_files(
cls.__multiple_download(connection, download_files) connection, folder_api, laboratory, folder.id, l_dirname, excludes, is_skip_if_exists
)
@classmethod @classmethod
def __multiple_download_pickup_recursive_files( def __multiple_download_pickup_recursive_files(
cls, folder_api: FoldersApi, infolist: list[DownloadFileInfo], folder_id: str, basedir: str cls,
connection: MDRSConnection,
folder_api: FoldersApi,
laboratory: Laboratory,
folder_id: str,
basedir: str,
excludes: list[str],
is_skip_if_exists: bool,
) -> None: ) -> None:
context = DownloadContext(False, is_skip_if_exists, [])
folder = folder_api.retrieve(folder_id) folder = folder_api.retrieve(folder_id)
dirname = os.path.join(basedir, folder.name) dirname = os.path.join(basedir, folder.name)
if cls.__check_excludes(excludes, laboratory, folder, None):
return
if not os.path.exists(dirname): if not os.path.exists(dirname):
os.makedirs(dirname) os.makedirs(dirname)
print(dirname) print(dirname)
for file in folder.files: for file in folder.files:
if cls.__check_excludes(excludes, laboratory, folder, file):
continue
path = os.path.join(dirname, file.name) path = os.path.join(dirname, file.name)
infolist.append(DownloadFileInfo(file, path)) context.files.append(DownloadFileInfo(file, path))
cls.__multiple_download(connection, context)
if context.hasError:
raise UnexpectedException("Some files failed to download.")
for sub_folder in folder.sub_folders: for sub_folder in folder.sub_folders:
cls.__multiple_download_pickup_recursive_files(folder_api, infolist, sub_folder.id, dirname) cls.__multiple_download_pickup_recursive_files(
connection, folder_api, laboratory, sub_folder.id, dirname, excludes, is_skip_if_exists
)
@classmethod @classmethod
def __multiple_download(cls, connection: MDRSConnection, infolist: list[DownloadFileInfo]) -> None: def __multiple_download(cls, connection: MDRSConnection, context: DownloadContext) -> None:
file_api = FilesApi(connection) file_api = FilesApi(connection)
with ThreadPoolExecutor(max_workers=CONCURRENT) as pool: with ThreadPoolExecutor(max_workers=CONCURRENT) as pool:
pool.map(lambda x: cls.__multiple_download_worker(file_api, x), infolist) results = pool.map(
lambda x: cls.__multiple_download_worker(file_api, x, context.isSkipIfExists), context.files
)
hasError = next(filter(lambda x: x is False, results), None)
if hasError is not None:
context.hasError = True
@classmethod @classmethod
def __multiple_download_worker(cls, file_api: FilesApi, info: DownloadFileInfo) -> None: def __multiple_download_worker(cls, file_api: FilesApi, info: DownloadFileInfo, is_skip_if_exists: bool) -> bool:
file_api.download(info.file, info.path) if not is_skip_if_exists or os.path.exists(info.path) and info.file.size != os.path.getsize(info.path):
try:
file_api.download(info.file, info.path)
except Exception:
print(f"Failed: ${info.path}")
if os.path.isfile(info.path):
os.remove(info.path)
return False
print(info.path) print(info.path)
return True
@classmethod
def __check_excludes(cls, excludes: list[str], laboratory: Laboratory, folder: Folder, file: File | None) -> bool:
path = f"/{laboratory.name}{folder.path}{file.name if file is not None else ''}".rstrip("/").lower()
return path in excludes

View File

@ -106,4 +106,4 @@ class UploadCommand(BaseCommand):
file_api.update(file, info.path) file_api.update(file, info.path)
print(os.path.join(info.folder.path, basename)) print(os.path.join(info.folder.path, basename))
except MDRSException as e: except MDRSException as e:
print(f"API Error: {e}") print(f"Error: {e}")

View File

@ -2,7 +2,7 @@ import configparser
import os import os
from typing import Final from typing import Final
import validators # type: ignore import validators
from mdrsclient.exceptions import IllegalArgumentException from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.settings import CONFIG_DIRNAME from mdrsclient.settings import CONFIG_DIRNAME
@ -41,7 +41,7 @@ class ConfigFile:
@url.setter @url.setter
def url(self, url: str) -> None: def url(self, url: str) -> None:
if not validators.url(url): # type: ignore if not validators.url(url):
raise IllegalArgumentException("malformed URI sequence") raise IllegalArgumentException("malformed URI sequence")
self.__load() self.__load()
if self.__config.has_section(self.remote): if self.__config.has_section(self.remote):

View File

@ -1,9 +1,9 @@
import platform import platform
import threading import threading
from io import BufferedReader
from typing import TypedDict from typing import TypedDict
from requests import Response, Session from requests import Response, Session
from requests_toolbelt.multipart.encoder import MultipartEncoder
# Unpack is new in 3.11 # Unpack is new in 3.11
from typing_extensions import Unpack from typing_extensions import Unpack
@ -21,14 +21,14 @@ class _KwArgsMDRSConnectionGet(TypedDict, total=False):
class _KwArgsMDRSConnectionPost(TypedDict, total=False): class _KwArgsMDRSConnectionPost(TypedDict, total=False):
params: dict[str, str | int] params: dict[str, str | int]
data: dict[str, str | int] data: dict[str, str | int] | MultipartEncoder
files: dict[str, BufferedReader] headers: dict[str, str]
class _KwArgsMDRSConnectionPut(TypedDict, total=False): class _KwArgsMDRSConnectionPut(TypedDict, total=False):
params: dict[str, str | int] params: dict[str, str | int]
data: dict[str, str | int] data: dict[str, str | int] | MultipartEncoder
files: dict[str, BufferedReader] headers: dict[str, str]
class _KwArgsMDRSConnectionDelete(TypedDict, total=False): class _KwArgsMDRSConnectionDelete(TypedDict, total=False):

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "mdrs-client-python" name = "mdrs-client-python"
version = "1.3.3" version = "1.3.11"
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"
@ -13,6 +13,7 @@ classifiers=[
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"OSI Approved :: MIT License", "OSI Approved :: MIT License",
"Topic :: Utilities", "Topic :: Utilities",
] ]
@ -22,19 +23,20 @@ packages = [
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.10"
requests = "^2.31.0" requests = "^2.32.3"
requests-toolbelt = "^1.0.0"
python-dotenv = "^1.0.1" python-dotenv = "^1.0.1"
pydantic = "^2.6.1" pydantic = "^2.10.5"
pydantic-settings = "^2.1.0" pydantic-settings = "^2.7.1"
PyJWT = "^2.8.0" PyJWT = "^2.10.1"
validators = "^0.22.0" validators = "^0.34.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "^24.2.0" black = "^24.10.0"
flake8 = "^7.0.0" flake8 = "^7.1.1"
Flake8-pyproject = "^1.2.3" Flake8-pyproject = "^1.2.3"
isort = "^5.13.2" isort = "^5.13.2"
pyright = "^1.1.350" pyright = "^1.1.391"
[tool.poetry.scripts] [tool.poetry.scripts]
mdrs = 'mdrsclient.__main__:main' mdrs = 'mdrsclient.__main__:main'