Coretex
base.py
1 # Copyright (C) 2023 Coretex LLC
2 
3 # This file is part of Coretex.ai
4 
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Affero General Public License for more details.
14 
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <https://www.gnu.org/licenses/>.
17 
18 from typing import Dict, Any, Optional, Type, TypeVar, List, Tuple
19 from typing_extensions import Self
20 from abc import abstractmethod
21 from pathlib import Path
22 
23 import os
24 import json
25 
26 
27 T = TypeVar("T", int, float, str, bool)
28 
29 CONFIG_DIR = Path.home().joinpath(".config", "coretex")
30 DEFAULT_VENV_PATH = CONFIG_DIR / "venv"
31 
32 
33 class InvalidConfiguration(Exception):
34 
35  def __init__(self, message: str, errors: List[str]) -> None:
36  super().__init__(message)
37 
38  self.errors = errors
39 
40 
41 class ConfigurationNotFound(Exception):
42  pass
43 
44 
45 class BaseConfiguration:
46 
47  def __init__(self, raw: Dict[str, Any]) -> None:
48  self._raw = raw
49 
50  @classmethod
51  @abstractmethod
52  def getConfigPath(cls) -> Path:
53  pass
54 
55  @abstractmethod
56  def _isConfigValid(self) -> Tuple[bool, List[str]]:
57  pass
58 
59  @classmethod
60  def load(cls) -> Self:
61  configPath = cls.getConfigPath()
62  if not configPath.exists():
63  raise ConfigurationNotFound(f"Configuration not found at path: {configPath}")
64 
65  with configPath.open("r") as file:
66  raw = json.load(file)
67 
68  config = cls(raw)
69 
70  isValid, errors = config._isConfigValid()
71  if not isValid:
72  raise InvalidConfiguration("Invalid configuration found.", errors)
73 
74  return config
75 
76  def _value(self, configKey: str, valueType: Type[T], envKey: Optional[str] = None) -> Optional[T]:
77  if envKey is not None and envKey in os.environ:
78  return valueType(os.environ[envKey])
79 
80  return self._raw.get(configKey)
81 
82  def getValue(self, configKey: str, valueType: Type[T], envKey: Optional[str] = None, default: Optional[T] = None) -> T:
83  value = self._value(configKey, valueType, envKey)
84 
85  if value is None:
86  value = default
87 
88  if not isinstance(value, valueType):
89  raise TypeError(f"Invalid {configKey} type \"{type(value)}\", expected: \"{valueType.__name__}\".")
90 
91  return value
92 
93  def getOptValue(self, configKey: str, valueType: Type[T], envKey: Optional[str] = None) -> Optional[T]:
94  value = self._value(configKey, valueType, envKey)
95 
96  if not isinstance(value, valueType):
97  return None
98 
99  return value
100 
101  def save(self) -> None:
102  configPath = self.getConfigPath()
103 
104  if not configPath.parent.exists():
105  configPath.parent.mkdir(parents = True, exist_ok = True)
106 
107  with configPath.open("w") as configFile:
108  json.dump(self._raw, configFile, indent = 4)
109 
110  def update(self, config: Self) -> None:
111  self._raw = config._raw