from typing import Dict
from dataclasses import dataclass, fields
import json
import configparser
class ConfigError(Exception):
pass
@dataclass(frozen=True)
class Config:
"""
Configuration file, parsed from either a single .ini section or a .json dictionary.
base_url: URL of the instance, gets the API calls appended, for example: 'https://my.nextcloud.com/'
username: Username to login with.
password: Password to login with, using an 'App Password' instead of the main account password should be considered.
password_type: Either 'plain' or 'openssl-aes', the latter for an encrypted password in the config. Encrypt with:
`echo -n "nextcloud-password" | openssl enc -e -aes-256-cbc -salt -pbkdf2 -base64`
This prompts for an encryption password that then needs to be given to the CLI at startup.
verify_ssl: Verify the SSL certificate of an HTTPS API URL, enabled per default.
"""
base_url: str
username: str
password: str
password_type: str = 'plain'
verify_ssl: bool = True
def __post_init__(self) -> None:
for f in fields(self):
if not isinstance(getattr(self, f.name), f.type):
if f.type is bool:
super().__setattr__(f.name, self._to_bool(getattr(self, f.name)))
else:
raise ConfigError(f"Invalid '{f.name}' type")
if not self.base_url.startswith(("http://", "https://")):
raise ConfigError(f"Invalid URL '{self.base_url}'")
if self.password_type not in {"plain", "openssl-aes"}:
raise ConfigError(f"Invalid password type '{self.password_type}'")
@classmethod
def _to_bool(cls, value) -> bool:
try:
return configparser.RawConfigParser.BOOLEAN_STATES[value]
except (ValueError, KeyError):
raise ConfigError(f"Invalid boolean value '{value}'")
@classmethod
def from_dict(cls, config: Dict) -> 'Config':
if not isinstance(config, dict):
raise ConfigError("Expecting config dict")
try:
return cls(**config)
except TypeError as e:
raise ConfigError(str(e)) from None
@classmethod
def from_json(cls, filename: str) -> 'Config':
try:
with open(filename, "r") as fp:
return cls.from_dict(json.load(fp))
except (OSError, json.JSONDecodeError, UnicodeDecodeError) as e:
raise ConfigError(str(e)) from None
@classmethod
def from_ini(cls, filename: str) -> 'Config':
parser: configparser.ConfigParser = configparser.ConfigParser()
try:
with open(filename, "r") as fp:
parser.read_file(fp)
except (OSError, configparser.Error, UnicodeDecodeError) as e:
raise ConfigError(str(e)) from None
if len(parser.sections()) != 1:
raise ConfigError("Expecting single ini section")
else:
return cls.from_dict({k: v for k, v in parser.items(parser.sections()[0])})
@classmethod
def from_file(cls, filename: str) -> 'Config':
if filename.endswith('.json'):
return cls.from_json(filename)
else:
return cls.from_ini(filename)