Skip to content

Configuration

Stored API keys. See API keys.

provesid.config

Configuration management for PROVESID API keys and settings. Provides persistent storage for API keys and user preferences.

The keys live in config.json under $XDG_CONFIG_HOME/provesid (by default ~/.config/provesid), or %APPDATA%\PROVESID on Windows, as plain text. Only CAS Common Chemistry needs one today: CASCommonChem reads it when no key or key file is passed, and before the CCC_API_KEY and CAS_API_KEY environment variables --- so a stored key wins over the environment.

Examples:

>>> from provesid.config import get_config_manager
>>> get_config_manager().config_file.name
'config.json'

Classes

ConfigManager

Manages persistent configuration for PROVESID.

Every read goes to the file, so a key set in another process is seen at once. Use get_config_manager for the shared instance.

Examples:

>>> manager = ConfigManager()
>>> manager.set_api_key("example", "not-a-real-key")
>>> manager.get_api_key("example")
'not-a-real-key'
>>> manager.remove_api_key("example")
True
Source code in src/provesid/config.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
class ConfigManager:
    """
    Manages persistent configuration for PROVESID.

    Every read goes to the file, so a key set in another process is seen at
    once. Use [`get_config_manager`][provesid.config.get_config_manager] for
    the shared instance.

    Examples:
        >>> manager = ConfigManager()
        >>> manager.set_api_key("example", "not-a-real-key")
        >>> manager.get_api_key("example")
        'not-a-real-key'
        >>> manager.remove_api_key("example")
        True
    """

    def __init__(self):
        """
        Initialize configuration manager with default paths.

        Creates the configuration directory if it is missing; a failure to
        create it is logged, not raised.

        Examples:
            >>> ConfigManager().config_dir.name in ("provesid", "PROVESID")
            True
        """
        self.config_dir = self._get_config_directory()
        self.config_file = self.config_dir / "config.json"
        self._ensure_config_directory()

    def _get_config_directory(self) -> Path:
        """Get the appropriate configuration directory for the current OS"""
        if os.name == 'nt':  # Windows
            config_root = Path(os.environ.get('APPDATA', os.path.expanduser('~'))) / 'PROVESID'
        else:  # Unix-like (Linux, macOS)
            config_root = Path(os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))) / 'provesid'

        return config_root

    def _ensure_config_directory(self):
        """Create configuration directory if it doesn't exist"""
        try:
            self.config_dir.mkdir(parents=True, exist_ok=True)
        except Exception as e:
            logging.warning(f"Could not create config directory {self.config_dir}: {e}")

    def load_config(self) -> Dict[str, Any]:
        """
        Load configuration from file.

        Returns:
            (dict): The file's contents; empty when there is no file or it cannot
            be read (the latter logged).

        Examples:
            >>> isinstance(ConfigManager().load_config(), dict)
            True
        """
        if not self.config_file.exists():
            return {}

        try:
            with open(self.config_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception as e:
            logging.warning(f"Error loading config from {self.config_file}: {e}")
            return {}

    def save_config(self, config: Dict[str, Any]):
        """
        Save configuration to file, replacing what is there.

        Args:
            config: The whole configuration. Keys not in it are lost; to change
                one entry, load, modify and save.

        Note:
            A failure to write is logged at ERROR, not raised.

        Examples:
            >>> manager = ConfigManager()
            >>> config = manager.load_config()
            >>> manager.save_config(config)
        """
        try:
            with open(self.config_file, 'w', encoding='utf-8') as f:
                json.dump(config, f, indent=2, ensure_ascii=False)
        except Exception as e:
            logging.error(f"Error saving config to {self.config_file}: {e}")

    def get_api_key(self, service: str) -> Optional[str]:
        """
        Get API key for a specific service.

        Args:
            service: Service name, e.g. ``"cas"``.

        Returns:
            The stored key, or None when there is none.

        Examples:
            >>> ConfigManager().get_api_key("no-such-service") is None
            True
        """
        config = self.load_config()
        api_keys = config.get('api_keys', {})
        return api_keys.get(service)

    def set_api_key(self, service: str, api_key: str):
        """
        Set API key for a specific service, replacing any stored one.

        Args:
            service: Service name, e.g. ``"cas"``.
            api_key: The key; surrounding whitespace is stripped.

        Examples:
            >>> manager = ConfigManager()
            >>> manager.set_api_key("example", "  not-a-real-key  ")
            >>> manager.get_api_key("example")
            'not-a-real-key'
            >>> manager.remove_api_key("example")
            True
        """
        config = self.load_config()
        if 'api_keys' not in config:
            config['api_keys'] = {}

        config['api_keys'][service] = api_key.strip()
        self.save_config(config)
        logging.info(f"API key saved for service: {service}")

    def remove_api_key(self, service: str) -> bool:
        """
        Remove API key for a specific service.

        Args:
            service: Service name.

        Returns:
            True if a key was removed, False if none was stored.

        Examples:
            >>> ConfigManager().remove_api_key("no-such-service")
            False
        """
        config = self.load_config()
        api_keys = config.get('api_keys', {})

        if service in api_keys:
            del api_keys[service]
            config['api_keys'] = api_keys
            self.save_config(config)
            logging.info(f"API key removed for service: {service}")
            return True
        return False

    def list_configured_services(self) -> list:
        """
        List all services with configured API keys.

        Returns:
            (list): Service names, in the order they were first stored.

        Examples:
            >>> manager = ConfigManager()
            >>> manager.set_api_key("example", "not-a-real-key")
            >>> "example" in manager.list_configured_services()
            True
            >>> manager.remove_api_key("example")
            True
        """
        config = self.load_config()
        api_keys = config.get('api_keys', {})
        return list(api_keys.keys())

    def get_config_info(self) -> Dict[str, Any]:
        """
        Get information about the configuration.

        Returns:
            (dict): ``config_directory``, ``config_file``, ``config_exists`` and
            ``configured_services``. The keys themselves are not included.

        Examples:
            >>> sorted(ConfigManager().get_config_info())
            ['config_directory', 'config_exists', 'config_file', 'configured_services']
        """
        return {
            'config_directory': str(self.config_dir),
            'config_file': str(self.config_file),
            'config_exists': self.config_file.exists(),
            'configured_services': self.list_configured_services()
        }
Methods:
__init__()

Initialize configuration manager with default paths.

Creates the configuration directory if it is missing; a failure to create it is logged, not raised.

Examples:

>>> ConfigManager().config_dir.name in ("provesid", "PROVESID")
True
Source code in src/provesid/config.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def __init__(self):
    """
    Initialize configuration manager with default paths.

    Creates the configuration directory if it is missing; a failure to
    create it is logged, not raised.

    Examples:
        >>> ConfigManager().config_dir.name in ("provesid", "PROVESID")
        True
    """
    self.config_dir = self._get_config_directory()
    self.config_file = self.config_dir / "config.json"
    self._ensure_config_directory()
load_config()

Load configuration from file.

Returns:

Type Description
dict

The file's contents; empty when there is no file or it cannot be read (the latter logged).

Examples:

>>> isinstance(ConfigManager().load_config(), dict)
True
Source code in src/provesid/config.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def load_config(self) -> Dict[str, Any]:
    """
    Load configuration from file.

    Returns:
        (dict): The file's contents; empty when there is no file or it cannot
        be read (the latter logged).

    Examples:
        >>> isinstance(ConfigManager().load_config(), dict)
        True
    """
    if not self.config_file.exists():
        return {}

    try:
        with open(self.config_file, 'r', encoding='utf-8') as f:
            return json.load(f)
    except Exception as e:
        logging.warning(f"Error loading config from {self.config_file}: {e}")
        return {}
save_config(config)

Save configuration to file, replacing what is there.

Parameters:

Name Type Description Default
config Dict[str, Any]

The whole configuration. Keys not in it are lost; to change one entry, load, modify and save.

required
Note

A failure to write is logged at ERROR, not raised.

Examples:

>>> manager = ConfigManager()
>>> config = manager.load_config()
>>> manager.save_config(config)
Source code in src/provesid/config.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def save_config(self, config: Dict[str, Any]):
    """
    Save configuration to file, replacing what is there.

    Args:
        config: The whole configuration. Keys not in it are lost; to change
            one entry, load, modify and save.

    Note:
        A failure to write is logged at ERROR, not raised.

    Examples:
        >>> manager = ConfigManager()
        >>> config = manager.load_config()
        >>> manager.save_config(config)
    """
    try:
        with open(self.config_file, 'w', encoding='utf-8') as f:
            json.dump(config, f, indent=2, ensure_ascii=False)
    except Exception as e:
        logging.error(f"Error saving config to {self.config_file}: {e}")
get_api_key(service)

Get API key for a specific service.

Parameters:

Name Type Description Default
service str

Service name, e.g. "cas".

required

Returns:

Type Description
Optional[str]

The stored key, or None when there is none.

Examples:

>>> ConfigManager().get_api_key("no-such-service") is None
True
Source code in src/provesid/config.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def get_api_key(self, service: str) -> Optional[str]:
    """
    Get API key for a specific service.

    Args:
        service: Service name, e.g. ``"cas"``.

    Returns:
        The stored key, or None when there is none.

    Examples:
        >>> ConfigManager().get_api_key("no-such-service") is None
        True
    """
    config = self.load_config()
    api_keys = config.get('api_keys', {})
    return api_keys.get(service)
set_api_key(service, api_key)

Set API key for a specific service, replacing any stored one.

Parameters:

Name Type Description Default
service str

Service name, e.g. "cas".

required
api_key str

The key; surrounding whitespace is stripped.

required

Examples:

>>> manager = ConfigManager()
>>> manager.set_api_key("example", "  not-a-real-key  ")
>>> manager.get_api_key("example")
'not-a-real-key'
>>> manager.remove_api_key("example")
True
Source code in src/provesid/config.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def set_api_key(self, service: str, api_key: str):
    """
    Set API key for a specific service, replacing any stored one.

    Args:
        service: Service name, e.g. ``"cas"``.
        api_key: The key; surrounding whitespace is stripped.

    Examples:
        >>> manager = ConfigManager()
        >>> manager.set_api_key("example", "  not-a-real-key  ")
        >>> manager.get_api_key("example")
        'not-a-real-key'
        >>> manager.remove_api_key("example")
        True
    """
    config = self.load_config()
    if 'api_keys' not in config:
        config['api_keys'] = {}

    config['api_keys'][service] = api_key.strip()
    self.save_config(config)
    logging.info(f"API key saved for service: {service}")
remove_api_key(service)

Remove API key for a specific service.

Parameters:

Name Type Description Default
service str

Service name.

required

Returns:

Type Description
bool

True if a key was removed, False if none was stored.

Examples:

>>> ConfigManager().remove_api_key("no-such-service")
False
Source code in src/provesid/config.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def remove_api_key(self, service: str) -> bool:
    """
    Remove API key for a specific service.

    Args:
        service: Service name.

    Returns:
        True if a key was removed, False if none was stored.

    Examples:
        >>> ConfigManager().remove_api_key("no-such-service")
        False
    """
    config = self.load_config()
    api_keys = config.get('api_keys', {})

    if service in api_keys:
        del api_keys[service]
        config['api_keys'] = api_keys
        self.save_config(config)
        logging.info(f"API key removed for service: {service}")
        return True
    return False
list_configured_services()

List all services with configured API keys.

Returns:

Type Description
list

Service names, in the order they were first stored.

Examples:

>>> manager = ConfigManager()
>>> manager.set_api_key("example", "not-a-real-key")
>>> "example" in manager.list_configured_services()
True
>>> manager.remove_api_key("example")
True
Source code in src/provesid/config.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def list_configured_services(self) -> list:
    """
    List all services with configured API keys.

    Returns:
        (list): Service names, in the order they were first stored.

    Examples:
        >>> manager = ConfigManager()
        >>> manager.set_api_key("example", "not-a-real-key")
        >>> "example" in manager.list_configured_services()
        True
        >>> manager.remove_api_key("example")
        True
    """
    config = self.load_config()
    api_keys = config.get('api_keys', {})
    return list(api_keys.keys())
get_config_info()

Get information about the configuration.

Returns:

Type Description
dict

config_directory, config_file, config_exists and configured_services. The keys themselves are not included.

Examples:

>>> sorted(ConfigManager().get_config_info())
['config_directory', 'config_exists', 'config_file', 'configured_services']
Source code in src/provesid/config.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def get_config_info(self) -> Dict[str, Any]:
    """
    Get information about the configuration.

    Returns:
        (dict): ``config_directory``, ``config_file``, ``config_exists`` and
        ``configured_services``. The keys themselves are not included.

    Examples:
        >>> sorted(ConfigManager().get_config_info())
        ['config_directory', 'config_exists', 'config_file', 'configured_services']
    """
    return {
        'config_directory': str(self.config_dir),
        'config_file': str(self.config_file),
        'config_exists': self.config_file.exists(),
        'configured_services': self.list_configured_services()
    }

Functions:

get_config_manager()

Get the global configuration manager instance.

Built on first call; the same object every time after.

Returns:

Type Description
ConfigManager

The shared instance.

Examples:

>>> get_config_manager() is get_config_manager()
True
Source code in src/provesid/config.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
def get_config_manager() -> ConfigManager:
    """
    Get the global configuration manager instance.

    Built on first call; the same object every time after.

    Returns:
        (ConfigManager): The shared instance.

    Examples:
        >>> get_config_manager() is get_config_manager()
        True
    """
    global _config_manager
    if _config_manager is None:
        _config_manager = ConfigManager()
    return _config_manager

set_cas_api_key(api_key)

Set CAS Common Chemistry API key for persistent storage

CASCommonChem uses the stored key from then on. The file's path is logged at INFO.

Parameters:

Name Type Description Default
api_key str

Your CAS API key

required

Returns:

Type Description
Path

The config file the key was written to.

Note

This replaces any key already stored, without asking.

Examples:

>>> from provesid.config import set_cas_api_key
>>> set_cas_api_key("your-cas-api-key-here")
PosixPath('/home/me/.config/provesid/config.json')
Source code in src/provesid/config.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
def set_cas_api_key(api_key: str) -> Path:
    """
    Set CAS Common Chemistry API key for persistent storage

    [`CASCommonChem`][provesid.cascommonchem.CASCommonChem] uses the stored
    key from then on. The file's path is logged at INFO.

    Args:
        api_key: Your CAS API key

    Returns:
        (Path): The config file the key was written to.

    Note:
        This replaces any key already stored, without asking.

    Examples:
        >>> from provesid.config import set_cas_api_key
        >>> set_cas_api_key("your-cas-api-key-here")    # doctest: +SKIP
        PosixPath('/home/me/.config/provesid/config.json')
    """
    config_mgr = get_config_manager()
    config_mgr.set_api_key('cas', api_key)
    logger.info("CAS API key saved to %s; CASCommonChem() will use it", config_mgr.config_file)
    return config_mgr.config_file

get_cas_api_key()

Get the stored CAS API key.

Only the config file is read; the CAS_API_KEY environment variable, which CASCommonChem consults after this file, is not.

Returns:

Type Description
Optional[str]

The key, or None when none is stored.

Examples:

>>> key = get_cas_api_key()
>>> key is None or isinstance(key, str)
True
Source code in src/provesid/config.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def get_cas_api_key() -> Optional[str]:
    """
    Get the stored CAS API key.

    Only the config file is read; the ``CAS_API_KEY`` environment variable,
    which [`CASCommonChem`][provesid.cascommonchem.CASCommonChem] consults after this
    file, is not.

    Returns:
        The key, or None when none is stored.

    Examples:
        >>> key = get_cas_api_key()
        >>> key is None or isinstance(key, str)
        True
    """
    return get_config_manager().get_api_key('cas')

remove_cas_api_key()

Remove the stored CAS API key.

Returns:

Type Description
bool

True if a key was stored and is now gone, False if none was stored.

Examples:

>>> remove_cas_api_key()
True
Source code in src/provesid/config.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def remove_cas_api_key() -> bool:
    """
    Remove the stored CAS API key.

    Returns:
        True if a key was stored and is now gone, False if none was stored.

    Examples:
        >>> remove_cas_api_key()                          # doctest: +SKIP
        True
    """
    removed = get_config_manager().remove_api_key('cas')
    if removed:
        logger.info("CAS API key removed")
    else:
        logger.info("No CAS API key was configured")
    return removed

show_config()

The configuration's location and which services have keys.

The same as get_config_manager().get_config_info(). The keys themselves are not included.

Returns:

Type Description
dict

config_directory, config_file, config_exists and configured_services.

Examples:

>>> show_config()
{'config_directory': '/home/me/.config/provesid',
 'config_file': '/home/me/.config/provesid/config.json',
 'config_exists': True,
 'configured_services': ['cas']}
Source code in src/provesid/config.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def show_config() -> Dict[str, Any]:
    """
    The configuration's location and which services have keys.

    The same as ``get_config_manager().get_config_info()``. The keys
    themselves are not included.

    Returns:
        (dict): ``config_directory``, ``config_file``, ``config_exists`` and
        ``configured_services``.

    Examples:
        >>> show_config()                                 # doctest: +SKIP
        {'config_directory': '/home/me/.config/provesid',
         'config_file': '/home/me/.config/provesid/config.json',
         'config_exists': True,
         'configured_services': ['cas']}
    """
    return get_config_manager().get_config_info()