diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index c52d587..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM python:3.11-alpine - -COPY example-files . - -CMD ["ls", "/shows"] diff --git a/docker/build.sh b/docker/build.sh deleted file mode 100755 index bdf0b07..0000000 --- a/docker/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -# builds a container with the current Dockerfile for testing the bb-asset-validation program -docker build -t bb-asset-validation . diff --git a/docker/example-files/shows/projectX/assets/environment/forest_v001/model/render/forest_beauty_v001.1001.exr b/docker/example-files/shows/projectX/assets/environment/forest_v001/model/render/forest_beauty_v001.1001.exr deleted file mode 100644 index e69de29..0000000 diff --git a/docker/run.sh b/docker/run.sh deleted file mode 100755 index ee0c741..0000000 --- a/docker/run.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -# run the docker container for testing the bb-asset-validation program -docker run bb-asset-validation diff --git a/example-paths-malformed.txt b/example-paths-malformed.txt new file mode 100644 index 0000000..ed4336a --- /dev/null +++ b/example-paths-malformed.txt @@ -0,0 +1,9 @@ +# should throw invalid version error +/shows/projectX/assets/environment/forest_v003/model/render/forest_beauty_v002.1001.exr + +# should throw wrong size error +/shows/projectX/assets/environment/forest_v003/model/forest_beauty_v002.1001.exr + +# should throw invalid prefix path error +/other/projectX/assets/environment/forest_v002/model/cache/forest_v002.fbx + diff --git a/example-paths-mixed.txt b/example-paths-mixed.txt new file mode 100644 index 0000000..aa357d1 --- /dev/null +++ b/example-paths-mixed.txt @@ -0,0 +1,22 @@ +# valid paths +/shows/projectX/assets/environment/forest_v001/model/render/forest_beauty_v001.1001.exr +/shows/projectX/assets/environment/forest_v001/model/render/forest_beauty_v001.1002.exr +/shows/projectX/assets/environment/forest_v001/model/render/forest_beauty_v001.1003.exr +/shows/projectX/assets/environment/forest_v001/model/cache/forest_v001.fbx +/shows/projectX/assets/environment/forest_v002/model/render/forest_beauty_v002.1001.exr +/shows/projectX/assets/environment/forest_v002/model/render/forest_beauty_v002.1002.exr +/shows/projectX/assets/environment/forest_v002/model/render/forest_beauty_v002.1003.exr +/shows/projectX/assets/environment/forest_v002/model/cache/forest_v002.fbx + +# should throw invalid version error +/shows/projectX/assets/environment/forest_v003/model/render/forest_beauty_v002.1001.exr + +# should throw wrong size error +/shows/projectX/assets/environment/forest_v003/model/forest_beauty_v002.1001.exr + +# valid path +/shows/projectX/assets/environment/forest_v002/model/cache/forest_v002.fbx + +# should throw invalid prefix path error +/other/projectX/assets/environment/forest_v002/model/cache/forest_v002.fbx + diff --git a/example-paths.txt b/example-paths-valid.txt similarity index 100% rename from example-paths.txt rename to example-paths-valid.txt diff --git a/src/bb_asset_validation/cli.py b/src/bb_asset_validation/cli.py index 9586960..fce8b6a 100644 --- a/src/bb_asset_validation/cli.py +++ b/src/bb_asset_validation/cli.py @@ -1,6 +1,14 @@ import argparse import os.path import sys +import re + +# Global Variables. In production code I would not use global variables + +# ANSI color escape codes, will not work on windows. +ANSI_FORE_RED = "\033[31m" +ANSI_FORE_GREEN = "\033[32m" +ANSI_DEFAULT = "\033[0m" def parse_args() -> argparse.Namespace: """ @@ -21,21 +29,6 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -def validate_shows_dir(shows_directory: str): - """ - Checks the given show directory is valid. - - Arguments: - shows_directory (str): The directory shows are stored within - - Returns: - bool: Whether the directory is valid. - """ - - # Check path exists - if(not os.path.exists(shows_directory)): - raise ValueError("Designated show path does not exist: " + shows_directory) - def split_path(path): PATH_DELIMETER = os.path.sep @@ -46,66 +39,162 @@ def split_path(path): return split_path -def validate_asset_path_structure(parsed_args, asset_path: str): +def get_path_components(asset_path: str): + pattern = re.compile(r"(?P/shows/(?P\S*?)/\S*?/(?P\S*?)/(?P\S*?)_v(?P\S*?)/(?P\S*?)/\S*?/(?P\S*?)_v(?P\d*)\D*?(?P\d*)\D*?\.(?P\w*)\Z)") + match = pattern.match(asset_path) + path_components = match.groupdict() + + return path_components + +def assemble_new_path(path_components: dict) -> str: + frame_number = f".{path_components['frame_number']}" if path_components['frame_number'] else "" + file_extension = f".{path_components['file_extension']}" if path_components['file_extension'] else "" + return f"/shows/{path_components['project_name']}/staging/delivery/assets/{path_components['asset_type']}_{path_components['asset_name']}/{path_components['task']}_{path_components['asset_version']}/{path_components['file_name']}{frame_number}{file_extension}" + +def should_skip_line(line): + line = line.strip() + + return ( + len(line)<=1 or # too short + line[0]=="#" # comment + ) + + +def validate_paths_file(parsed_args): + with open(parsed_args.paths_file, "r") as f: + line = f.readline() + while(line): + line = f.readline() + + if(should_skip_line(line)): + continue + # clean line + path = line.strip() + + # validate path + if(not validate_path(parsed_args, path)): + continue + + # all components of the project + all_components = get_path_components(path) + + # map the specific components listed in the brief + stored_components = map_stored_components(all_components) + + # assemble the new path + new_path = assemble_new_path(all_components) + + # print out the resulting values + print_result(path, new_path, stored_components) + +def validate_static_list(parsed_args): + static_list = [ + "/shows/projectX/assets/environment/forest_v001/model/render/forest_beauty_v001.1002.exr", + "/shows/projectX/assets/environment/forest_v001/model/cache/forest_v001.fbx" + ] + + for line in static_list: + if(should_skip_line(line)): + continue + # clean line + path = line.strip() + + # validate path + if(not validate_path(parsed_args, path)): + continue + + # all components of the project + all_components = get_path_components(path) + + # map the specific components listed in the brief + stored_components = map_stored_components(all_components) + + # assemble the new path + new_path = assemble_new_path(all_components) + + # print out the resulting values + print_result(path, new_path, stored_components) + +def map_stored_components(all_components): + stored_components = { + "full_path" : all_components["full_path"], + "project" : all_components["project_name"], + "asset_type" : all_components["asset_type"], + "asset_name" : all_components["asset_name"], + "version" : all_components["asset_version"], + "task" : all_components["task"], + "file_extension" :all_components["file_extension"], + } + return stored_components + +def print_result(original_path, new_path, stored_components): + print(f"{ANSI_FORE_GREEN}\n--------------") + print(f"original path:\n\t{original_path}\n") + print(f"new path:\n\t{new_path}\n") + print(f"stored components:\n\t{stored_components}") + print(f"--------------\n{ANSI_DEFAULT}") + +# def validate_static_list(): + + +def throw_schema_error(path: str, error_message: str) -> None: + print(f"\n{ANSI_FORE_RED}-----\nPath is invalid: {path}\n{error_message}\n-----{ANSI_DEFAULT}", file=sys.stderr) + +def validate_path(parsed_args: argparse.Namespace, asset_path: str) -> bool: + """ + Validates the given path based on the arguments provided. + + Parameters: + parsed_args (argparse.Namespace): CLI arguments + asset_path (str): The path to validate + + Returns: + Whether the provided path is valid + """ + # check path is absolute if(asset_path[0] != "/"): - print(f"\nError for path: {asset_path}\n-------------", file=sys.stderr) - print("Expected absolute asset path, recieved path does not start with '/'\n", file=sys.stderr) - - split_asset_path = split_path(os.path.normpath(asset_path)) + throw_schema_error(asset_path, "Expected absolute asset path, recieved path does not start with '/'") + return False + # check length + EXPECTED_LENGTH=8 + split_asset_path = split_path(asset_path) + if(EXPECTED_LENGTH is not None and len(split_asset_path) != EXPECTED_LENGTH): + throw_schema_error(asset_path, f"Path isn't expected length {EXPECTED_LENGTH}.") + return False # check path is prefixed with shows path - # valid_show_path = True - split_shows_path = split_path(os.path.realpath(parsed_args.shows_directory)) - valid_show_path = split_shows_path == split_asset_path[:len(split_shows_path)] - + shows_path = os.path.realpath(parsed_args.shows_directory) + valid_show_path = shows_path == asset_path[:len(shows_path)] if(not valid_show_path): - print(f"\nError for path: {asset_path}\n-------------", file=sys.stderr) - print(f"Path starting with '{split_asset_path[0]}' does not start with expected show directory '{parsed_args.shows_directory}'\n", file=sys.stderr) - else: - print("\n--------------\nGOOD PATH:", asset_path,"\n----------------\n") + throw_schema_error(asset_path, f"Path does not start with expected show directory '{parsed_args.shows_directory}'") + return False - split_asset_path_suffix = split_asset_path[len(split_shows_path):] + # get components + path_components = get_path_components(asset_path) + # validate versions + if(path_components["asset_version"] != path_components["file_version"]): + throw_schema_error(asset_path, "Asset version and file version does not match.") + return False - path_components = { - "project_name" : split_asset_path_suffix[0], - "asset_type" : split_asset_path_suffix[2], - "asset_name" : split_asset_path_suffix[3].split("_")[0], - "task" : split_asset_path_suffix[4], - "version" : split_asset_path_suffix[3].split("_")[1], - "filename" : os.path.splitext(split_asset_path_suffix[6])[0].split("_")[0], - "file_extension" : os.path.splitext(split_asset_path_suffix[6])[1], - } - print(path_components) - - new_path = f"/shows/{path_components['project_name']}/staging/delivery/assets/{path_components['asset_type']}_{path_components['asset_name']}/{path_components['task']}_{path_components['version']}/{path_components['filename']}{path_components['file_extension']}" - print("new path:\n", new_path) - - + return True def main() -> None: # Parse args parsed_args = parse_args() - shows_directory = parsed_args.shows_directory - - # validate_shows_dir(shows_directory) if(parsed_args.paths_file): - with open(parsed_args.paths_file, "r") as f: - line = f.readline() - while(line): - line = f.readline() + validate_paths_file(parsed_args) + else: + print("No file provided.\n") + print("Try passing one of the example files with the --paths-file argument.") + print(f"eg. '{ANSI_FORE_GREEN}bb-asset-validation --paths-file example-paths-mixed.txt{ANSI_DEFAULT}'") + print("\nAlternatively, here is the validation of a hardcoded list of paths:") - # clean line - line = line.strip() - # skip empty lines - if(line==""): - continue + validate_static_list(parsed_args) - print('line:', line) - validate_asset_path_structure(parsed_args, line) if __name__ == "__main__":