diff --git a/README.md b/README.md index 633010c..cc4fe8f 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,8 @@ More extended documentation can be found [here](http://docs.ai4os.eu/en/latest/u ## For developers -Once you update the template, please, update this `README.md`, and **especially** `cookiecutter.json` file and `"__ai4_template"` entry with the corresponging, incremented version. The convention for the `"__ai4_template"` entry is to provide the template repository name, slash '/' closest version of the template, following [SymVer](https://semver.org/) specs, e.g. +Once you update the template, please, update this `README.md` **and** `VERSION` file with the corresponging, incremented version following [SymVer](https://semver.org/) specs. +Once commited, please, verify in the remote repository that the entry `"__ai4_template"` in the `cookiecutter.json` file was updated properly by the GitHub Action. The convention for the `"__ai4_template"` entry is to provide the template repository name, slash '/' closest version of the template, i.e. it should look like: ``` "__ai4_template": "ai4-template/2.1.0" diff --git a/VERSION b/VERSION index 7c32728..8f9174b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.1 \ No newline at end of file +2.1.2 \ No newline at end of file diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 5b3ec35..70912c4 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -11,81 +11,76 @@ Creates 'test' branch Switches back to 'main' """ +import logging import os import re import shutil import subprocess as subp import sys -APP_REGEX = r'^[a-z][_a-z0-9]+$' +logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") repo_name = '{{ cookiecutter.__repo_name }}' default_branch = 'main' readme_file = 'README.md' -app_name = '{{ cookiecutter.__app_name }}' -if not re.match(APP_REGEX, app_name): - print("") - print("[ERROR]: %s is not a valid Python package name!" % app_name) - print(" Please, use low case and no dashes!") - - # exits with status 1 to indicate failure - sys.exit(1) - - def git_ini(repo): - """ Function - Initializes Git repository + """ Function to initialize Git repository """ gitrepo = ('{{ cookiecutter.git_base_url }}'.rstrip('/') + "/" + repo + '.git') - try: - os.chdir("../" + repo) - subp.call(["git", "init", "-b", default_branch]) - subp.call(["git", "add", "."]) - subp.call(["git", "commit", "-m", "initial commit"]) - subp.call(["git", "remote", "add", "origin", gitrepo]) - - # create test branch automatically - subp.call(["git", "checkout", "-b", "test"]) - # adjust [Build Status] for the test branch - readme_content=[] - with open(readme_file) as f_old: - for line in f_old: - if "[![Build Status]" in line: - line = re.sub("/main*", "/test", line) - readme_content.append(line) - - with open(readme_file, "w") as f_new: - for line in readme_content: - f_new.write(line) - - subp.call(["git", "commit", "-a", "-m", "update README.md for the BuildStatus"]) - - # switch back to main - subp.call(["git", "checkout", "main"]) - except OSError as os_error: - sys.stdout.write('[Error] Creating git repository failed for ' + repo + " !") - sys.stdout.write('[Error] {} '.format(os_error)) - return "Error" - else: - return gitrepo + + subp.call(["git", "init", "-b", default_branch]) + subp.call(["git", "add", "."]) + subp.call(["git", "commit", "-m", "initial commit"]) + subp.call(["git", "remote", "add", "origin", gitrepo]) + + return gitrepo + + +def create_branch(branch): + """ Function to create an additional branch""" + + # switch to 'main' + subp.call(["git", "checkout", default_branch]) + + # create new branch + subp.call(["git", "checkout", "-b", branch]) + # adjust [Build Status] for the branch + readme_content=[] + with open(readme_file) as f_old: + for line in f_old: + if "[![Build Status]" in line: + line = re.sub("/main*", "/"+branch, line) + readme_content.append(line) + + with open(readme_file, "w") as f_new: + for line in readme_content: + f_new.write(line) + + subp.call(["git", "commit", "-a", "-m", "update README.md for the BuildStatus"]) + + # switch back to main + subp.call(["git", "checkout", default_branch]) try: # initialize git repository + os.chdir("../" + repo_name) git_user_app = git_ini(repo_name) + create_branch("test") + create_branch("dev") + + message = f""" +******************************************* +{repo_name} was created successfully. +Don't forget to create corresponding remote repository: {git_user_app} +then you can do 'git push origin --all' +******************************************* +""" + print(message) + logging.info(message) +except Exception as err: + logging.error(f"While attempting to create git repository an error occurred! {err}", exc_info=True) + raise SystemExit(1) from err - if "Error" not in git_user_app: - print() - print("[Info] {} was created successfully,".format(repo_name)) - print(" Don't forget to create corresponding remote repository: {}".format(git_user_app)) - print(" then you can do 'git push origin --all'") - print() - - sys.exit(0) -except OSError as os_error: - sys.stdout.write( - 'While attempting to create git repository an error occurred! ' - ) - sys.stdout.write('Error! {} '.format(os_error)) diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py index 88f81ca..d062348 100644 --- a/hooks/pre_gen_project.py +++ b/hooks/pre_gen_project.py @@ -13,41 +13,111 @@ b. has characters valid for python """ +import logging import re import sys from urllib.parse import urlparse +logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") +FOLDER_REGEX = r"^[a-zA-Z0-9_-]+$" +MODULE_REGEX = r"^[_a-zA-Z][_a-zA-Z0-9]+$" +EMAIL_REGEX = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$" +APP_VERSION_REGEX = r"^\d+\.\d+\.\d+$" + +# ----------------------------------------------------------------------------- +def validate_git_base_url(): + """Validate git_base_url""" + git_base_url = "{{ cookiecutter.git_base_url }}" + parsed_url = urlparse(url=git_base_url) + if not bool(parsed_url.scheme and parsed_url.netloc): + e_message = f"Invalid git_base_url ({git_base_url})" + logging.error(e_message) + raise ValueError(e_message) + + +# ----------------------------------------------------------------------------- +def validate_project_name(): + """Validate project_name""" + project_name = "{{ cookiecutter.project_name }}" + e_message = [] + if len(project_name) < 2: + e_message = f"Invalid project name ({project_name}), length < 2 characters" + logging.error(e_message) + raise ValueError(e_message) + if len(project_name.split(" ")) > 4: + e_message = f"Invalid project name ({project_name}), length > 4 words)" + logging.error(e_message) + raise ValueError(e_message) + +# repo_name and app_name are derived automatically in cookiecutter.json, +# nevertheless, let's check them here + +def validate_repo_name(): + """Validate repo_name""" + repo_name = "{{ cookiecutter.__repo_name }}" + if not re.match(FOLDER_REGEX, repo_name): + e_message = f"Invalid characters in repo_name ({repo_name})" + logging.error(e_message) + raise ValueError(e_message) + + +def validate_app_name(): + """Validate app_name""" + app_name = "{{ cookiecutter.__app_name }}" + if not re.match(MODULE_REGEX, app_name): + e_message = f"Invalid package name ({app_name})" + logging.error(e_message ) + raise ValueError(e_message) + +# ----------------------------------------------------------------------------- +def validate_authors(): + """Validate author_emails and author_names""" + author_emails = "{{ cookiecutter.author_email }}".split(",") + for email in author_emails: + if not re.match(EMAIL_REGEX, email.strip()): + e_message = f"Invalid author_email ({email})" + logging.error(e_message) + raise ValueError(e_message) + author_names = "{{ cookiecutter.author_name }}".split(",") + lens = n_authors, n_emails = len(author_names), len(author_emails) + if n_emails != n_authors: + e_message = f"Authors ({n_authors}) not matching number of emails ({n_emails})" + logging.error(e_message) + raise ValueError(e_message) + + +# ----------------------------------------------------------------------------- +def validate_app_version(): + """Validate app_version""" + app_version = "{{ cookiecutter.app_version }}" + if not re.match(APP_VERSION_REGEX, app_version): + e_message = f"Invalid app_version ({app_version})" + logging.error(e_message) + raise ValueError(e_message) + +# ----------------------------------------------------------------------------- +# Run all validations, exit with error if any # init error_messages error = False error_messages = [] +validations = [ validate_git_base_url, + validate_project_name, + validate_repo_name, + validate_app_name, + validate_authors, + validate_app_version + ] -# check {{ cookiecutter.git_base_url}} -def check_url(url): - """Function to check URL""" +# allow to run all validations +for fn in validations: try: - result = urlparse(url) - return all([result.scheme, result.netloc]) - except: - return False - -git_base_url = '{{ cookiecutter.git_base_url}}' -if (not check_url(git_base_url)): - message = ("'{}' is not a valid URL! ".format(git_base_url) + - "Please, check the 'git_base_url' input") - print("[ERROR]: " + message) - error = True - error_messages.append(message) - -# check {{ cookiecutter.__app_name }} -MODULE_REGEX = r'^[_a-zA-Z][_a-zA-Z0-9]+$' -app_name = '{{ cookiecutter.__app_name }}' -if (not re.match(MODULE_REGEX, app_name) or - len(app_name) < 2): - message = ("'{}' is not a valid Python module name! ".format(app_name) + - "Please, check the 'project_name' input") - print("[ERROR]: " + message) - error = True - error_messages.append(message) + fn() + except ValueError as err: + error_messages.append(err.args[0]) + error = True +# if any error, raise SystemExit(1) if error: - sys.exit("; ".join(error_messages)) + #e_message = "; ".join(error_messages) + #logging.error(e_message, exc_info=True) + raise SystemExit(1)