Skip to content

Commit

Permalink
Merge pull request #6 from ai4os/feat/hooks
Browse files Browse the repository at this point in the history
feat: update pre_ and post_ generation hooks for logging and better error handling
  • Loading branch information
vykozlov authored Jul 31, 2024
2 parents 70419fb + 1434bcd commit c30236b
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 86 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.1.1
2.1.2
109 changes: 52 additions & 57 deletions hooks/post_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
124 changes: 97 additions & 27 deletions hooks/pre_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit c30236b

Please sign in to comment.