-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge commit '286d61416e68a372634cf2df6b8663467cf5e8c5' into enhancem…
…ent-item-body-section-parsing
- Loading branch information
Showing
6 changed files
with
302 additions
and
2 deletions.
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 |
---|---|---|
|
@@ -26,4 +26,5 @@ yarn-error.log* | |
|
||
# Python Files | ||
/api/venv | ||
__pycache__/ | ||
__pycache__/ | ||
venv-manager.log |
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
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,31 @@ | ||
aniso8601==8.0.0 | ||
astroid==2.4.2 | ||
attrs==20.1.0 | ||
click==7.1.2 | ||
Flask==1.1.2 | ||
Flask-RESTful==0.3.8 | ||
gunicorn==20.0.4 | ||
importlib-metadata==1.7.0 | ||
iniconfig==1.0.1 | ||
isort==4.3.21 | ||
itsdangerous==1.1.0 | ||
Jinja2==2.11.2 | ||
lazy-object-proxy==1.4.3 | ||
MarkupSafe==1.1.1 | ||
mccabe==0.6.1 | ||
more-itertools==8.5.0 | ||
packaging==20.4 | ||
pkg-resources==0.0.0 | ||
pluggy==0.13.1 | ||
py==1.9.0 | ||
pylint==2.5.3 | ||
pyparsing==2.4.7 | ||
pytest==6.0.1 | ||
python-dateutil==2.8.1 | ||
pytz==2020.1 | ||
six==1.15.0 | ||
toml==0.10.1 | ||
typed-ast==1.4.1 | ||
Werkzeug==1.0.1 | ||
wrapt==1.12.1 | ||
zipp==3.1.0 |
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 @@ | ||
testfile |
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,247 @@ | ||
"""Allows for creating, deleting and removing Python virtual environments in webqueue2 | ||
Examples: | ||
Create a virtual environment: | ||
$ venv-manager.py create | ||
Delete a virtual environment: | ||
$ venv-manager.py delete | ||
Reset a virtual environment: | ||
$ venv-manager.py reset | ||
""" | ||
|
||
from pathlib import Path | ||
import os, logging, argparse, subprocess | ||
from typing import Union | ||
|
||
|
||
################################################################################ | ||
# Configuration | ||
################################################################################ | ||
|
||
# Set virtual environment path | ||
WEBQUEUE2_DIR = Path(os.path.abspath(__file__)).parent.parent | ||
API_DIR = Path(WEBQUEUE2_DIR, "api") | ||
VENV_NAME = "venv" | ||
VENV_DIR = Path(API_DIR, VENV_NAME) | ||
|
||
|
||
# Set minimum pip major version | ||
TARGET_PIP_VERSION = 19 | ||
|
||
|
||
# Configure the logger | ||
logger_name = "venv-manager" | ||
logger = logging.getLogger(logger_name) | ||
logger.setLevel(logging.INFO) | ||
|
||
# See: https://docs.python.org/3/library/logging.html#logrecord-attributes | ||
log_message_format = "%(asctime)s %(name)s : [%(levelname)s] %(message)s" | ||
# See: https://docs.python.org/3.6/library/time.html#time.strftime | ||
log_time_format = "%b %d %Y %H:%M:%S" | ||
log_formatter = logging.Formatter(log_message_format, log_time_format) | ||
|
||
stream_handler = logging.StreamHandler() | ||
stream_handler.setFormatter(log_formatter) | ||
|
||
log_file_path = Path(WEBQUEUE2_DIR, "utils", f'{logger_name}.log') | ||
file_handler = logging.FileHandler(log_file_path) | ||
file_handler.setFormatter(log_formatter) | ||
|
||
logger.addHandler(stream_handler) | ||
logger.addHandler(file_handler) | ||
|
||
|
||
################################################################################ | ||
# Functions | ||
################################################################################ | ||
|
||
|
||
def get_args() -> argparse.Namespace: | ||
"""Parses arguments and returns argparses's generated namespace. | ||
Returns: | ||
argparse.Namespace: Argparses's generated namespace. | ||
""" | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument( | ||
"action", | ||
help="Action to perform.", | ||
choices=("create", "delete", "reset") | ||
) | ||
parser.add_argument( | ||
"--debug", | ||
help="Print debug logs", | ||
action="store_true", | ||
) | ||
return parser.parse_args() | ||
|
||
|
||
def run_logged_subprocess(command: Union[str, list], timeout: int = 10, shell: bool = True) -> tuple: | ||
"""Executes a shell command using subprocess with logging. | ||
stderr is redirected to stdout and stdout is pipped to logger. | ||
If the subprocess raises an exception, the exception is logged as critical. | ||
Example: | ||
Running a successful command: | ||
run_logged_subprocess(command=["git", "commit", "-m", "'Commit message.'"]) | ||
Returns: (0, "") | ||
Running an unsuccessful shell command with a 20 second timeout: | ||
run_logged_subprocess(command="cd test/", timeout=20, shell=True) | ||
Returns: (1, "cd: test: No such file or directory\n") | ||
Args: | ||
command (Union): The command to run. If shell=False, pass a list with the first item being the command and the subsequent items being arguments. If shell=True, pass a string as you would type it into a shell. | ||
timeout (int): The number of seconds to wait for a program before killing it | ||
Returns: | ||
tuple: With the first value being the return code and second being the combined stdout+stderr | ||
""" | ||
logger.debug(f"Entering subprocess for '{command}'") | ||
with subprocess.Popen(command,\ | ||
stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=shell, text=True)\ | ||
as logged_shell_process: | ||
|
||
subprocess_log_prefix = f"(PID: {logged_shell_process.pid})" | ||
|
||
try: | ||
# Convert combined stdout and stderr stream to list of strings | ||
process_output_stream, _ = logged_shell_process.communicate(timeout=timeout) | ||
process_output_lines = process_output_stream.split("\n") | ||
# Remove last entry in process_output_lines because it is always empty | ||
process_output_lines.pop(-1) | ||
|
||
for line in process_output_lines: | ||
logger.debug(f"{subprocess_log_prefix}: {line}") | ||
except Exception as exception: | ||
logger.critical(str(exception)) | ||
else: | ||
if logged_shell_process.returncode != 0: | ||
logger.debug(f"Something went wrong. '{command}' exited with return code {logged_shell_process.returncode}") | ||
elif logged_shell_process.returncode == 0: | ||
logger.debug(f"Subprocess for '{command}' completed successfuly") | ||
finally: | ||
logger.debug(f"Exiting subprocess for '{command}'") | ||
return (logged_shell_process.returncode, process_output_stream) | ||
|
||
|
||
def create_environment() -> int: | ||
"""Creates a virtual environment for webqueue2 | ||
Exit Codes: | ||
0 = Success | ||
5 = VENV_DIR already exists | ||
10 = Could not create VENV_DIR | ||
15 = Could not install requirements | ||
Returns: | ||
int: Exit code | ||
""" | ||
|
||
# Check for an existing virtual environment | ||
try: | ||
os.mkdir(VENV_DIR) | ||
except FileExistsError: | ||
logger.warning(f"The directory {VENV_DIR} already exists. Exiting") | ||
return 5 | ||
|
||
# Create virtual environment | ||
logger.info(f"Creating virtual environment {VENV_NAME} at {VENV_DIR}") | ||
create_env_returncode, _ = run_logged_subprocess(f"cd {API_DIR} && python3 -m venv {VENV_NAME}", shell=True) | ||
if create_env_returncode == 0: | ||
logger.info(f"Virtual environment {VENV_NAME} created at {VENV_DIR}") | ||
else: | ||
logger.critical(f"Could not create virtual environment {VENV_NAME} at {VENV_DIR}. Exiting") | ||
return 10 | ||
|
||
# Check pip version | ||
logger.debug("Checking pip version") | ||
check_pip_returncode, check_pip_output = run_logged_subprocess(f"{VENV_DIR}/bin/pip --version") | ||
|
||
if check_pip_returncode != 0: | ||
logger.warning("Could not check pip version. Virtual environment dependencies may not install") | ||
|
||
pip_version_full = check_pip_output.split()[1] | ||
logger.debug(f"pip version is {pip_version_full}") | ||
|
||
pip_version_major = pip_version_full.split(".")[0] | ||
if int(pip_version_major) < 19: | ||
logger.info(f"pip verion is {pip_version_major}.x (pip >= {TARGET_PIP_VERSION}.x needed.) Upgrading pip") | ||
update_pip_returncode, update_pip_output = run_logged_subprocess(f"{VENV_DIR}/bin/pip install --upgrade pip") | ||
|
||
if update_pip_returncode == 0: | ||
logger.info(update_pip_output.split("\n")[-2]) | ||
else: | ||
logger.warning("Failed to update pip. Virtual environment dependencies may not install") | ||
|
||
# Install requirements | ||
logger.info("Installing requirements") | ||
install_requirements_returncode, _ = run_logged_subprocess(f"{VENV_DIR}/bin/pip install -r {API_DIR}/requirements.txt") | ||
if install_requirements_returncode == 0: | ||
logger.info("Successfully installed requirements") | ||
return 0 | ||
else: | ||
logger.critical("Failed to install requirements. Exiting") | ||
return 15 | ||
|
||
|
||
def delete_environment() -> int: | ||
"""Deletes a virtual environment for webqueue2 | ||
Exit Codes: | ||
0 = Success | ||
5 = Could not delete VENV_DIR | ||
Returns: | ||
int: Exit code | ||
""" | ||
delete_venv_returncode, _ = run_logged_subprocess(f"rm -rf {VENV_DIR}") | ||
if delete_venv_returncode == 0: | ||
logger.info(f"Successfully deleted virtual environment {VENV_DIR} at {VENV_DIR}") | ||
return 0 | ||
else: | ||
logger.critical(f"Failed to delete virtual environment {VENV_DIR} at {VENV_DIR}. Exiting") | ||
return 5 | ||
|
||
def reset_environment() -> int: | ||
"""Resets a virtual environment for webqueue2 | ||
Exit Codes: | ||
0 = Success | ||
5 = Could not delete VENV_DIR | ||
10 = Could not create VENV_DIR | ||
Returns: | ||
int: Exit code | ||
""" | ||
delete_returncode = delete_environment() | ||
if delete_returncode != 0: | ||
logger.critical(f"Failed to reset virtual environment {VENV_DIR} at {VENV_DIR}. Exiting") | ||
return 5 | ||
|
||
create_returncode = create_environment() | ||
if create_returncode != 0: | ||
logger.critical(f"Failed to reset virtual environment {VENV_DIR} at {VENV_DIR}. Exiting") | ||
return 10 | ||
|
||
logger.info(f"Successfully reset virtual environment {VENV_DIR} at {VENV_DIR}. Exiting") | ||
|
||
|
||
if __name__ == "__main__": | ||
args = get_args() | ||
action = args.action | ||
|
||
if args.debug: | ||
logger.setLevel(logging.DEBUG) | ||
|
||
if action == "create": | ||
exit(create_environment()) | ||
elif action == "delete": | ||
exit(delete_environment()) | ||
elif action == "reset": | ||
exit(reset_environment()) | ||
else: | ||
logger.critical(f'Invalid argument {action}') |