refactor(config): abstract config storage and enable dependency injection

Abstract the configuration storage mechanism to allow using custom
configurations, such as in-memory setups, when using the tool as
a library. This aligns the configuration architecture with the
session cache abstraction.

- Define ConfigInterface protocol and InMemoryConfig class
- Make CacheFile, InMemoryCache, ConfigFile, and InMemoryConfig
  explicitly inherit their interfaces
- Update MdrsService and MdrsClient to accept customizable
  config_class and config instances
- Add validation to check remote parameter consistency in
  create_connection
- Remove unused imports across command files
This commit is contained in:
2026-07-02 23:30:33 +09:00
parent 8ce9e09e69
commit b95fc0cd7d
7 changed files with 78 additions and 34 deletions
+2 -2
View File
@@ -66,7 +66,7 @@ class CacheInterface(Protocol):
def laboratories(self, laboratories: Laboratories) -> None: ... def laboratories(self, laboratories: Laboratories) -> None: ...
class InMemoryCache: class InMemoryCache(CacheInterface):
def __init__(self) -> None: def __init__(self) -> None:
self.__data = CacheData() self.__data = CacheData()
@@ -105,7 +105,7 @@ class InMemoryCache:
self.__data.laboratories = laboratories self.__data.laboratories = laboratories
class CacheFile: class CacheFile(CacheInterface):
__serial: int __serial: int
__cache_dir: str __cache_dir: str
__cache_file: str __cache_file: str
+11 -16
View File
@@ -4,6 +4,7 @@ from unicodedata import normalize
from mdrsclient.api import DoiApi, FilesApi, FoldersApi, LaboratoriesApi, UsersApi from mdrsclient.api import DoiApi, FilesApi, FoldersApi, LaboratoriesApi, UsersApi
from mdrsclient.cache import CacheInterface from mdrsclient.cache import CacheInterface
from mdrsclient.config import ConfigInterface
from mdrsclient.connection import MDRSConnection from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import IllegalArgumentException, MDRSException, UnauthorizedException, UnexpectedException from mdrsclient.exceptions import IllegalArgumentException, MDRSException, UnauthorizedException, UnexpectedException
from mdrsclient.models import File, Folder, Laboratory, Token, User from mdrsclient.models import File, Folder, Laboratory, Token, User
@@ -14,12 +15,14 @@ from mdrsclient.services import MdrsService
class MdrsClient(MdrsService): class MdrsClient(MdrsService):
"""Service layer client for MDRS.""" """Service layer client for MDRS."""
def __init__(self, connection: MDRSConnection): def __init__(self, connection: MDRSConnection, config_class: type[ConfigInterface] | None = None):
super().__init__(connection) super().__init__(connection, config_class)
@classmethod @classmethod
def from_remote(cls, remote: str, cache: CacheInterface | None = None) -> "MdrsClient": def from_remote(
return cls(cls.create_connection(remote, cache)) cls, remote: str, cache: CacheInterface | None = None, config: ConfigInterface | None = None
) -> "MdrsClient":
return cls(cls.create_connection(remote, cache, config))
def mkdir(self, remote_path: str) -> None: def mkdir(self, remote_path: str) -> None:
remote, laboratory_name, r_path = self.parse_remote_host_with_path(remote_path) remote, laboratory_name, r_path = self.parse_remote_host_with_path(remote_path)
@@ -208,36 +211,28 @@ class MdrsClient(MdrsService):
return f"mdrs {__version__}" return f"mdrs {__version__}"
def config_create(self, remote: str, url: str) -> None: def config_create(self, remote: str, url: str) -> None:
from mdrsclient.config import ConfigFile
remote = self.parse_remote_host(remote) remote = self.parse_remote_host(remote)
config = ConfigFile(remote) config = self.config_class(remote)
if config.url is not None: if config.url is not None:
raise IllegalArgumentException(f"Remote host `{remote}` is already exists.") raise IllegalArgumentException(f"Remote host `{remote}` is already exists.")
else: else:
config.url = url config.url = url
def config_update(self, remote: str, url: str) -> None: def config_update(self, remote: str, url: str) -> None:
from mdrsclient.config import ConfigFile
remote = self.parse_remote_host(remote) remote = self.parse_remote_host(remote)
config = ConfigFile(remote) config = self.config_class(remote)
if config.url is None: if config.url is None:
raise IllegalArgumentException(f"Remote host `{remote}` is not exists.") raise IllegalArgumentException(f"Remote host `{remote}` is not exists.")
else: else:
config.url = url config.url = url
def config_list(self) -> list: def config_list(self) -> list:
from mdrsclient.config import ConfigFile config = self.config_class("")
config = ConfigFile("")
return config.list() return config.list()
def config_delete(self, remote: str) -> None: def config_delete(self, remote: str) -> None:
from mdrsclient.config import ConfigFile
remote = self.parse_remote_host(remote) remote = self.parse_remote_host(remote)
config = ConfigFile(remote) config = self.config_class(remote)
if config.url is None: if config.url is None:
raise IllegalArgumentException(f"Remote host `{remote}` is not exists.") raise IllegalArgumentException(f"Remote host `{remote}` is not exists.")
else: else:
-4
View File
@@ -2,11 +2,7 @@ import getpass
from argparse import Namespace from argparse import Namespace
from typing import Any from typing import Any
from mdrsclient.api import UsersApi
from mdrsclient.commands.base import BaseCommand from mdrsclient.commands.base import BaseCommand
from mdrsclient.config import ConfigFile
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import MissingConfigurationException
class LoginCommand(BaseCommand): class LoginCommand(BaseCommand):
-3
View File
@@ -2,9 +2,6 @@ from argparse import Namespace
from typing import Any from typing import Any
from mdrsclient.commands.base import BaseCommand from mdrsclient.commands.base import BaseCommand
from mdrsclient.config import ConfigFile
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import MissingConfigurationException
class LogoutCommand(BaseCommand): class LogoutCommand(BaseCommand):
-3
View File
@@ -2,9 +2,6 @@ from argparse import Namespace
from typing import Any, Final from typing import Any, Final
from mdrsclient.commands.base import BaseCommand from mdrsclient.commands.base import BaseCommand
from mdrsclient.config import ConfigFile
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import MissingConfigurationException
class WhoamiCommand(BaseCommand): class WhoamiCommand(BaseCommand):
+52 -2
View File
@@ -1,6 +1,7 @@
import configparser import configparser
import os import os
from typing import Final import threading
from typing import Final, Protocol, runtime_checkable
import validators import validators
@@ -9,7 +10,56 @@ from mdrsclient.settings import CONFIG_DIRNAME
from mdrsclient.utils import FileLock from mdrsclient.utils import FileLock
class ConfigFile: @runtime_checkable
class ConfigInterface(Protocol):
remote: str
def list(self) -> list[tuple[str, str]]: ...
@property
def url(self) -> str | None: ...
@url.setter
def url(self, url: str) -> None: ...
@url.deleter
def url(self) -> None: ...
class InMemoryConfig(ConfigInterface):
__configs: dict[str, str] = {}
__lock: threading.Lock = threading.Lock()
remote: str
def __init__(self, remote: str) -> None:
self.remote = remote
def list(self) -> list[tuple[str, str]]:
with self.__lock:
return list(self.__configs.items())
@property
def url(self) -> str | None:
with self.__lock:
return self.__configs.get(self.remote)
@url.setter
def url(self, url: str) -> None:
if not validators.url(url):
raise IllegalArgumentException("malformed URI sequence")
with self.__lock:
self.__configs[self.remote] = url
@url.deleter
def url(self) -> None:
with self.__lock:
if self.remote in self.__configs:
del self.__configs[self.remote]
@classmethod
def clear(cls) -> None:
with cls.__lock:
cls.__configs.clear()
class ConfigFile(ConfigInterface):
OPTION_URL: Final[str] = "url" OPTION_URL: Final[str] = "url"
CONFIG_FILENAME: Final[str] = "config.ini" CONFIG_FILENAME: Final[str] = "config.ini"
remote: str remote: str
+13 -4
View File
@@ -5,7 +5,7 @@ from unicodedata import normalize
from mdrsclient.api import DoiApi, FilesApi, FoldersApi, LaboratoriesApi, UsersApi from mdrsclient.api import DoiApi, FilesApi, FoldersApi, LaboratoriesApi, UsersApi
from mdrsclient.cache import CacheInterface from mdrsclient.cache import CacheInterface
from mdrsclient.config import ConfigFile from mdrsclient.config import ConfigFile, ConfigInterface
from mdrsclient.connection import MDRSConnection from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import ( from mdrsclient.exceptions import (
IllegalArgumentException, IllegalArgumentException,
@@ -18,12 +18,21 @@ from mdrsclient.utils import page_num_from_url
class MdrsService: class MdrsService:
def __init__(self, connection: MDRSConnection): config_class: type[ConfigInterface] = ConfigFile
def __init__(self, connection: MDRSConnection, config_class: type[ConfigInterface] | None = None):
self.connection = connection self.connection = connection
if config_class is not None:
self.config_class = config_class
@classmethod @classmethod
def create_connection(cls, remote: str, cache: CacheInterface | None = None) -> MDRSConnection: def create_connection(
config = ConfigFile(remote) cls, remote: str, cache: CacheInterface | None = None, config: ConfigInterface | None = None
) -> MDRSConnection:
if config is None:
config = ConfigFile(remote)
elif config.remote != remote:
raise IllegalArgumentException("Remote host parameter mismatch.")
if config.url is None: if config.url is None:
raise MissingConfigurationException(f"Remote host `{remote}` is not found.") raise MissingConfigurationException(f"Remote host `{remote}` is not found.")
return MDRSConnection(config.remote, config.url, cache=cache) return MDRSConnection(config.remote, config.url, cache=cache)