Compare commits

...
Author SHA1 Message Date
Andrew Ferlitsch 8b7bc1a396 fix: script format 2022-10-10 21:35:02 +00:00
Andrew Ferlitsch 3235c5cf86 fix: script format 2022-10-10 20:29:24 +00:00
Andrew Ferlitsch 45d50e6520 fix: script format 2022-10-10 19:32:13 +00:00
Andrew Ferlitsch c1b56fbaf8 fix: script format 2022-10-10 17:18:45 +00:00
+477 -185
View File
@@ -1,3 +1,33 @@
"""
AutoReview: Script to automatically review Vertex AI notebooks for conformance to notebook template requirements:
python3 notebook_template_review.py [options]
# options for selecting notebooks
--notebook: review the specified notebook
--notebook-dir: recursively traverse the directory and review each notebook enocuntered
--notebook-file: A CSV file with list of notebooks to review.
# options for error handling
--errors: Report detected errors.
--errors-codes: A list of error codes to report errors. Otherwise, all errors are reported.
--errors-csv: Report errors in CSV format
# index generatation
--repo: Generate index in markdown format
--web: Generate index in HTML format
--title: Add title to index
--desc: Add description to index
--steps: Add steps to index
--uses: Add "resources" used to index
Format of CSV file for notebooks to review:
tags,notebook-path,backlink
tags: Double quoted ist of tags: e.g., "AutoML, Tabular Data"
notebook-path: path of notebook, relative to https://github.com/GoogleCloudPlatform/vertex-ai-samples/notebooks
backlink: webdoc page with more details, relative to https://cloud.google.com/
"""
import argparse
import json
@@ -41,9 +71,13 @@ if args.errors_csv:
args.errors = True
# Copyright cell
# Google copyright cell required
ERROR_COPYRIGHT = 0
# Links cell
# H1 heading required
# git, colab and workbench link required
# links must be valid links
ERROR_TITLE_HEADING = 1
ERROR_HEADING_CASE = 2
ERROR_HEADING_CAP = 3
@@ -54,11 +88,79 @@ ERROR_LINK_GIT_BAD = 7
ERROR_LINK_COLAB_BAD = 8
ERROR_LINK_WORKBENCH_BAD = 9
# Overview cells
# Overview cell required
# Objective cell required
# Dataset cell required
# Costs cell required
# Check for required Vertex and optional BQ and Dataflow
ERROR_OVERVIEW_NOTFOUND = 10
ERROR_OBJECTIVE_NOTFOUND = 11
ERROR_OBJECTIVE_MISSING_DESC = 12
ERROR_OBJECTIVE_MISSING_USES = 13
ERROR_OBJECTIVE_MISSING_STEPS = 14
ERROR_DATASET_NOTFOUND = 15
ERROR_COSTS_NOTFOUND = 16
ERROR_COSTS_MISSING = 17
# Installation cell
# Installation cell required
# Wrong heading for installation cell
# Installation code cell not found
# pip3 required
# option -q required
# option {USER_FLAG} required
# installation code cell not match template
# all packages must be installed as a single pip3
ERROR_INSTALLATION_NOTFOUND = 18
ERROR_INSTALLATION_HEADING = 19
ERROR_INSTALLATION_CODE_NOTFOUND = 20
ERROR_INSTALLATION_PIP3 = 21
ERROR_INSTALLATION_QUIET = 22
ERROR_INSTALLATION_USER_FLAG = 23
ERROR_INSTALLATION_CODE_TEMPLATE = 24
ERROR_INSTALLATION_SINGLE_PIP3 = 25
# Restart kernel cell
# Restart code cell required
# Restart code cell not found
ERROR_RESTART_NOTFOUND = 23
ERROR_RESTART_CODE_NOTFOUND = 24
# Before you begin cell
# Before you begin cell required
# Before you begin cell incomplete
ERROR_BEFOREBEGIN_NOTFOUND = 25
ERROR_BEFOREBEGIN_INCOMPLETE = 26
# Set Project ID
# Set project ID cell required
# Set project ID code cell not found
# Set project ID not match template
ERROR_PROJECTID_NOTFOUND = 27
ERROR_PROJECTID_CODE_NOTFOUND = 28
ERROR_PROJECTID_TEMPLATE = 29
# Technical Writer Rules
ERROR_TWRULE_TODO = 51
ERROR_TWRULE_FIRSTPERSON = 52
ERROR_TWRULE_FUTURETENSE = 53
ERROR_TWRULE_BRANDING = 54
ERROR_PLACEHOLDER = 100
ERROR_EMPTY_CALL = ERROR_PLACEHOLDER + 1
# globals
num_errors = 0
last_tag = ''
def parse_dir(directory):
def parse_dir(directory: str) -> None:
"""
Recursively walk the specified directory, reviewing each notebook (.ipynb) encountered.
directory: The directory path.
"""
entries = os.scandir(directory)
for entry in entries:
if entry.is_dir():
@@ -71,7 +173,13 @@ def parse_dir(directory):
elif entry.name.endswith('.ipynb'):
parse_notebook(entry.path)
def parse_notebook(path):
def parse_notebook(path: str) -> None:
"""
Review the specified notebook.
path: The path to the notebook.
"""
with open(path, 'r') as f:
try:
content = json.load(f)
@@ -79,112 +187,51 @@ def parse_notebook(path):
print("Corrupted notebook:", path)
return
nth = 0
cells = content['cells']
# cell 1 is copyright
nth = 0
cell, nth = get_cell(path, cells, nth)
if not 'Copyright' in cell['source'][0]:
report_error(path, ERROR_COPYRIGHT, "missing copyright cell")
nth = parse_copyright(path, cells, nth)
# check for notices
cell, nth = get_cell(path, cells, nth)
if cell['source'][0].startswith('This notebook'):
cell, nth = get_cell(path, cells, nth)
# cell 2 is title and links
if not cell['source'][0].startswith('# '):
report_error(path, ERROR_TITLE_HEADING, "title cell must start with H1 heading")
title = ''
else:
title = cell['source'][0][2:].strip()
check_sentence_case(path, title)
# H1 title only
if len(cell['source']) == 1:
cell, nth = get_cell(path, cells, nth)
nth = parse_notices(path, cells, nth)
# check for title
nth, title = parse_title(path, cells, nth)
# check links.
source = ''
git_link = None
colab_link = None
workbench_link = None
for line in cell['source']:
source += line
if '<a href="https://github.com' in line:
git_link = line.strip()[9:-2].replace('" target="_blank', '')
try:
code = urllib.request.urlopen(git_link).getcode()
except Exception as e:
# if new notebook
derived_link = os.path.join('https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/', path)
if git_link != derived_link:
report_error(path, ERROR_LINK_GIT_BAD, f"bad GitHub link: {git_link}")
if '<a href="https://colab.research.google.com/' in line:
colab_link = 'https://github.com/' + line.strip()[50:-2].replace('" target="_blank', '')
try:
code = urllib.request.urlopen(colab_link).getcode()
except Exception as e:
# if new notebook
derived_link = os.path.join('https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks', path)
if colab_link != derived_link:
report_error(path, ERROR_LINK_COLAB_BAD, f"bad Colab link: {colab_link}")
if '<a href="https://console.cloud.google.com/vertex-ai/workbench/' in line:
workbench_link = line.strip()[91:-2].replace('" target="_blank', '')
try:
code = urllib.request.urlopen(workbench_link).getcode()
except Exception as e:
derived_link = os.path.join('https://console.cloud.google.com/vertex-ai/workbench/deploy-notebook?download_url=https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/main/notebooks/', path)
if colab_link != workbench_link:
report_error(path, ERROR_LINK_WORKBENCH_BAD, f"bad Workbench link: {workbench_link}")
if 'View on GitHub' not in source or not git_link:
report_error(path, ERROR_LINK_GIT_MISSING, 'Missing link for GitHub')
if 'Run in Colab' not in source or not colab_link:
report_error(path, ERROR_LINK_COLAB_MISSING, 'Missing link for Colab')
if 'Open in Vertex AI Workbench' not in source or not workbench_link:
report_error(path, ERROR_LINK_WORKBENCH_MISSING, 'Missing link for Workbench')
nth, git_link, colab_link, workbench_link = parse_links(path, cells, nth)
# Overview
cell, nth = get_cell(path, cells, nth)
if not cell['source'][0].startswith("## Overview"):
report_error(path, 11, "Overview section not found")
nth = parse_overview(path, cells, nth)
# Objective
cell, nth = get_cell(path, cells, nth)
if not cell['source'][0].startswith("### Objective"):
report_error(path, 12, "Objective section not found")
costs = []
else:
desc, uses, steps, costs = parse_objective(path, cell)
nth, desc, uses, steps, costs = parse_objective(path, cells, nth)
if desc != '':
add_index(path, tag, title, desc, uses, steps, git_link, colab_link, workbench_link)
# (optional) Recommendation
cell, nth = get_cell(path, cells, nth)
if cell['source'][0].startswith("### Recommendations"):
cell, nth = get_cell(path, cells, nth)
nth = parse_recommendations(path, cells, nth)
# Dataset
cell, nth = get_cell(path, cells, nth)
if not cell['source'][0].startswith("### Dataset") and not cell['source'][0].startswith("### Model") and not cell['source'][0].startswith("### Embedding"):
report_error(path, 13, "Dataset/Model section not found")
report_error(path, ERROR_DATASET_NOTFOUND, "Dataset/Model section not found")
# Costs
cell, nth = get_cell(path, cells, nth)
if not cell['source'][0].startswith("### Costs"):
report_error(path, 14, "Costs section not found")
report_error(path, ERROR_COSTS_NOTFOUND, "Costs section not found")
else:
text = ''
for line in cell['source']:
text += line
if 'BQ' in costs and 'BigQuery' not in text:
report_error(path, 20, 'Costs section missing reference to BiqQuery')
report_error(path, ERROR_COSTS_MISSING, 'Costs section missing reference to BiqQuery')
if 'Vertex' in costs and 'Vertex' not in text:
report_error(path, 20, 'Costs section missing reference to Vertex')
report_error(path, ERROR_COSTS_MISSING, 'Costs section missing reference to Vertex')
if 'Dataflow' in costs and 'Dataflow' not in text:
report_error(path, 20, 'Costs section missing reference to Dataflow')
report_error(path, ERROR_COSTS_MISSING, 'Costs section missing reference to Dataflow')
# (optional) Setup local environment
cell, nth = get_cell(path, cells, nth)
@@ -201,13 +248,13 @@ def parse_notebook(path):
# Installation
if not cell['source'][0].startswith("## Install"):
if cell['source'][0].startswith("### Install"):
report_error(path, 27, "Installation section needs to be H2 heading")
report_error(path, ERROR_INSTALLATION_HEADING, "Installation section needs to be H2 heading")
else:
report_error(path, 21, "Installation section not found")
report_error(path, ERROR_INSTALLATION_NOTFOUND, "Installation section not found")
else:
cell, nth = get_cell(path, cells, nth)
if cell['cell_type'] != 'code':
report_error(path, 22, "Installation code section not found")
report_error(path, ERROR_INSTALLATION_NOTFOUND, "Installation section not found")
else:
if cell['source'][0].startswith('! mkdir'):
cell, nth = get_cell(path, cells, nth)
@@ -219,15 +266,15 @@ def parse_notebook(path):
text += line
if 'pip ' in line:
if 'pip3' not in line:
report_error(path, 23, "Installation code section: use pip3")
report_error(path, ERROR_INSTALLATION_PIP3, "Installation code section: use pip3")
if line.endswith('\\\n'):
continue
if '-q' not in line:
report_error(path, 23, "Installation code section: use -q with pip3")
if '-q' not in line and '--quiet' not in line :
report_error(path, ERROR_INSTALLATION_QUIET, "Installation code section: use -q with pip3")
if 'USER_FLAG' not in line and 'sh(' not in line:
report_error(path, 23, "Installation code section: use {USER_FLAG} with pip3")
report_error(path, ERROR_INSTALLATION_USER_FLAG, "Installation code section: use {USER_FLAG} with pip3")
if 'if IS_WORKBENCH_NOTEBOOK:' not in text:
report_error(path, 24, "Installation code section out of date (see template)")
report_error(path, ERROR_INSTALLATION_CODE_TEMPLATE, "Installation code section out of date (see template)")
# Restart kernel
while True:
@@ -235,18 +282,18 @@ def parse_notebook(path):
cell, nth = get_cell(path, cells, nth)
for line in cell['source']:
if 'pip' in line:
report_error(path, 25, f"All pip installations must be in a single code cell: {line}")
report_error(path, ERROR_INSTALLATION_SINGLE_PIP3, f"All pip installations must be in a single code cell: {line}")
cont = True
break
if not cont:
break
if not cell['source'][0].startswith("### Restart the kernel"):
report_error(path, 26, "Restart the kernel section not found")
report_error(path, ERROR_RESTART_NOTFOUND, "Restart the kernel section not found")
else:
cell, nth = get_cell(path, cells, nth) # code cell
if cell['cell_type'] != 'code':
report_error(path, 28, "Restart the kernel code section not found")
report_error(path, ERROR_RESTART_CODE_NOTFOUND, "Restart the kernel code section not found")
# (optional) Check package versions
cell, nth = get_cell(path, cells, nth)
@@ -256,13 +303,13 @@ def parse_notebook(path):
# Before you begin
if not cell['source'][0].startswith("## Before you begin"):
report_error(path, 29, "Before you begin section not found")
report_error(path, ERROR_BEFOREBEGIN_NOTFOUND, "Before you begin section not found")
else:
# maybe one or two cells
if len(cell['source']) < 2:
cell, nth = get_cell(path, cells, nth)
if not cell['source'][0].startswith("### Set up your Google Cloud project"):
report_error(path, 30, "Before you begin section incomplete")
report_error(path, ERROR_BEFOREBEGIN_INCOMPLETE, "Before you begin section incomplete")
# (optional) enable APIs
cell, nth = get_cell(path, cells, nth)
@@ -272,21 +319,21 @@ def parse_notebook(path):
# Set project ID
if not cell['source'][0].startswith('#### Set your project ID'):
report_error(path, 31, "Set project ID section not found")
report_error(path, ERROR_PROJECTID_NOTFOUND, "Set project ID section not found")
else:
cell, nth = get_cell(path, cells, nth)
if cell['cell_type'] != 'code':
report_error(path, 32, "Set project ID code section not found")
report_error(path, ERROR_PROJECTID_CODE_NOTFOUND, "Set project ID code section not found")
elif not cell['source'][0].startswith('PROJECT_ID = "[your-project-id]"'):
report_error(path, 33, f"Set project ID not match template: {line}")
cell, nth = get_cell(path, cells, nth)
if cell['cell_type'] != 'code' or 'or PROJECT_ID == "[your-project-id]":' not in cell['source'][0]:
report_error(path, 33, f"Set project ID not match template: {line}")
report_error(path, ERROR_PROJECTID_TEMPLATE, f"Set project ID not match template: {line}")
cell, nth = get_cell(path, cells, nth)
if cell['cell_type'] != 'code' or '! gcloud config set project' not in cell['source'][0]:
report_error(path, 33, f"Set project ID not match template: {line}")
report_error(path, ERROR_PROJECTID_TEMPLATE, f"Set project ID not match template: {line}")
'''
# Region
@@ -296,7 +343,273 @@ def parse_notebook(path):
'''
def get_cell(path, cells, nth):
def parse_copyright(path: str,
cells: list,
nth: int) -> int:
"""
Parse the copyright cell
path: used only for reporting an error
cells: The content cells (JSON) for the notebook
nth: The index of the last cell that was parsed (reviewed).
Returns: cell index
"""
cell, nth = get_cell(path, cells, nth)
if not 'Copyright' in cell['source'][0]:
report_error(path, ERROR_COPYRIGHT, "missing copyright cell")
return nth
def parse_notices(path: str,
cells: list,
nth: int) -> int:
"""
Parse the (optional) notices cell
path: used only for reporting an error
cells: The content cells (JSON) for the notebook
nth: The index of the last cell that was parsed (reviewed).
Returns: cell index
"""
cell, nth = get_cell(path, cells, nth)
if cell['source'][0].startswith('This notebook'):
return nth
return nth - 1
def parse_title(path: str,
cells: list,
nth: int) -> (int, str):
"""
Parse the title in the links cell
path: used only for reporting an error
cells: The content cells (JSON) for the notebook
nth: The index of the last cell that was parsed (reviewed).
Returns: cell index, and title
"""
cell, nth = get_cell(path, cells, nth)
if not cell['source'][0].startswith('# '):
report_error(path, ERROR_TITLE_HEADING, "title cell must start with H1 heading")
title = ''
else:
title = cell['source'][0][2:].strip()
check_sentence_case(path, title)
# H1 title only
if len(cell['source']) == 1:
cell, nth = get_cell(path, cells, nth)
return nth, title
def parse_links(path: str,
cells: list,
nth: int) -> (int, str, str, str):
"""
Parse the links in the links cell
path: used only for reporting an error
cells: The content cells (JSON) for the notebook
nth: The index of the last cell that was parsed (reviewed).
Returns: cell index, and git, colab and workbench links
"""
cell = cells[nth-1]
source = ''
git_link = None
colab_link = None
workbench_link = None
for line in cell['source']:
source += line
if '<a href="https://github.com' in line:
git_link = line.strip()[9:-2].replace('" target="_blank', '')
try:
code = urllib.request.urlopen(git_link).getcode()
except Exception as e:
# if new notebook
derived_link = os.path.join('https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/', path)
if git_link != derived_link:
report_error(path, ERROR_LINK_GIT_BAD, f"bad GitHub link: {git_link}")
if '<a href="https://colab.research.google.com/' in line:
colab_link = 'https://github.com/' + line.strip()[50:-2].replace('" target="_blank', '')
try:
code = urllib.request.urlopen(colab_link).getcode()
except Exception as e:
# if new notebook
derived_link = os.path.join('https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks', path)
if colab_link != derived_link:
report_error(path, ERROR_LINK_COLAB_BAD, f"bad Colab link: {colab_link}")
if '<a href="https://console.cloud.google.com/vertex-ai/workbench/' in line:
workbench_link = line.strip()[91:-2].replace('" target="_blank', '')
try:
code = urllib.request.urlopen(workbench_link).getcode()
except Exception as e:
derived_link = os.path.join('https://console.cloud.google.com/vertex-ai/workbench/deploy-notebook?download_url=https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/main/notebooks/', path)
if colab_link != workbench_link:
report_error(path, ERROR_LINK_WORKBENCH_BAD, f"bad Workbench link: {workbench_link}")
if 'View on GitHub' not in source or not git_link:
report_error(path, ERROR_LINK_GIT_MISSING, 'Missing link for GitHub')
if 'Run in Colab' not in source or not colab_link:
report_error(path, ERROR_LINK_COLAB_MISSING, 'Missing link for Colab')
if 'Open in Vertex AI Workbench' not in source or not workbench_link:
report_error(path, ERROR_LINK_WORKBENCH_MISSING, 'Missing link for Workbench')
return nth, git_link, colab_link, workbench_link
def parse_overview(path: str,
cells: list,
nth: int) -> int:
"""
Parse the overview cell
path: used only for reporting an error
cells: The content cells (JSON) for the notebook
nth: The index of the last cell that was parsed (reviewed).
Returns: cell index
"""
cell, nth = get_cell(path, cells, nth)
if not cell['source'][0].startswith("## Overview"):
report_error(path, ERROR_OVERVIEW_NOTFOUND, "Overview section not found")
return nth
def parse_objective(path: str,
cells: list,
nth: int) -> (int, str, str, str, list):
"""
Parse the objective cell.
Find the description, uses and steps.
path: The path to the notebook.
cells: The content cells (JSON) for the notebook
nth: The index of the last cell that was parsed (reviewed).
Returns cell index, desc, uses, steps and costs
"""
desc = ''
uses = ''
steps = ''
costs = []
cell, nth = get_cell(path, cells, nth)
if not cell['source'][0].startswith("### Objective"):
report_error(path, ERROR_OBJECTIVE_NOTFOUND, "Objective section not found")
return nth, desc, uses, steps, costs
in_desc = True
in_uses = False
in_steps = False
for line in cell['source'][1:]:
if line.startswith('This tutorial uses'):
in_desc = False
in_steps = False
in_uses = True
uses += line
continue
elif line.startswith('The steps performed'):
in_desc = False
in_uses = False
in_steps = True
steps += line
continue
if in_desc:
if len(desc) > 0 and line.strip() == '':
in_desc = False
continue
desc += line
elif in_uses:
sline = line.strip()
if len(sline) == 0:
uses += '\n'
else:
ch = sline[0]
if ch in ['-', '*', '1', '2', '3', '4', '5', '6', '7', '8', '9']:
uses += line
elif in_steps:
sline = line.strip()
if len(sline) == 0:
steps += '\n'
else:
ch = sline[0]
if ch in ['-', '*', '1', '2', '3', '4', '5', '6', '7', '8', '9']:
steps += line
if desc == '':
report_error(path, ERROR_OBJECTIVE_MISSING_DESC, "Objective section missing desc")
else:
desc = desc.lstrip()
sentences = desc.split('.')
if len(sentences) > 1:
desc = sentences[0] + '.\n'
if desc.startswith('In this tutorial, you learn') or desc.startswith('In this notebook, you learn'):
desc = desc[22].upper() + desc[23:]
if uses == '':
report_error(path, ERROR_OBJECTIVE_MISSING_USES, "Objective section missing uses services list")
else:
if 'BigQuery' in uses:
costs.append('BQ')
if 'Vertex' in uses:
costs.append('Vertex')
if 'Dataflow' in uses:
costs.append('Dataflow')
if steps == '':
report_error(path, ERROR_OBJECTIVE_MISSING_STEPS, "Objective section missing steps list")
return nth, desc, uses, steps, costs
def parse_recommendations(path: str,
cells: list,
nth: int) -> int:
"""
Parse the overview cell
path: used only for reporting an error
cells: The content cells (JSON) for the notebook
nth: The index of the last cell that was parsed (reviewed).
Returns: cell index
"""
# (optional) Recommendation
cell, nth = get_cell(path, cells, nth)
if cell['source'][0].startswith("### Recommendations"):
return nth
return nth - 1
def get_cell(path: str,
cells: list,
nth: int) -> (list, int):
"""
Get the next notebook cell.
path: used only for reporting an error
cells: The content cells (JSON) for the notebook
nth: The index of the last cell that was parsed (reviewed).
Returns:
cell: content of the next cell
nth + 1: index of subsequent cell
"""
while empty_cell(path, cells, nth):
nth += 1
@@ -306,14 +619,38 @@ def get_cell(path, cells, nth):
return cell, nth + 1
def empty_cell(path, cells, nth):
def empty_cell(path: str,
cells: list,
nth: int) -> bool:
"""
Check for empty cells
path: used only for reporting an error
cells: The content cells (JSON) for the notebook
nth: The index of the last cell that was parsed (reviewed).
Returns:
bool: whether cell is empty or not
"""
if len(cells[nth]['source']) == 0:
report_error(path, 10, f'empty cell: cell #{nth}')
report_error(path, ERROR_EMPTY_CELL, f'empty cell: cell #{nth}')
return True
else:
return False
def check_text_cell(path, cell):
def check_text_cell(path: str,
cell: list) -> None:
"""
Check text cells for technical writing requirements
1. Product branding names
2. No future tense
3. No 1st person
path: used only for reporting an error
cell: The text cell to review.
"""
branding = {
'Vertex SDK': 'Vertex AI SDK',
@@ -360,35 +697,56 @@ def check_text_cell(path, cell):
}
for line in cell['source']:
if 'TODO' in line:
report_error(path, 14, f'TODO in cell: {line}')
if 'TODO' in line or 'WIP' in line:
report_error(path, ERROR_TWRULE_TODO, f'TODO in cell: {line}')
if 'we ' in line.lower() or "let's" in line.lower() in line.lower():
report_error(path, 15, f'Do not use first person (e.g., we), replace with 2nd person (you): {line}')
report_error(path, ERROR_TWRULE_FIRSTPERSON, f'Do not use first person (e.g., we), replace with 2nd person (you): {line}')
if 'will' in line.lower() or 'would' in line.lower():
report_error(path, 16, f'Do not use future tense (e.g., will), replace with present tense: {line}')
report_error(path, ERROR_TWRULE_FUTURETENSE, f'Do not use future tense (e.g., will), replace with present tense: {line}')
for mistake, brand in branding.items():
if mistake in line:
report_error(path, 27, f"Branding {brand}: {line}")
report_error(path, ERROR_TWRULE_BRANDING, f"Branding {brand}: {line}")
def check_sentence_case(path, heading):
def check_sentence_case(path: str,
heading: str) -> None:
"""
Check that headings are in sentence case
path: used only for reporting an error
heading: the heading to check
"""
ACRONYMS = ['E2E', 'Vertex', 'AutoML', 'ML', 'AI', 'GCP', 'API', 'R', 'CMEK',
'TF', 'TFX', 'TFDV', 'SDK', 'VM', 'CPR', 'NVIDIA', 'ID', 'DASK',
'ARIMA_PLUS', 'KFP', 'I/O', 'GPU', 'Google', 'TensorFlow', 'PyTorch'
]
words = heading.split(' ')
if not words[0][0].isupper():
report_error(path, ERROR_HEADING_CAP, f"heading must start with capitalized word: {words[0]}")
for word in words[1:]:
word = word.replace(':', '').replace('(', '').replace(')', '')
if word in ['E2E', 'Vertex', 'AutoML', 'ML', 'AI', 'GCP', 'API', 'R', 'CMEK', 'TF', 'TFX', 'TFDV', 'SDK',
'VM', 'CPR', 'NVIDIA', 'ID', 'DASK', 'ARIMA_PLUS', 'KFP', 'I/O', 'GPU', 'Google', 'TensorFlow',
'PyTorch'
]:
if word in ACRONYMS:
continue
if word.isupper():
report_error(path, ERROR_HEADING_CASE, f"heading is not sentence case: {word}")
def report_error(notebook, code, msg):
def report_error(notebook: str,
code: str,
errmsg: str) -> None:
"""
Report an error.
If args.errors_codes set, then only report these errors. Otherwise, all errors.
notebook: The notebook path.
code: The error code number.
errmsg: The error message
"""
global num_errors
if args.errors:
@@ -399,78 +757,12 @@ def report_error(notebook, code, msg):
if args.errors_csv:
print(notebook, ',', code)
else:
print(f"{notebook}: ERROR ({code}): {msg}", file=sys.stderr)
print(f"{notebook}: ERROR ({code}): {errmsg}", file=sys.stderr)
num_errors += 1
def parse_objective(path, cell):
desc = ''
in_desc = True
uses = ''
in_uses = False
steps = ''
in_steps = False
costs = []
for line in cell['source'][1:]:
if line.startswith('This tutorial uses'):
in_desc = False
in_steps = False
in_uses = True
uses += line
continue
elif line.startswith('The steps performed'):
in_desc = False
in_uses = False
in_steps = True
steps += line
continue
if in_desc:
if len(desc) > 0 and line.strip() == '':
in_desc = False
continue
desc += line
elif in_uses:
sline = line.strip()
if len(sline) == 0:
uses += '\n'
else:
ch = sline[0]
if ch in ['-', '*', '1', '2', '3', '4', '5', '6', '7', '8', '9']:
uses += line
elif in_steps:
sline = line.strip()
if len(sline) == 0:
steps += '\n'
else:
ch = sline[0]
if ch in ['-', '*', '1', '2', '3', '4', '5', '6', '7', '8', '9']:
steps += line
if desc == '':
report_error(path, 17, "Objective section missing desc")
else:
desc = desc.lstrip()
sentences = desc.split('.')
if len(sentences) > 1:
desc = sentences[0] + '.\n'
if desc.startswith('In this tutorial, you learn') or desc.startswith('In this notebook, you learn'):
desc = desc[22].upper() + desc[23:]
if uses == '':
report_error(path, 18, "Objective section missing uses services list")
else:
if 'BigQuery' in uses:
costs.append('BQ')
if 'Vertex' in uses:
costs.append('Vertex')
if 'Dataflow' in uses:
costs.append('Dataflow')
if steps == '':
report_error(path, 19, "Objective section missing steps list")
return desc, uses, steps, costs
def add_index(path, tag, title, desc, uses, steps, git_link, colab_link, workbench_link):
global last_tag