Skip to content

Commit d82939b

Browse files
committed
feat: added generation of simplifed data model in Excel.
1 parent e4eb45f commit d82939b

File tree

6 files changed

+346
-7
lines changed

6 files changed

+346
-7
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
index.html
22
upload/
3-
build/**/*.svg
3+
build/
44
.DS_Store
55
scripts/__pycache__/*
66
spec/**/*.generated.md

assets/logo-dark-margin.png

23.7 KB
Loading

assets/logo-dark.svg

+58
Loading

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ invoke==2.2.0
33
jsonref==1.1.0
44
Markdown==3.7
55
PyYAML==6.0.2
6+
openpyxl==3.1.5

scripts/excel.py

+259
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import os
2+
import sys
3+
import yaml
4+
import jsonref
5+
import logging
6+
from openpyxl import Workbook
7+
from openpyxl.drawing.image import Image
8+
from openpyxl.styles import Alignment,PatternFill,Font
9+
from openpyxl.drawing.spreadsheet_drawing import AnchorMarker, TwoCellAnchor, OneCellAnchor
10+
11+
12+
status = " (DRAFT)"
13+
14+
def generate_excel(ws, schema, types):
15+
"""
16+
@param ws: the worksheet to write to
17+
@param schema: the OpenAPI schema
18+
@param types: list of types to include in the worksheet
19+
20+
This function generates an Excel worksheet from an OpenAPI schema.
21+
It starts with the types specified in the list and follows all
22+
references to other types.
23+
"""
24+
25+
def format(item, style):
26+
alignment = None
27+
28+
#if style.get("word_wrap"):
29+
# alignment = Alignment(vertical="top", wrap_text=style.get("word_wrap"))
30+
#else:
31+
# if style.get("vertical_align"):
32+
# alignment = Alignment(vertical=style.get("vertical_align"))
33+
# logging.debug(f"Setting vertical alignment to {style.get('vertical_align')}")
34+
# logging.debug(alignment)
35+
36+
font = Font(name=style.get("font"), size=style.get("size"), bold=style.get("bold"), color=style.get("fgcolor"))
37+
38+
if style.get("bgcolor"):
39+
fill = PatternFill(start_color=style.get("bgcolor"), end_color=style.get("bgcolor"), fill_type="solid")
40+
else:
41+
fill = None
42+
43+
if not isinstance(item, tuple):
44+
item = [item]
45+
for cell in item:
46+
if alignment:
47+
cell.alignment = alignment
48+
if font:
49+
cell.font = font
50+
if fill:
51+
cell.fill = fill
52+
53+
# Append the title and header rows
54+
ws.append(["PACT Simplified Tech Specs" + status, "", "", "", "", "", "", "", "", "", ""])
55+
ws.append([schema["info"]["version"], "", "", "", "", "", "", "", "", ""])
56+
ws.append([
57+
"Property", # Column A
58+
"Mandatory?", # Column B
59+
"Methodology Attribute Name", # Column C
60+
"Link to Methodology", # Column D
61+
"User Friendly Description of Attribute", # Column E
62+
"Unit", # Column F
63+
"Accepted Value(s)", # Column G
64+
"Example 1 (Dummy data)", # Column H
65+
"Example 2 (Dummy data)", # Column I
66+
"Example 3 (Dummy data)" # Column J
67+
])
68+
# TODO: descriptions
69+
ws.append(["description", "description", "", "", "", "", "", "", "", ""])
70+
71+
# Set cell widths
72+
ws.column_dimensions["A"].width = 30
73+
ws.column_dimensions["B"].width = 15
74+
ws.column_dimensions["C"].width = 30
75+
ws.column_dimensions["D"].width = 15
76+
ws.column_dimensions["E"].width = 60
77+
ws.column_dimensions["F"].width = 15
78+
ws.column_dimensions["G"].width = 20
79+
ws.column_dimensions["H"].width = 30
80+
ws.column_dimensions["I"].width = 30
81+
ws.column_dimensions["J"].width = 30
82+
83+
84+
fontname = "Aptos Narrow"
85+
color_title = "08094C"
86+
color_table_header = "2A4879"
87+
color_header = "489F81"
88+
89+
title_style = dict(font = fontname, size = 16, bold = False, bgcolor = color_title, fgcolor = "FFFFFF")
90+
subtitle_style = dict(font = fontname, size = 16, bold = False, bgcolor = color_title, fgcolor = "FFFFFF")
91+
header_style = dict(font = fontname, bgcolor = color_table_header, fgcolor = "FFFFFF")
92+
heading_style = dict(font = fontname, bold = True, bgcolor = color_header, fgcolor = "FFFFFF")
93+
normal_style = dict(font = fontname)
94+
bold_style = dict(font = fontname, bold = True)
95+
wrap_style = dict(font = fontname, word_wrap = True)
96+
97+
# format the first rows with the styles
98+
format(ws[1], title_style)
99+
format(ws[2], subtitle_style)
100+
format(ws[3], header_style)
101+
format(ws[4], header_style)
102+
format(ws["B4"], normal_style)
103+
104+
# Inner function to get a succinct type description
105+
def get_type_description(info):
106+
type_description = ""
107+
if info.get("type", "") == "array":
108+
type_description = "list of " + get_type_description(info["items"])
109+
if info.get("type", "") == "object":
110+
type_description = "object"
111+
type_description += " " + info.get("format", "") + "\n"
112+
type_description += " " + info.get("comment", "") + "\n"
113+
type_description += "|".join(info.get("enum", [])) + "\n"
114+
type_description = type_description.strip()
115+
if (type_description == ""):
116+
type_description = info.get("type","")
117+
elif not info.get("type","") in ["object","array"]:
118+
type_description += "\n(" + info.get("type","") + ")"
119+
return type_description
120+
121+
# Inner function to write a property to the worksheet
122+
def write_property(name, info, parent, level):
123+
# Extract the type and description of the property
124+
type = info.get("type", "")
125+
126+
if type == "object":
127+
write_type(name, info, level + 1)
128+
return
129+
130+
type_description = get_type_description(info)
131+
description = info.get("summary") or info.get("description") or "N/A"
132+
examples = info.get("examples", []) + ['','','']
133+
mandatory = name in parent.get("required", [])
134+
135+
# Append a row to the worksheet
136+
ws.append([
137+
name,
138+
"M" if mandatory else "O",
139+
info.get("title", name),
140+
"-",
141+
description.rstrip(), # remove last newline from the description
142+
"-",
143+
type_description,
144+
str(examples[0]),
145+
str(examples[1]),
146+
str(examples[2])
147+
])
148+
# Indent the first cell of the row just added
149+
format(ws[ws.max_row], normal_style)
150+
format(ws[ws.max_row][0], bold_style)
151+
ws[ws.max_row][0].alignment = Alignment(indent=level)
152+
153+
if info.get("type", None) == "array" and info["items"].get("type") == "object":
154+
logging.debug(f"Writing array for {name} at level {level}")
155+
write_type(None, info["items"], level + 1)
156+
157+
158+
159+
# Inner function to write a type to the worksheet
160+
def write_type(name, info, level=0):
161+
logging.debug(f"Writing type {name} at level {level}")
162+
if info.get("title") and name:
163+
# Append a row for the type itself and set background color to blue
164+
ws.append([name + ": " + info["title"], "", "", "", "", "", "", "", "", ""])
165+
format(ws[ws.max_row], heading_style)
166+
167+
for prop_name, prop_info in info.get("properties", {}).items():
168+
# Extract the type and description of the property
169+
logging.debug(f"Writing property {prop_name}")
170+
write_property(prop_name, prop_info, info, level)
171+
172+
173+
# Find the specified types in the schema
174+
for name in types:
175+
type = schema["components"]["schemas"][name]
176+
write_type(name, type)
177+
178+
179+
# Set word wrap for all cells in the description colum
180+
for cell in ws["A"] :
181+
cell.alignment = Alignment(vertical="top", indent=cell.alignment.indent)
182+
for cell in ws["B"] + ws["C"] + ws["D"] + ws["E"] + ws["F"] + ws["G"] + ws["H"] + ws["I"] + ws["J"]:
183+
cell.alignment = Alignment(vertical="top")
184+
for cell in ws["E"]:
185+
cell.alignment = Alignment(wrap_text=True, vertical="top")
186+
for cell in ws["G"]:
187+
cell.alignment = Alignment(wrap_text=True, vertical="top")
188+
189+
# Insert the PACT logo
190+
img = Image("./assets/logo-dark-margin.png")
191+
img.width = img.width / 5
192+
img.height = img.height / 5
193+
ws.add_image(img, "A1")
194+
ws.row_dimensions[1].height = img.height * 4 / 3
195+
ws["A1"].alignment = Alignment(vertical="bottom")
196+
197+
198+
def openapi_to_excel(input_path, output_path, title, types):
199+
"""
200+
@param input_path: path to the OpenAPI schema file
201+
@param output_path: path to the output Excel file
202+
@param title: title of the Excel worksheet
203+
@param types: list of types to include in the Excel worksheet
204+
"""
205+
206+
# Load the schema from the file
207+
with open(input_path) as file:
208+
schema = yaml.safe_load(file)
209+
schema = jsonref.replace_refs(schema, merge_props=True)
210+
211+
# Create a new workbook and select the active worksheet
212+
wb = Workbook()
213+
ws = wb.active
214+
ws.title = title
215+
ws.sheet_view.zoomScale = 140
216+
217+
# Generate the worksheet
218+
generate_excel(ws, schema, types)
219+
wb.save(output_path)
220+
221+
222+
223+
if __name__ == "__main__":
224+
# Get command line args
225+
if len(sys.argv) < 2:
226+
print("Usage: python3 generate-excel.py <input-path>")
227+
print("This script generates an Excel file from a OpenAPI schema.")
228+
print("")
229+
print("Example:")
230+
print("python3 generate-excel pact-openapi-2.2.1-wip.yaml")
231+
print()
232+
exit()
233+
input_path = sys.argv[1]
234+
if not os.path.exists(input_path):
235+
print("File not found:", input_path)
236+
exit()
237+
status = " (DRAFT)"
238+
if (len(sys.argv) >= 3):
239+
status = " (" + sys.argv[2].upper() + ")"
240+
241+
# Load the schema from the file
242+
with open(input_path) as file:
243+
schema1 = yaml.safe_load(file)
244+
schema = jsonref.replace_refs(schema1, merge_props=True)
245+
246+
# Create a new workbook and select the active worksheet
247+
wb = Workbook()
248+
ws = wb.active
249+
ws.title = "PACT Simplified Data Model"
250+
ws.sheet_view.zoomScale = 140
251+
252+
generate_excel(ws, schema, ["ProductFootprint"])
253+
254+
# Save the workbook to a file
255+
output_path = os.path.basename(input_path)
256+
output_path = output_path.replace('-openapi-', '-simplified-model-')
257+
output_path = output_path.replace(".yaml", "") + ".xlsx"
258+
output_path = os.path.join(os.path.dirname(input_path), output_path)
259+
wb.save(output_path)

tasks.py

+27-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import os
44
import sys
55
import subprocess
6+
import logging
67
import scripts.openapi
8+
import scripts.excel
79
from scripts.patchup import patchup, parse_bikeshed_file
810
from scripts.build import Dependency, fileset
911
from invoke import task
@@ -23,6 +25,9 @@ def run(cmd):
2325
print(cmd)
2426
os.system(cmd)
2527

28+
# Set up logging
29+
logging.basicConfig(level=logging.INFO)
30+
2631
# Set up a custom exception handler to print friendly errors to stderr
2732
def error_handler(exctype, value, traceback):
2833
print(f"Error: {value}", file=sys.stderr)
@@ -35,6 +40,12 @@ def error_handler(exctype, value, traceback):
3540
def is_repo_pristine(directory = None):
3641
return subprocess.check_output("git diff --stat", shell=True, cwd=directory).decode(encoding="utf-8") == ""
3742

43+
def build_task(dependencies, task):
44+
for dependency in dependencies:
45+
if dependency.outdated():
46+
dependency.makedir()
47+
task(dependency.sources[0], dependency.target)
48+
3849
# Build Bikeshed files, and patch up title and status based on metadata
3950
def build_bikeshed(dependencies):
4051
for dependency in dependencies:
@@ -63,13 +74,23 @@ def build(c):
6374
"""
6475
Build the specifcation (all versions) from the source files.
6576
"""
66-
deps = [
77+
build_task([
6778
Dependency("spec/v3/data-model.generated.md", ["spec/v3/openapi.yaml"]),
68-
Dependency("spec/v2/data-model.generated.md", ["spec/v2/openapi.yaml"])
69-
]
70-
for dep in deps:
71-
if dep.outdated():
72-
scripts.openapi.generate_data_model(dep.sources[0], dep.target)
79+
Dependency("spec/v2/data-model.generated.md", ["spec/v2/openapi.yaml"])],
80+
scripts.openapi.generate_data_model
81+
)
82+
build_task([
83+
Dependency("build/v2/pact-simplified.xlsx", ["spec/v2/openapi.yaml"]),
84+
Dependency("build/v3/pact-simplified.xlsx", ["spec/v3/openapi.yaml"])],
85+
lambda source, target: scripts.excel.openapi_to_excel(source, target, "PACT Simplified Data Model", ["ProductFootprint"])
86+
)
87+
# deps = [
88+
# Dependency("spec/v3/data-model.generated.md", ["spec/v3/openapi.yaml"]),
89+
# Dependency("spec/v2/data-model.generated.md", ["spec/v2/openapi.yaml"])
90+
# ]
91+
# for dep in deps:
92+
# if dep.outdated():
93+
# scripts.openapi.generate_data_model(dep.sources[0], dep.target)
7394

7495
build_bikeshed([
7596
Dependency("build/faq/index.html", ["spec/faq/index.bs"]),

0 commit comments

Comments
 (0)