Files
dougal-software/bin/configuration.py
D. Berge 25ab623328 Add functions for translating paths.
The Dougal database will no longer store physical file paths
but rather logical ones, relative to (config.yaml).imports.paths.

These functions translate between physical and logical paths.
2023-08-30 14:17:47 +02:00

161 lines
5.1 KiB
Python

import os
import pathlib
from glob import glob
from yaml import full_load as _load
"""
Interface to the instance configuration.
The instance configuration is expected to be found under
$HOME/etc/config.yaml and the configuration for the individual
surveys should be under $HOME/etc/surveys/*.yaml. In both cases,
$HOME is the home directory of the user running this script.
"""
prefix = os.environ.get("DOUGAL_ROOT", os.environ.get("HOME", ".")+"/software")
DOUGAL_ROOT = os.environ.get("DOUGAL_ROOT", os.environ.get("HOME", ".")+"/software")
VARDIR = os.environ.get("VARDIR", DOUGAL_ROOT+"/var")
LOCKFILE = os.environ.get("LOCKFILE", VARDIR+"/runner.lock")
def vars ():
return {
"DOUGAL_ROOT": DOUGAL_ROOT,
"VARDIR": VARDIR,
"LOCKFILE": LOCKFILE
}
def read (file = None):
if file is None:
file = prefix+"/etc/config.yaml"
with open(file, "r") as fd:
cfg = _load(fd)
if "db" in cfg and "connection_string" in cfg["db"] and os.environ.get("PGDATABASE"):
# This should cause the PostgreSQL library to use the environment variables
cfg["db"]["connection_string"] = ""
print("Using environment variables for database connection")
return cfg
def files (globspec = None, include_archived = False):
"""
Read and parse survey configuration files.
Arguments:
globspec (string): a glob spec matching the selection of files to read.
If not provided, a default value will be used.
include_archived (bool): whether to include surveys marked as archived or
not. Defaults to ignoring archived surveys.
Note that file names starting with `.` or `_` (dot or underscore) will
never be read (and there is no option to override this). Intended for
quickly and temporarily “disabling” a survey configuration by renaming
the relevant file.
"""
tuples = []
if globspec is None:
globspec = prefix+'/etc/surveys/*.yaml'
for filepath in glob(globspec):
filepath = os.path.abspath(filepath)
if os.path.basename(filepath).startswith("_") or os.path.basename(filepath).startswith("."):
continue
survey = read(filepath)
if not include_archived and "archived" in survey and survey["archived"] is not False:
continue
tuples.append((filepath, survey))
return tuples
def surveys (globspec = None, include_archived = False):
return [i[1] for i in files(globspec, include_archived)]
def rxflags (flagstr):
"""
Convert flags string into a Python flags argument.
"""
flags = 0
cases = {
"i": re.I
}
for flag in flagstr:
flags |= cases.get(flag, 0)
return flags
def translate_path (file):
"""
Translate a path from a Dougal import directory to an actual
physical path on disk.
Any user files accessible by Dougal must be under a path prefixed
by `(config.yaml).imports.paths`. The value of `imports.paths` may
be either a string, in which case this represents the prefix under
which all Dougal data resides, or a dictionary where the keys are
logical paths and their values the corresponding physical path.
"""
cfg = read()
root = pathlib.Path(DOUGAL_ROOT)
filepath = pathlib.Path(file).resolve()
import_paths = cfg["imports"]["paths"]
if filepath.is_absolute():
if type(import_paths) == str:
# Substitute the root for the real physical path
# NOTE: `root` deals with import_paths not being absolute
prefix = root.joinpath(pathlib.Path(import_paths)).resolve()
return str(pathlib.Path(prefix).joinpath(*filepath.parts[2:]))
else:
# Look for a match on the second path element
if filepath.parts[1] in import_paths:
# NOTE: `root` deals with import_paths[…] not being absolute
prefix = root.joinpath(import_paths[filepath.parts[1]])
return str(pathlib.Path(prefix).joinpath(*filepath.parts[2:]))
else:
# This path is invalid
raise TypeError("invalid path or file: {0!r}".format(filepath))
else:
# A relative filepath is always resolved relative to the logical root
root = pathlib.Path("/")
return translate_path(root.joinpath(filepath))
def untranslate_path (file):
"""
Attempt to convert a physical path into a logical one.
See `translate_path()` above for details.
"""
cfg = read()
dougal_root = pathlib.Path(DOUGAL_ROOT)
filepath = pathlib.Path(file).resolve()
import_paths = cfg["imports"]["paths"]
physical_root = pathlib.Path("/")
if filepath.is_absolute():
if type(import_paths) == str:
if filepath.is_relative_to(import_paths):
physical_root = pathlib.Path("/")
physical_prefix = pathlib.Path(import_paths)
return str(root.joinpath(filepath.relative_to(physical_prefix)))
else:
raise TypeError("invalid path or file: {0!r}".format(filepath))
else:
for key, value in import_paths.items():
value = dougal_root.joinpath(value)
physical_prefix = pathlib.Path(value)
if filepath.is_relative_to(physical_prefix):
logical_prefix = physical_root.joinpath(pathlib.Path(key)).resolve()
return str(logical_prefix.joinpath(filepath.relative_to(physical_prefix)))
# If we got here with no matches, this is not a valid
# Dougal data path
raise TypeError("invalid path or file: {0!r}".format(filepath))
else:
# A relative filepath is always resolved relative to DOUGAL_ROOT
return untranslate_path(root.joinpath(filepath))