From b95fc0cd7d6f12a0cc59679a91a2e4546fdbe195 Mon Sep 17 00:00:00 2001 From: Yoshihiro OKUMURA Date: Thu, 2 Jul 2026 23:30:33 +0900 Subject: [PATCH] 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 --- mdrsclient/cache.py | 4 +-- mdrsclient/client.py | 27 +++++++----------- mdrsclient/commands/login.py | 4 --- mdrsclient/commands/logout.py | 3 -- mdrsclient/commands/whoami.py | 3 -- mdrsclient/config.py | 54 +++++++++++++++++++++++++++++++++-- mdrsclient/services.py | 17 ++++++++--- 7 files changed, 78 insertions(+), 34 deletions(-) diff --git a/mdrsclient/cache.py b/mdrsclient/cache.py index 7eae530..758ca96 100644 --- a/mdrsclient/cache.py +++ b/mdrsclient/cache.py @@ -66,7 +66,7 @@ class CacheInterface(Protocol): def laboratories(self, laboratories: Laboratories) -> None: ... -class InMemoryCache: +class InMemoryCache(CacheInterface): def __init__(self) -> None: self.__data = CacheData() @@ -105,7 +105,7 @@ class InMemoryCache: self.__data.laboratories = laboratories -class CacheFile: +class CacheFile(CacheInterface): __serial: int __cache_dir: str __cache_file: str diff --git a/mdrsclient/client.py b/mdrsclient/client.py index 6b7ee5e..dd55b93 100644 --- a/mdrsclient/client.py +++ b/mdrsclient/client.py @@ -4,6 +4,7 @@ from unicodedata import normalize from mdrsclient.api import DoiApi, FilesApi, FoldersApi, LaboratoriesApi, UsersApi from mdrsclient.cache import CacheInterface +from mdrsclient.config import ConfigInterface from mdrsclient.connection import MDRSConnection from mdrsclient.exceptions import IllegalArgumentException, MDRSException, UnauthorizedException, UnexpectedException from mdrsclient.models import File, Folder, Laboratory, Token, User @@ -14,12 +15,14 @@ from mdrsclient.services import MdrsService class MdrsClient(MdrsService): """Service layer client for MDRS.""" - def __init__(self, connection: MDRSConnection): - super().__init__(connection) + def __init__(self, connection: MDRSConnection, config_class: type[ConfigInterface] | None = None): + super().__init__(connection, config_class) @classmethod - def from_remote(cls, remote: str, cache: CacheInterface | None = None) -> "MdrsClient": - return cls(cls.create_connection(remote, cache)) + def from_remote( + 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: remote, laboratory_name, r_path = self.parse_remote_host_with_path(remote_path) @@ -208,36 +211,28 @@ class MdrsClient(MdrsService): return f"mdrs {__version__}" def config_create(self, remote: str, url: str) -> None: - from mdrsclient.config import ConfigFile - remote = self.parse_remote_host(remote) - config = ConfigFile(remote) + config = self.config_class(remote) if config.url is not None: raise IllegalArgumentException(f"Remote host `{remote}` is already exists.") else: config.url = url def config_update(self, remote: str, url: str) -> None: - from mdrsclient.config import ConfigFile - remote = self.parse_remote_host(remote) - config = ConfigFile(remote) + config = self.config_class(remote) if config.url is None: raise IllegalArgumentException(f"Remote host `{remote}` is not exists.") else: config.url = url def config_list(self) -> list: - from mdrsclient.config import ConfigFile - - config = ConfigFile("") + config = self.config_class("") return config.list() def config_delete(self, remote: str) -> None: - from mdrsclient.config import ConfigFile - remote = self.parse_remote_host(remote) - config = ConfigFile(remote) + config = self.config_class(remote) if config.url is None: raise IllegalArgumentException(f"Remote host `{remote}` is not exists.") else: diff --git a/mdrsclient/commands/login.py b/mdrsclient/commands/login.py index 22a98f4..f21be39 100644 --- a/mdrsclient/commands/login.py +++ b/mdrsclient/commands/login.py @@ -2,11 +2,7 @@ import getpass from argparse import Namespace from typing import Any -from mdrsclient.api import UsersApi from mdrsclient.commands.base import BaseCommand -from mdrsclient.config import ConfigFile -from mdrsclient.connection import MDRSConnection -from mdrsclient.exceptions import MissingConfigurationException class LoginCommand(BaseCommand): diff --git a/mdrsclient/commands/logout.py b/mdrsclient/commands/logout.py index 4f95983..059d770 100644 --- a/mdrsclient/commands/logout.py +++ b/mdrsclient/commands/logout.py @@ -2,9 +2,6 @@ from argparse import Namespace from typing import Any from mdrsclient.commands.base import BaseCommand -from mdrsclient.config import ConfigFile -from mdrsclient.connection import MDRSConnection -from mdrsclient.exceptions import MissingConfigurationException class LogoutCommand(BaseCommand): diff --git a/mdrsclient/commands/whoami.py b/mdrsclient/commands/whoami.py index e625d4e..ff4ffe3 100644 --- a/mdrsclient/commands/whoami.py +++ b/mdrsclient/commands/whoami.py @@ -2,9 +2,6 @@ from argparse import Namespace from typing import Any, Final from mdrsclient.commands.base import BaseCommand -from mdrsclient.config import ConfigFile -from mdrsclient.connection import MDRSConnection -from mdrsclient.exceptions import MissingConfigurationException class WhoamiCommand(BaseCommand): diff --git a/mdrsclient/config.py b/mdrsclient/config.py index b344201..d5ed9e9 100644 --- a/mdrsclient/config.py +++ b/mdrsclient/config.py @@ -1,6 +1,7 @@ import configparser import os -from typing import Final +import threading +from typing import Final, Protocol, runtime_checkable import validators @@ -9,7 +10,56 @@ from mdrsclient.settings import CONFIG_DIRNAME 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" CONFIG_FILENAME: Final[str] = "config.ini" remote: str diff --git a/mdrsclient/services.py b/mdrsclient/services.py index 47fd718..32667a5 100644 --- a/mdrsclient/services.py +++ b/mdrsclient/services.py @@ -5,7 +5,7 @@ from unicodedata import normalize from mdrsclient.api import DoiApi, FilesApi, FoldersApi, LaboratoriesApi, UsersApi from mdrsclient.cache import CacheInterface -from mdrsclient.config import ConfigFile +from mdrsclient.config import ConfigFile, ConfigInterface from mdrsclient.connection import MDRSConnection from mdrsclient.exceptions import ( IllegalArgumentException, @@ -18,12 +18,21 @@ from mdrsclient.utils import page_num_from_url 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 + if config_class is not None: + self.config_class = config_class @classmethod - def create_connection(cls, remote: str, cache: CacheInterface | None = None) -> MDRSConnection: - config = ConfigFile(remote) + def create_connection( + 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: raise MissingConfigurationException(f"Remote host `{remote}` is not found.") return MDRSConnection(config.remote, config.url, cache=cache)