mirror of
https://github.com/GoogleCloudPlatform/vertex-ai-samples.git
synced 2026-09-26 14:42:04 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b7bc1a396 | ||
|
|
3235c5cf86 | ||
|
|
45d50e6520 | ||
|
|
c1b56fbaf8 |
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user