1
1
mirror of https://github.com/chubin/cheat.sh.git synced 2024-12-04 03:32:05 +03:00
cheat.sh/lib/config.py
2020-05-29 12:39:16 -04:00

265 lines
7.9 KiB
Python

"""
Global configuration of the project.
All configurable parameters are stored in the global variable CONFIG,
the only variable which is exported from the module.
Default values of all configuration parameters are specified
in the `_CONFIG` dictionary. Those parameters can be overridden
by three means:
* config file `etc/config.yaml` located in the work dir
* config file `etc/config.yaml` located in the project dir
(if the work dir and the project dir are not the same)
* environment variables prefixed with `CHEATSH_`
Configuration placement priorities, from high to low:
* environment variables;
* configuration file in the workdir
* configuration file in the project dir
* default values specified in the `_CONFIG` dictionary
If the work dir and the project dir are not the same, we do not
recommend that you use the config file located in the project dir,
except the cases when you use your own cheat.sh fork, and thus
configuration is a part of the project repository.
In all other cases `WORKDIR/etc/config.yaml` should be preferred.
Location of this config file can be overridden by the `CHEATSH_PATH_CONFIG`
environment variable.
Configuration parameters set by environment variables are mapped
in this way:
* CHEATSH_ prefix is trimmed
* _ replaced with .
* the string is lowercased
For instance, an environment variable named `CHEATSH_SERVER_PORT`
specifies the value for the `server.port` configuration parameter.
Only parameters that imply scalar values (integer or string)
can be set using environment variables, for the rest config files
should be used. If a parameter implies an integer, and the value
specified by an environment variable is not an integer, it is ignored.
"""
from __future__ import print_function
import os
from pygments.styles import get_all_styles
#def get_all_styles():
# return []
_ENV_VAR_PREFIX = "CHEATSH"
_MYDIR = os.path.abspath(os.path.join(__file__, '..', '..'))
def _config_locations():
"""
Return three possible config locations
where configuration can be found:
* `_WORKDIR`, `_CONF_FILE_WORKDIR`, `_CONF_FILE_MYDIR`
"""
var = _ENV_VAR_PREFIX + '_PATH_WORKDIR'
workdir = os.environ[var] if var in os.environ \
else os.path.join(os.environ['HOME'], '.cheat.sh')
var = _ENV_VAR_PREFIX + '_CONFIG'
conf_file_workdir = os.environ[var] if var in os.environ \
else os.path.join(workdir, 'etc/config.yaml')
conf_file_mydir = os.path.join(_MYDIR, 'etc/config.yaml')
return workdir, conf_file_workdir, conf_file_mydir
_WORKDIR, _CONF_FILE_WORKDIR, _CONF_FILE_MYDIR = _config_locations()
_CONFIG = {
"adapters.active": [
"tldr",
"cheat",
"fosdem",
"translation",
"rosetta",
"late.nz",
"question",
"cheat.sheets",
"cheat.sheets dir",
"learnxiny",
"rfc",
"oeis",
"chmod",
],
"adapters.mandatory": [
"search",
],
"cache.redis.db": 0,
"cache.redis.host": "localhost",
"cache.redis.port": 6379,
"cache.redis.prefix": "",
"cache.type": "redis",
"frontend.styles": sorted(list(get_all_styles())),
"log.level": 4,
"path.internal.ansi2html": os.path.join(_MYDIR, "share/ansi2html.sh"),
"path.internal.bin": os.path.join(_MYDIR, "bin"),
"path.internal.bin.upstream": os.path.join(_MYDIR, "bin", "upstream"),
"path.internal.malformed": os.path.join(_MYDIR, "share/static/malformed-response.html"),
"path.internal.pages": os.path.join(_MYDIR, "share"),
"path.internal.static": os.path.join(_MYDIR, "share/static"),
"path.internal.templates": os.path.join(_MYDIR, "share/templates"),
"path.internal.vim": os.path.join(_MYDIR, "share/vim"),
"path.log.main": "log/main.log",
"path.log.queries": "log/queries.log",
"path.log.fetch": "log/fetch.log",
"path.repositories": "upstream",
"path.spool": "spool",
"path.workdir": _WORKDIR,
"routing.pre": [
("^$", "search"),
("^[^/]*/rosetta(/|$)", "rosetta"),
("^rfc/", "rfc"),
("^oeis/", "oeis"),
("^chmod/", "chmod"),
("^:", "internal"),
("/:list$", "internal"),
("/$", "cheat.sheets dir"),
],
"routing.main": [
("", "cheat.sheets"),
("", "cheat"),
("", "tldr"),
("", "late.nz"),
("", "fosdem"),
("", "learnxiny"),
],
"routing.post": [
("^[^/ +]*$", "unknown"),
("^[a-z][a-z]-[a-z][a-z]$", "translation"),
],
"routing.default": "question",
"upstream.url": "https://cheat.sh",
"upstream.timeout": 5,
"search.limit": 20,
"server.bind": "0.0.0.0",
"server.port": 8002,
}
class Config(dict):
"""
configuration dictionary that handles relative
paths properly (making them relative to path.workdir)
"""
def _absolute_path(self, val):
if val.startswith('/'):
return val
return os.path.join(self['path.workdir'], val)
def __init__(self, *args, **kwargs):
dict.__init__(self)
self.update(*args, **kwargs)
def __setitem__(self, key, val):
if key.startswith('path.') and not val.startswith('/'):
val = self._absolute_path(val)
dict.__setitem__(self, key, val)
def update(self, *args, **kwargs):
"""
the built-in __init__ doesn't call update,
and the built-in update doesn't call __setitem__,
so `update` should be overridden
"""
newdict = dict(*args, **kwargs)
if 'path.workdir' in newdict:
self['path.workdir'] = newdict['path.workdir']
for key, val in newdict.items():
self[key] = val
def _load_config_from_environ(config):
update = {}
for key, val in config.items():
if not isinstance(val, str) or isinstance(val, int):
continue
env_var = _ENV_VAR_PREFIX + '_' + key.replace('.', '_').upper()
if not env_var in os.environ:
continue
env_val = os.environ[env_var]
if isinstance(val, int):
try:
env_val = int(env_val)
except (ValueError, TypeError):
continue
update[key] = env_val
return update
def _get_nested(data, key):
"""
Return value for a hierrachical key (like a.b.c).
Return None if nothing found.
If there is a key with . in the name, and a subdictionary,
the former is preferred:
>>> print(_get_nested({'a.b': 10, 'a':{'b': 20}}, 'a.b'))
10
>>> print(_get_nested({'a': {'b': 20}}, 'a.b'))
20
>>> print(_get_nested({'a': {'b': {'c': 30}}}, 'a.b.c'))
30
"""
if not data or not isinstance(data, dict):
return None
if '.' not in key:
return data.get(key)
if key in data:
return data[key]
parts = key.split('.')
for i in range(len(parts))[::-1]:
prefix = ".".join(parts[:i])
if prefix in data:
return _get_nested(data[prefix], ".".join(parts[i:]))
return None
def _load_config_from_file(default_config, filename):
import yaml
update = {}
if not os.path.exists(filename):
return update
with open(filename) as f:
newconfig = yaml.load(f.read(), Loader=yaml.SafeLoader)
for key, val in default_config.items():
newval = _get_nested(newconfig, key)
if newval is None:
continue
if isinstance(val, int):
try:
newval = int(newval)
except (ValueError, TypeError):
continue
update[key] = newval
return update
CONFIG = Config()
CONFIG.update(_CONFIG)
CONFIG.update(_load_config_from_file(_CONFIG, _CONF_FILE_MYDIR))
if _CONF_FILE_WORKDIR != _CONF_FILE_MYDIR:
CONFIG.update(_load_config_from_file(_CONFIG, _CONF_FILE_WORKDIR))
CONFIG.update(_load_config_from_environ(_CONFIG))
if __name__ == "__main__":
import doctest
doctest.testmod()