Source code for cosapp.core.config
"""
Configuration of CoSApp.
"""
import os
import json
try:
from json import JSONDecodeError
except ImportError:
JSONDecodeError = ValueError
import logging
from pathlib import Path
from typing import FrozenSet, Union
import jsonschema
logger = logging.getLogger(__name__)
[docs]
class CoSAppConfiguration:
r"""Encapsulate CoSApp configuration parameters and its handlers.
By default, CoSApp configuration folder is stored in
- `$HOME/.cosapp.d` (Linux)
- `%USERPROFILE%\.cosapp.d` (MS Windows)
This default configuration folder is overwritten by the environment
variable ``COSAPP_CONFIG_DIR``.
"""
COSAPP_CONFIG_DIR = ".cosapp.d"
CONFIG_FILE = "cosapp_config.json"
def __init__(self) -> None:
"""Constructor"""
self._userid = ""
self._roles: FrozenSet[FrozenSet[str]] = frozenset()
self.__load_configuration()
def __load_configuration(self) -> None:
"""Read configuration from file or generate the default.
Raises
------
OSError
If the current platform is not recognized
"""
fullpath = self.get_config_filename()
ok = False
if os.path.isfile(fullpath): # Load local parameters - offline connection
try:
parameters = self.validate_file(fullpath)
except (OSError, JSONDecodeError):
ok = False
else:
ok = True
self._userid = parameters["userid"]
self._roles = frozenset(map(frozenset, parameters["roles"]))
if not ok:
logger.warn(
f"Configuration file `{fullpath!s}` cannot be opened; fall back to default."
)
try:
self.update_userid()
except OSError:
self._userid = unknwon_id = "UNKNOWN"
self._roles = frozenset()
logger.warn(
f"Unable to determine user ID; fall back to {unknwon_id!r}."
)
# Try to update user permission from official root
self.update_configuration()
[docs]
@staticmethod
def get_config_dir() -> Path:
try:
return Path(os.environ["COSAPP_CONFIG_DIR"])
except KeyError:
return Path.home().joinpath(CoSAppConfiguration.COSAPP_CONFIG_DIR)
[docs]
@classmethod
def get_config_filename(cls) -> Path:
config_dir = cls.get_config_dir()
return config_dir.joinpath(cls.CONFIG_FILE)
@property
def userid(self) -> str:
"""str : User ID"""
return self._userid
@property
def roles(self) -> FrozenSet[FrozenSet[str]]:
"""FrozenSet[FrozenSet[str]] : Roles assigned to user."""
return self._roles
[docs]
@staticmethod
def config_schema() -> dict:
"""Static method returning the JSON validation schema of the class."""
path = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(path , "configuration_schema.json"), "r") as fp:
config_schema = json.load(fp)
return config_schema
[docs]
@classmethod
def validate_file(cls, filename: Union[str, Path]) -> dict:
"""Validate the provided file against JSON schema for configuration file.
Parameters
----------
filename : str or Path
Absolute path to the file to be tested.
Returns
-------
dict
The dictionary read in the validated file
Raises
------
`OSError` (and derived exceptions)
If a problem occurs while opening the file.
`jsonschema.exceptions.ValidationError`
If the provided file does not conform to the JSON schema.
"""
with open(filename, "r") as fp:
params = json.load(fp)
jsonschema.validate(params, cls.config_schema())
return params
[docs]
def update_userid(self) -> None:
"""Update current user id.
"""
candidates = iter(["USERNAME", "USER"])
old_id = self._userid
while not self._userid:
try:
self._userid = os.environ.get(next(candidates), "")
except StopIteration:
raise OSError("Unable to determine user ID.")
if self._userid != old_id:
logger.info(f"User ID changed from {old_id!r} to {self._userid!r}.")
[docs]
def update_configuration(self) -> None:
"""Update current user configuration file.
"""
# TODO - request reference roles source here
# Save back changes following the update
parameters = {
"userid": self.userid,
"roles": list(map(list, self.roles)),
}
try:
config_file = self.get_config_filename()
config_dir = os.path.dirname(config_file)
if not os.path.isdir(config_dir):
os.makedirs(config_dir)
with open(config_file, "w") as file:
json.dump(parameters, file, sort_keys=True)
except OSError:
logger.warning("Failed to save configuration locally.")