Skip to content

Commit

Permalink
Merge commit '286d61416e68a372634cf2df6b8663467cf5e8c5' into enhancem…
Browse files Browse the repository at this point in the history
…ent-item-body-section-parsing
  • Loading branch information
Jacob Daniel Bennett committed Sep 17, 2020
2 parents 09841c1 + 286d614 commit ffb77be
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 2 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ yarn-error.log*

# Python Files
/api/venv
__pycache__/
__pycache__/
venv-manager.log
19 changes: 18 additions & 1 deletion api/ECNQueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def __init__(self, queue: str, number: int) -> None:
self.priority = self.__getMostRecentHeaderByType("Priority")
self.department = self.__getMostRecentHeaderByType("Department")
self.building = self.__getMostRecentHeaderByType("Building")
self.dateReceived = self.__getMostRecentHeaderByType("Date")
self.dateReceived = self.__getParsedDate(self.__getMostRecentHeaderByType("Date"))

self.jsonData = {
"queue": self.queue,
Expand Down Expand Up @@ -608,6 +608,23 @@ def __getAssignedTo(self) -> str:
"""
assignedTo = self.__getMostRecentHeaderByType("Assigned-To")
return assignedTo

def __getFormattedDate(self, date: str) -> str:
"""Returns the date/time formatted as RFC 8601 YYYY-MM-DDTHH:MM:SS+00:00.
Returns empty string if the string argument passed to the function is not a datetime.
See: https://en.wikipedia.org/wiki/ISO_8601
Returns:
str: Properly formatted date/time recieved or empty string.
"""
try:
parsedDate = parse(date)
except:
return ""

parsedDateString = parsedDate.strftime("%Y-%m-%dT%H:%M:%S%z")

return parsedDateString

def toJson(self) -> dict:
"""Returns a JSON safe representation of the item.
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
"start:docs": "npx styleguidist server --open --config styleguidist/styleguide.config.js",
"build:frontend": "react-scripts build",
"build:docs": "npx styleguidist build --config styleguidist/styleguide.config.js",
"venv:create": "python3 utils/venv-manager.py create",
"venv:delete": "python3 utils/venv-manager.py delete",
"venv:reset": "python3 utils/venv-manager.py reset",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
Expand Down
31 changes: 31 additions & 0 deletions requirements.txt
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
1 change: 1 addition & 0 deletions testfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testfile
247 changes: 247 additions & 0 deletions utils/venv-manager.py
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}')

0 comments on commit ffb77be

Please sign in to comment.