nrp_cmd.config

This package contains classes to work with the configuration of the NRP invenio client.

This configuration is stored in a JSON file on a file system, usually in .nrp/invenio-config.json.

Normally you'd not use the classes directly, but instantiate the client directly with get_sync_client / get_async_client functions from the nrp_cmd.client module.

To get a configuration object, you can use the Config.from_file() method and either pass the path to the configuration file or let it default to ~/.nrp/invenio-config.json.

 1#
 2# Copyright (C) 2024 CESNET z.s.p.o.
 3#
 4# invenio-nrp is free software; you can redistribute it and/or
 5# modify it under the terms of the MIT License; see LICENSE file for more
 6# details.
 7#
 8"""This package contains classes to work with the configuration of the NRP invenio client.
 9
10This configuration is stored in a JSON file
11on a file system, usually in .nrp/invenio-config.json.
12
13Normally you'd not use the classes directly, but instantiate the client directly
14with get_sync_client / get_async_client functions from the nrp_cmd.client module.
15
16To get a configuration object, you can use the Config.from_file() method
17and either pass the path to the configuration file or let it default to ~/.nrp/invenio-config.json.
18"""
19
20from .config import Config
21from .repository import RepositoryConfig
22
23__all__ = ("Config", "RepositoryConfig")
@define(kw_only=True)
class Config:
 28@define(kw_only=True)
 29class Config:
 30    """The configuration of the NRP client as stored in the configuration file."""
 31
 32    repositories: list[RepositoryConfig] = field(factory=list)
 33    """Locally known repositories."""
 34
 35    default_alias: Optional[str] = None
 36    """The alias of the default repository"""
 37
 38    per_directory_variables: bool = True
 39    """Whether to load variables from a .nrp directory in the current directory.
 40       If set to False, the variables are loaded from the global configuration file
 41       located in ~/.nrp/variables.json.
 42    """
 43
 44    _config_file_path: Optional[Path] = None
 45    """The path from which the config file was loaded."""
 46
 47    @classmethod
 48    def from_file(cls, config_file_path: Optional[Path] = None) -> Self:
 49        """Load the configuration from a file."""
 50        if not config_file_path:
 51            if "NRP_CMD_CONFIG_PATH" in os.environ:
 52                config_file_path = Path(os.environ["NRP_CMD_CONFIG_PATH"])
 53            else:
 54                config_file_path = Path.home() / ".nrp" / "invenio-config.json"
 55
 56        if config_file_path.exists() and config_file_path.stat().st_size > 0:
 57            ret = converter.structure(
 58                json.loads(config_file_path.read_text(encoding="utf-8")), cls
 59            )
 60        else:
 61            ret = cls()
 62        ret._config_file_path = config_file_path
 63        return ret
 64
 65    def save(self, path: Optional[Path] = None) -> None:
 66        """Save the configuration to a file, creating parent directory if needed."""
 67        if path:
 68            self._config_file_path = path
 69        else:
 70            path = self._config_file_path
 71        assert path is not None
 72        path.parent.mkdir(parents=True, exist_ok=True)
 73        path.write_text(json.dumps(converter.unstructure(self), indent=4))
 74
 75    #
 76    # Repository management
 77    #
 78
 79    def get_repository(self, alias: str) -> RepositoryConfig:
 80        """Get a repository by its alias."""
 81        for repo in self.repositories:
 82            if repo.alias == alias:
 83                if not repo.enabled:
 84                    raise ValueError(f"Repository with alias '{alias}' is disabled")
 85                return repo
 86        raise KeyError(f"Repository with alias '{alias}' not found")
 87
 88    @property
 89    def default_repository(self) -> RepositoryConfig:
 90        """Get the default repository."""
 91        if self.default_alias is None:
 92            raise ValueError("Default repository not set")
 93        return self.get_repository(self.default_alias)
 94
 95    def add_repository(self, repository: RepositoryConfig) -> None:
 96        """Add a repository to the configuration."""
 97        self.repositories.append(repository)
 98
 99    def remove_repository(self, repository: RepositoryConfig | str) -> None:
100        """Remove a repository from the configuration."""
101        if isinstance(repository, str):
102            repository = self.get_repository(repository)
103        self.repositories.remove(repository)
104
105    def set_default_repository(self, alias: str) -> None:
106        """Set the default repository by its alias."""
107        try:
108            next(repo for repo in self.repositories if repo.alias == alias)
109        except StopIteration:
110            raise ValueError(f"Repository with alias '{alias}' not found") from None
111        self.default_alias = alias
112
113    def get_repository_from_url(self, record_url: str | URL) -> RepositoryConfig:
114        """Get the repository configuration for a given record URL.
115
116        If there is no repository configuration for the given URL, a dummy
117        repository configuration is returned.
118        """
119        record_url = URL(record_url)
120        repository_root_url = record_url.with_path("/")
121        for repository in self.repositories:
122            if not repository.enabled:
123                continue
124            if repository.url == repository_root_url:
125                return repository
126        # return a dummy repository configuration
127        return RepositoryConfig(
128            alias=str(repository_root_url),
129            url=repository_root_url,
130            info=None,
131        )
132
133    def find_repository(self, repository: URL | str) -> RepositoryConfig:
134        """Find a repository configuration by its URL or alias name."""
135
136        # try alias first
137        if isinstance(repository, str):    
138            with contextlib.suppress(KeyError):
139                return self.get_repository(repository)
140        # if not successful, use the url to get/configure a repository
141        return self.get_repository_from_url(repository)
142
143    def load_variables(self) -> Variables:
144        """Load the global variables from the configuration file."""
145        if self.per_directory_variables:
146            return Variables.from_file(Path.cwd() / ".nrp" / "variables.json")
147        return Variables.from_file()

The configuration of the NRP client as stored in the configuration file.

Config( *, repositories: list[RepositoryConfig] = NOTHING, default_alias: Optional[str] = None, per_directory_variables: bool = True, config_file_path: Optional[pathlib.Path] = None)
2def __init__(self, *, repositories=NOTHING, default_alias=attr_dict['default_alias'].default, per_directory_variables=attr_dict['per_directory_variables'].default, config_file_path=attr_dict['_config_file_path'].default):
3    if repositories is not NOTHING:
4        self.repositories = repositories
5    else:
6        self.repositories = __attr_factory_repositories()
7    self.default_alias = default_alias
8    self.per_directory_variables = per_directory_variables
9    self._config_file_path = config_file_path

Method generated by attrs for class Config.

repositories: list[RepositoryConfig]

Locally known repositories.

default_alias: Optional[str]

The alias of the default repository

per_directory_variables: bool

Whether to load variables from a .nrp directory in the current directory. If set to False, the variables are loaded from the global configuration file located in ~/.nrp/variables.json.

@classmethod
def from_file(cls, config_file_path: Optional[pathlib.Path] = None) -> Self:
47    @classmethod
48    def from_file(cls, config_file_path: Optional[Path] = None) -> Self:
49        """Load the configuration from a file."""
50        if not config_file_path:
51            if "NRP_CMD_CONFIG_PATH" in os.environ:
52                config_file_path = Path(os.environ["NRP_CMD_CONFIG_PATH"])
53            else:
54                config_file_path = Path.home() / ".nrp" / "invenio-config.json"
55
56        if config_file_path.exists() and config_file_path.stat().st_size > 0:
57            ret = converter.structure(
58                json.loads(config_file_path.read_text(encoding="utf-8")), cls
59            )
60        else:
61            ret = cls()
62        ret._config_file_path = config_file_path
63        return ret

Load the configuration from a file.

def save(self, path: Optional[pathlib.Path] = None) -> None:
65    def save(self, path: Optional[Path] = None) -> None:
66        """Save the configuration to a file, creating parent directory if needed."""
67        if path:
68            self._config_file_path = path
69        else:
70            path = self._config_file_path
71        assert path is not None
72        path.parent.mkdir(parents=True, exist_ok=True)
73        path.write_text(json.dumps(converter.unstructure(self), indent=4))

Save the configuration to a file, creating parent directory if needed.

def get_repository(self, alias: str) -> RepositoryConfig:
79    def get_repository(self, alias: str) -> RepositoryConfig:
80        """Get a repository by its alias."""
81        for repo in self.repositories:
82            if repo.alias == alias:
83                if not repo.enabled:
84                    raise ValueError(f"Repository with alias '{alias}' is disabled")
85                return repo
86        raise KeyError(f"Repository with alias '{alias}' not found")

Get a repository by its alias.

default_repository: RepositoryConfig
88    @property
89    def default_repository(self) -> RepositoryConfig:
90        """Get the default repository."""
91        if self.default_alias is None:
92            raise ValueError("Default repository not set")
93        return self.get_repository(self.default_alias)

Get the default repository.

def add_repository(self, repository: RepositoryConfig) -> None:
95    def add_repository(self, repository: RepositoryConfig) -> None:
96        """Add a repository to the configuration."""
97        self.repositories.append(repository)

Add a repository to the configuration.

def remove_repository( self, repository: RepositoryConfig | str) -> None:
 99    def remove_repository(self, repository: RepositoryConfig | str) -> None:
100        """Remove a repository from the configuration."""
101        if isinstance(repository, str):
102            repository = self.get_repository(repository)
103        self.repositories.remove(repository)

Remove a repository from the configuration.

def set_default_repository(self, alias: str) -> None:
105    def set_default_repository(self, alias: str) -> None:
106        """Set the default repository by its alias."""
107        try:
108            next(repo for repo in self.repositories if repo.alias == alias)
109        except StopIteration:
110            raise ValueError(f"Repository with alias '{alias}' not found") from None
111        self.default_alias = alias

Set the default repository by its alias.

def get_repository_from_url( self, record_url: str | yarl.URL) -> RepositoryConfig:
113    def get_repository_from_url(self, record_url: str | URL) -> RepositoryConfig:
114        """Get the repository configuration for a given record URL.
115
116        If there is no repository configuration for the given URL, a dummy
117        repository configuration is returned.
118        """
119        record_url = URL(record_url)
120        repository_root_url = record_url.with_path("/")
121        for repository in self.repositories:
122            if not repository.enabled:
123                continue
124            if repository.url == repository_root_url:
125                return repository
126        # return a dummy repository configuration
127        return RepositoryConfig(
128            alias=str(repository_root_url),
129            url=repository_root_url,
130            info=None,
131        )

Get the repository configuration for a given record URL.

If there is no repository configuration for the given URL, a dummy repository configuration is returned.

def find_repository( self, repository: yarl.URL | str) -> RepositoryConfig:
133    def find_repository(self, repository: URL | str) -> RepositoryConfig:
134        """Find a repository configuration by its URL or alias name."""
135
136        # try alias first
137        if isinstance(repository, str):    
138            with contextlib.suppress(KeyError):
139                return self.get_repository(repository)
140        # if not successful, use the url to get/configure a repository
141        return self.get_repository_from_url(repository)

Find a repository configuration by its URL or alias name.

def load_variables(self) -> nrp_cmd.config.variables.Variables:
143    def load_variables(self) -> Variables:
144        """Load the global variables from the configuration file."""
145        if self.per_directory_variables:
146            return Variables.from_file(Path.cwd() / ".nrp" / "variables.json")
147        return Variables.from_file()

Load the global variables from the configuration file.

@define(kw_only=True)
class RepositoryConfig:
 19@define(kw_only=True)
 20class RepositoryConfig:
 21    """Configuration of the repository."""
 22
 23    alias: str
 24    """The local alias of the repository."""
 25
 26    url: URL
 27    """The api URL of the repository, usually something like https://repository.org/api."""
 28
 29    token: Optional[str] = None
 30    """Bearer token"""
 31
 32    verify_tls: bool = True
 33    """Verify the TLS certificate in https"""
 34
 35    retry_count: int = 10
 36    """Number of times idempotent operations will be retried if something goes wrong."""
 37
 38    retry_after_seconds: int = 10
 39    """If server does not suggest anything else, retry after this interval in seconds"""
 40
 41    info: RepositoryInfo | None = None
 42    """Cached repository info"""
 43
 44    enabled: bool = True
 45    
 46    class Config:  # noqa
 47        extra = "forbid"
 48
 49    @property
 50    def well_known_repository_url(self) -> URL:
 51        """Return URL to the well-known repository endpoint."""
 52        return self.url / ".well-known" / "repository/"
 53
 54    def search_url(self, model: str | None) -> URL:
 55        """Return URL to search for published records within a model."""
 56        assert self.info is not None
 57        model = model or self._default_model_name
 58        if model:
 59            return self.info.models[model].links.published
 60        return self.info.links.records
 61
 62    def user_search_url(self, model: str | None) -> URL:
 63        """Return URL to search for user's records within a model."""
 64        assert self.info is not None
 65        model = model or self._default_model_name
 66        if model:
 67            user_records = self.info.models[model].links.user_records
 68            if user_records is None:
 69                return self.search_url(model)
 70            return user_records
 71        return self.info.links.user_records
 72
 73    def create_url(self, model: str | None) -> URL:
 74        """Return URL to create a new record within a model."""
 75        assert self.info is not None
 76        model = model or self._default_model_name
 77        if model:
 78            return self.info.models[model].links.api
 79        return self.info.links.records
 80
 81    def read_url(self, model: str | None, record_id: str | URL) -> URL:
 82        """Return URL to a published record within a model."""
 83        assert self.info is not None
 84        if isinstance(record_id, URL):
 85            return record_id
 86        if record_id.startswith("https://"):
 87            return URL(record_id)
 88        model = model or self._default_model_name
 89        if model:
 90            return self.info.models[model].links.api / record_id
 91        return self.info.links.records / record_id
 92
 93    def user_read_url(self, model: str | None, record_id: str | URL) -> URL:
 94        """Return URL to a draft record within a model."""
 95        assert self.info is not None
 96        if isinstance(record_id, URL):
 97            return record_id
 98        if record_id.startswith("https://"):
 99            return URL(record_id)
100        model = model or self._default_model_name
101        if model:
102            return self.info.models[model].links.api / record_id / "draft"
103        return self.info.links.records / record_id / "draft"
104
105    @property
106    def requests_url(self) -> URL:
107        """Return URL to the requests endpoint."""
108        assert self.info is not None
109        return self.info.links.requests
110
111    @property
112    def _default_model_name(self) -> str | None:
113        """Return the default model name if there is only one model in the repository."""
114        if self.info and len(self.info.models) == 1:
115            return next(iter(self.info.models))
116        return None

Configuration of the repository.

RepositoryConfig( *, alias: str, url: yarl.URL, token: Optional[str] = None, verify_tls: bool = True, retry_count: int = 10, retry_after_seconds: int = 10, info: nrp_cmd.types.RepositoryInfo | None = None, enabled: bool = True)
 2def __init__(self, *, alias, url, token=attr_dict['token'].default, verify_tls=attr_dict['verify_tls'].default, retry_count=attr_dict['retry_count'].default, retry_after_seconds=attr_dict['retry_after_seconds'].default, info=attr_dict['info'].default, enabled=attr_dict['enabled'].default):
 3    self.alias = alias
 4    self.url = url
 5    self.token = token
 6    self.verify_tls = verify_tls
 7    self.retry_count = retry_count
 8    self.retry_after_seconds = retry_after_seconds
 9    self.info = info
10    self.enabled = enabled

Method generated by attrs for class RepositoryConfig.

alias: str

The local alias of the repository.

url: yarl.URL

The api URL of the repository, usually something like https://repository.org/api.

token: Optional[str]

Bearer token

verify_tls: bool

Verify the TLS certificate in https

retry_count: int

Number of times idempotent operations will be retried if something goes wrong.

retry_after_seconds: int

If server does not suggest anything else, retry after this interval in seconds

Cached repository info

enabled: bool
well_known_repository_url: yarl.URL
49    @property
50    def well_known_repository_url(self) -> URL:
51        """Return URL to the well-known repository endpoint."""
52        return self.url / ".well-known" / "repository/"

Return URL to the well-known repository endpoint.

def search_url(self, model: str | None) -> yarl.URL:
54    def search_url(self, model: str | None) -> URL:
55        """Return URL to search for published records within a model."""
56        assert self.info is not None
57        model = model or self._default_model_name
58        if model:
59            return self.info.models[model].links.published
60        return self.info.links.records

Return URL to search for published records within a model.

def user_search_url(self, model: str | None) -> yarl.URL:
62    def user_search_url(self, model: str | None) -> URL:
63        """Return URL to search for user's records within a model."""
64        assert self.info is not None
65        model = model or self._default_model_name
66        if model:
67            user_records = self.info.models[model].links.user_records
68            if user_records is None:
69                return self.search_url(model)
70            return user_records
71        return self.info.links.user_records

Return URL to search for user's records within a model.

def create_url(self, model: str | None) -> yarl.URL:
73    def create_url(self, model: str | None) -> URL:
74        """Return URL to create a new record within a model."""
75        assert self.info is not None
76        model = model or self._default_model_name
77        if model:
78            return self.info.models[model].links.api
79        return self.info.links.records

Return URL to create a new record within a model.

def read_url(self, model: str | None, record_id: str | yarl.URL) -> yarl.URL:
81    def read_url(self, model: str | None, record_id: str | URL) -> URL:
82        """Return URL to a published record within a model."""
83        assert self.info is not None
84        if isinstance(record_id, URL):
85            return record_id
86        if record_id.startswith("https://"):
87            return URL(record_id)
88        model = model or self._default_model_name
89        if model:
90            return self.info.models[model].links.api / record_id
91        return self.info.links.records / record_id

Return URL to a published record within a model.

def user_read_url(self, model: str | None, record_id: str | yarl.URL) -> yarl.URL:
 93    def user_read_url(self, model: str | None, record_id: str | URL) -> URL:
 94        """Return URL to a draft record within a model."""
 95        assert self.info is not None
 96        if isinstance(record_id, URL):
 97            return record_id
 98        if record_id.startswith("https://"):
 99            return URL(record_id)
100        model = model or self._default_model_name
101        if model:
102            return self.info.models[model].links.api / record_id / "draft"
103        return self.info.links.records / record_id / "draft"

Return URL to a draft record within a model.

requests_url: yarl.URL
105    @property
106    def requests_url(self) -> URL:
107        """Return URL to the requests endpoint."""
108        assert self.info is not None
109        return self.info.links.requests

Return URL to the requests endpoint.

class RepositoryConfig.Config:
46    class Config:  # noqa
47        extra = "forbid"
extra = 'forbid'