-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow configuration via environment variables #57
Open
hannahbast
wants to merge
4
commits into
main
Choose a base branch
from
add-config-command
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
from __future__ import annotations | ||
|
||
import shlex | ||
import subprocess | ||
from pathlib import Path | ||
|
||
import qlever.globals | ||
from qlever.command import QleverCommand | ||
from qlever.envvars import Envvars | ||
from qlever.log import log | ||
from qlever.util import get_random_string | ||
|
||
|
||
class ConfigCommand(QleverCommand): | ||
""" | ||
Class for executing the `config` command. | ||
""" | ||
|
||
def __init__(self): | ||
self.qleverfiles_path = Path(__file__).parent.parent / "Qleverfiles" | ||
self.qleverfile_names = \ | ||
[p.name.split(".")[1] | ||
for p in self.qleverfiles_path.glob("Qleverfile.*")] | ||
|
||
def description(self) -> str: | ||
return "Set up a Qleverfile or show the current configuration" | ||
|
||
def should_have_qleverfile(self) -> bool: | ||
return False | ||
|
||
def relevant_qleverfile_arguments(self) -> dict[str: list[str]]: | ||
return {} | ||
|
||
def additional_arguments(self, subparser) -> None: | ||
subparser.add_argument( | ||
"--get-qleverfile", type=str, | ||
choices=self.qleverfile_names, | ||
help="Get one the many pre-configured Qleverfiles") | ||
subparser.add_argument( | ||
"--show-qleverfile", action="store_true", | ||
default=False, | ||
help="Show the configuration from the Qleverfile " | ||
"(if it exists)") | ||
subparser.add_argument( | ||
"--show-envvars", action="store_true", default=False, | ||
help="Show all existing environment variables of the form " | ||
"QLEVER_SECTION_VARIABLE") | ||
subparser.add_argument( | ||
"--varname-width", type=int, default=25, | ||
help="Width for variable names in the output") | ||
subparser.add_argument( | ||
"--set-envvars-from-qleverfile", action="store_true", | ||
default=False, | ||
help="Set the environment variables that correspond to the " | ||
"Qleverfile configuration (for copying and pasting)") | ||
subparser.add_argument( | ||
"--unset-envvars", action="store_true", default=False, | ||
help="Unset all environment variables of the form " | ||
"QLEVER_SECTION_VARIABLE (for copying and pasting)") | ||
|
||
def execute(self, args) -> bool: | ||
# Show the configuration from the Qleverfile. | ||
if args.show_qleverfile: | ||
if qlever.globals.qleverfile_path is None: | ||
log.error("No Qleverfile found") | ||
return False | ||
if qlever.globals.qleverfile_config is None: | ||
log.error("Qleverfile found, but contains no configuration") | ||
return False | ||
self.show(f"Show the configuration from " | ||
f"{qlever.globals.qleverfile_path} (with any variables " | ||
f"on the right-hand side already substituted)", | ||
only_show=args.show) | ||
if args.show: | ||
return False | ||
else: | ||
print_empty_line_before_section = False | ||
for section, varname_and_values in \ | ||
qlever.globals.qleverfile_config.items(): | ||
if section == "DEFAULT": | ||
continue | ||
if print_empty_line_before_section: | ||
log.info("") | ||
print_empty_line_before_section = True | ||
log.info(f"[{section}]") | ||
for varname, value in varname_and_values.items(): | ||
log.info(f"{varname.upper():{args.varname_width}} = " | ||
f"{value}") | ||
return True | ||
|
||
# Show all environment variables of the form QLEVER_SECTION_VARIABLE. | ||
if args.show_envvars: | ||
self.show("Show all environment variables of the form " | ||
"QLEVER_SECTION_VARIABLE", only_show=args.show) | ||
if args.show: | ||
return False | ||
if qlever.globals.envvars_config is None: | ||
log.info("No environment variables found") | ||
else: | ||
for section, varname_and_values in \ | ||
qlever.globals.envvars_config.items(): | ||
for varname, value in varname_and_values.items(): | ||
var = Envvars.envvar_name(section, varname) | ||
log.info(f"{var:{args.varname_width+7}}" | ||
f" = {shlex.quote(value)}") | ||
return True | ||
|
||
# Show the environment variables that correspond to the Qleverfile. | ||
if args.set_envvars_from_qleverfile: | ||
if qlever.globals.qleverfile_path is None: | ||
log.error("No Qleverfile found") | ||
return False | ||
if qlever.globals.qleverfile_config is None: | ||
log.error("Qleverfile found, but contains no configuration") | ||
return False | ||
self.show("Show the environment variables that correspond to the " | ||
"Qleverfile configuration (for copying and pasting)", | ||
only_show=args.show) | ||
if args.show: | ||
return False | ||
else: | ||
for section, varname_and_values in \ | ||
qlever.globals.qleverfile_config.items(): | ||
if section == "DEFAULT": | ||
continue | ||
for varname, value in varname_and_values.items(): | ||
var = Envvars.envvar_name(section, varname) | ||
log.info(f"export {var}={shlex.quote(value)}") | ||
return True | ||
|
||
# Unset all environment variables of the form QLEVER_SECTION_VARIABLE. | ||
# Note that this cannot be done in this script because it would not | ||
# affect the shell calling this script. Instead, show the commands for | ||
# unsetting the environment variables to copy and paste. | ||
if args.unset_envvars: | ||
self.show("Unset all environment variables of the form " | ||
"QLEVER_SECTION_VARIABLE (for copying and pasting, " | ||
"this command cannot affect the shell from which you " | ||
" are calling it)", only_show=args.show) | ||
if args.show: | ||
return False | ||
if qlever.globals.envvars_config is None: | ||
log.info("No environment variables found") | ||
else: | ||
envvar_names = [] | ||
for section, varname_and_values in \ | ||
qlever.globals.envvars_config.items(): | ||
for varname, value in varname_and_values.items(): | ||
envvar_name = Envvars.envvar_name(section, varname) | ||
envvar_names.append(envvar_name) | ||
log.info(f"unset {' '.join(envvar_names)}") | ||
return True | ||
|
||
# Get one of the pre-configured Qleverfiles. | ||
if args.get_qleverfile: | ||
config_name = args.get_qleverfile | ||
preconfigured_qleverfile_path = \ | ||
self.qleverfiles_path / f"Qleverfile.{config_name}" | ||
random_string = get_random_string(12) | ||
setup_config_cmd = ( | ||
f"cat {preconfigured_qleverfile_path}" | ||
f" | sed -E 's/(^ACCESS_TOKEN.*)/\\1_{random_string}/'" | ||
f" > Qleverfile") | ||
self.show(setup_config_cmd, only_show=args.show) | ||
if args.show: | ||
return False | ||
|
||
# If there is already a Qleverfile in the current directory, exit. | ||
existing_qleverfile_path = Path("Qleverfile") | ||
if existing_qleverfile_path.exists(): | ||
log.error("`Qleverfile` already exists in current directory") | ||
log.info("") | ||
log.info("If you want to create a new Qleverfile using " | ||
"`qlever setup-config`, delete the existing " | ||
"Qleverfile first") | ||
return False | ||
|
||
# Copy the Qleverfile to the current directory. | ||
try: | ||
subprocess.run(setup_config_cmd, shell=True, check=True, | ||
stdin=subprocess.DEVNULL, | ||
stdout=subprocess.DEVNULL) | ||
except Exception as e: | ||
log.error(f"Could not copy \"{preconfigured_qleverfile_path}\"" | ||
f" to current directory: {e}") | ||
return False | ||
|
||
# If we get here, everything went well. | ||
log.info(f"Created Qleverfile for config \"{config_name}\"" | ||
f" in current directory") | ||
return True | ||
|
||
# Calling `qlever config` without arguments is an error. Show the help. | ||
log.error("`qlever config` requires at least one argument, " | ||
"see `qlever config --help`") | ||
return False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
from __future__ import annotations | ||
|
||
import os | ||
|
||
from qlever.qleverfile import Qleverfile | ||
|
||
|
||
class EnvvarsException(Exception): | ||
pass | ||
|
||
|
||
class Envvars: | ||
""" | ||
Class for parsing environment variables with analogous names to those in | ||
the `Qleverfile` class, according to the schema `QLEVER_SECTION_VARIABLE`. | ||
For example, variable `PORT` in section `server` corresponds to the | ||
environment variable `QLEVER_SERVER_PORT`. | ||
""" | ||
|
||
@staticmethod | ||
def envvar_name(section: str, name: str) -> str: | ||
""" | ||
For a given section and variable name, return the environment variable | ||
name according to the schema described above. | ||
""" | ||
return f"QLEVER_{section.upper()}_{name.upper()}" | ||
|
||
@staticmethod | ||
def read(): | ||
""" | ||
Check all environment variables that correspond to an entry in | ||
`Qleverfile.all_arguments()` according to the schema described above, | ||
and return a dictionary `config` with all the values found that way. | ||
For example, for `QLEVER_SERVER_PORT=8000`, there would be an entry | ||
`config['server']['port'] = 8000`. | ||
|
||
NOTE: If no environment variables was found at all, the method will | ||
return `None`. Otherwise, there will be an entry for each section, even | ||
if it is empty. | ||
""" | ||
|
||
all_args = Qleverfile.all_arguments() | ||
config = {} | ||
num_envvars_found = 0 | ||
for section, args in all_args.items(): | ||
config[section] = {} | ||
for arg in args: | ||
envvar = Envvars.envvar_name(section, arg) | ||
if envvar in os.environ: | ||
config[section][arg] = os.environ[envvar] | ||
num_envvars_found += 1 | ||
return config if num_envvars_found > 0 else None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Global variables for the full configuration, set in `qlever_main.py`. | ||
# For example, these are used by by `qlever config --show-qleverfile-config` or | ||
# `qlever config --show-envvars`. | ||
# | ||
# NOTE 1: Most commands do not (and should not) use these: the `args` passed to | ||
# the `execute` method of a command class is deliberately reduced to those | ||
# arguments that are relevant for the command. | ||
# | ||
# NOTE 2: If we would define these in `config.py`, which seems like the natural | ||
# place, we get a circular import error, because we need these in | ||
# `qlever/commands/config.py`, which would have to import `config.py`, which | ||
# imports `__init__.py`, which imports all the command modules. | ||
qleverfile_path = None | ||
qleverfile_config = None | ||
envvars_config = None |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For me many of these feel more natural as a subcommand than as a parameter