Add basic options and metadata (#214)

This commit is contained in:
kvid 2021-08-25 19:46:37 +02:00 committed by GitHub
parent e212fc9058
commit 92354e6852
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 151 additions and 41 deletions

View File

@ -3,6 +3,12 @@
## Main sections ## Main sections
```yaml ```yaml
metadata: # dictionary of meta-information describing the harness
<key> : <value> # any number of key value pairs (see below)
...
options: # dictionary of common attributes for the whole harness
<str> : <value> # optional harness attributes (see below)
...
connectors: # dictionary of all used connectors connectors: # dictionary of all used connectors
<str> : # unique connector designator/name <str> : # unique connector designator/name
... # connector attributes (see below) ... # connector attributes (see below)
@ -31,6 +37,55 @@ additional_bom_items: # custom items to add to BOM
``` ```
## Metadata entries
```yaml
# Meta-information describing the harness
# Each key/value pair replaces all key references in
# the HTML output template with the belonging value.
# Typical keys are 'title', 'description', and 'notes',
# but any key is accepted. Unused keys are ignored.
<key> : <value> # Any valid YAML syntax is accepted
# If no value is specified for 'title', then the
# output filename without extension is used.
```
## Options
```yaml
# Common attributes for the whole harness.
# All entries are optional and have default values.
# Background color of diagram and HTML output
bgcolor: <color> # Default = 'WH'
# Background color of other diagram elements
bgcolor_node: <color> # Default = 'WH'
bgcolor_connector: <color> # Default = bgcolor_node
bgcolor_cable: <color> # Default = bgcolor_node
bgcolor_bundle: <color> # Default = bgcolor_cable
# How to display colors as text in the diagram
# 'full' : Lowercase full color name
# 'FULL' : Uppercase full color name
# 'hex' : Lowercase hexadecimal values
# 'HEX' : Uppercase hexadecimal values
# 'short': Lowercase short color name
# 'SHORT': Uppercase short color name
# 'ger' : Lowercase short German color name
# 'GER' : Uppercase short German color name
color_mode: <str> # Default = 'SHORT'
# Fontname to use in diagram and HTML output
fontname: <str> # Default = 'arial'
# If True, show only a BOM entry reference together with basic info
# about additional components inside the diagram node (connector/cable box).
# If False, show all info about additional components inside the diagram node.
mini_bom_mode: <bool> # Default = True
```
## Connector attributes ## Connector attributes
```yaml ```yaml

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Optional, List, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
from dataclasses import dataclass, field, InitVar from dataclasses import dataclass, field, InitVar
from pathlib import Path from pathlib import Path
@ -20,6 +20,7 @@ ConnectorMultiplier = PlainText # = Literal['pincount', 'populated']
CableMultiplier = PlainText # = Literal['wirecount', 'terminations', 'length', 'total_length'] CableMultiplier = PlainText # = Literal['wirecount', 'terminations', 'length', 'total_length']
ImageScale = PlainText # = Literal['false', 'true', 'width', 'height', 'both'] ImageScale = PlainText # = Literal['false', 'true', 'width', 'height', 'both']
Color = PlainText # Two-letter color name = Literal[wv_colors._color_hex.keys()] Color = PlainText # Two-letter color name = Literal[wv_colors._color_hex.keys()]
ColorMode = PlainText # = Literal['full', 'FULL', 'hex', 'HEX', 'short', 'SHORT', 'ger', 'GER']
ColorScheme = PlainText # Color scheme name = Literal[wv_colors.COLOR_CODES.keys()] ColorScheme = PlainText # Color scheme name = Literal[wv_colors.COLOR_CODES.keys()]
# Type combinations # Type combinations
@ -30,6 +31,33 @@ Wire = Union[int, PlainText] # Wire number or Literal['s'] for shield
NoneOrMorePinIndices = Union[PinIndex, Tuple[PinIndex, ...], None] # None, one, or a tuple of zero-based pin indices NoneOrMorePinIndices = Union[PinIndex, Tuple[PinIndex, ...], None] # None, one, or a tuple of zero-based pin indices
OneOrMoreWires = Union[Wire, Tuple[Wire, ...]] # One or a tuple of wires OneOrMoreWires = Union[Wire, Tuple[Wire, ...]] # One or a tuple of wires
# Metadata can contain whatever is needed by the HTML generation/template.
MetadataKeys = PlainText # Literal['title', 'description', 'notes', ...]
class Metadata(dict):
pass
@dataclass
class Options:
fontname: PlainText = 'arial'
bgcolor: Color = 'WH'
bgcolor_node: Optional[Color] = 'WH'
bgcolor_connector: Optional[Color] = None
bgcolor_cable: Optional[Color] = None
bgcolor_bundle: Optional[Color] = None
color_mode: ColorMode = 'SHORT'
mini_bom_mode: bool = True
def __post_init__(self):
if not self.bgcolor_node:
self.bgcolor_node = self.bgcolor
if not self.bgcolor_connector:
self.bgcolor_connector = self.bgcolor_node
if not self.bgcolor_cable:
self.bgcolor_cable = self.bgcolor_node
if not self.bgcolor_bundle:
self.bgcolor_bundle = self.bgcolor_cable
@dataclass @dataclass
class Image: class Image:

View File

@ -4,13 +4,14 @@
from graphviz import Graph from graphviz import Graph
from collections import Counter from collections import Counter
from typing import List, Union from typing import List, Union
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from itertools import zip_longest from itertools import zip_longest
import re import re
from wireviz import wv_colors, __version__, APP_NAME, APP_URL from wireviz import wv_colors, __version__, APP_NAME, APP_URL
from wireviz.DataClasses import Connector, Cable from wireviz.DataClasses import Metadata, Options, Connector, Cable
from wireviz.wv_colors import get_color_hex from wireviz.wv_colors import get_color_hex, translate_color
from wireviz.wv_gv_html import nested_html_table, html_colorbar, html_image, \ from wireviz.wv_gv_html import nested_html_table, html_colorbar, html_image, \
html_caption, remove_links, html_line_breaks html_caption, remove_links, html_line_breaks
from wireviz.wv_bom import manufacturer_info_field, component_table_entry, \ from wireviz.wv_bom import manufacturer_info_field, component_table_entry, \
@ -20,11 +21,12 @@ from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, flatten2d, \
open_file_read, open_file_write open_file_read, open_file_write
@dataclass
class Harness: class Harness:
metadata: Metadata
options: Options
def __init__(self): def __post_init__(self):
self.color_mode = 'SHORT'
self.mini_bom_mode = True
self.connectors = {} self.connectors = {}
self.cables = {} self.cables = {}
self._bom = [] # Internal Cache for generated bom self._bom = [] # Internal Cache for generated bom
@ -91,18 +93,19 @@ class Harness:
dot = Graph() dot = Graph()
dot.body.append(f'// Graph generated by {APP_NAME} {__version__}') dot.body.append(f'// Graph generated by {APP_NAME} {__version__}')
dot.body.append(f'// {APP_URL}') dot.body.append(f'// {APP_URL}')
font = 'arial'
dot.attr('graph', rankdir='LR', dot.attr('graph', rankdir='LR',
ranksep='2', ranksep='2',
bgcolor='white', bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"),
nodesep='0.33', nodesep='0.33',
fontname=font) fontname=self.options.fontname)
dot.attr('node', shape='record', dot.attr('node',
shape='none',
width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label.
style='filled', style='filled',
fillcolor='white', fillcolor=wv_colors.translate_color(self.options.bgcolor_node, "HEX"),
fontname=font) fontname=self.options.fontname)
dot.attr('edge', style='bold', dot.attr('edge', style='bold',
fontname=font) fontname=self.options.fontname)
# prepare ports on connectors depending on which side they will connect # prepare ports on connectors depending on which side they will connect
for _, cable in self.cables.items(): for _, cable in self.cables.items():
@ -126,7 +129,8 @@ class Harness:
[html_line_breaks(connector.type), [html_line_breaks(connector.type),
html_line_breaks(connector.subtype), html_line_breaks(connector.subtype),
f'{connector.pincount}-pin' if connector.show_pincount else None, f'{connector.pincount}-pin' if connector.show_pincount else None,
connector.color, html_colorbar(connector.color)], translate_color(connector.color, self.options.color_mode) if connector.color else None,
html_colorbar(connector.color)],
'<!-- connector table -->' if connector.style != 'simple' else None, '<!-- connector table -->' if connector.style != 'simple' else None,
[html_image(connector.image)], [html_image(connector.image)],
[html_caption(connector.image)]] [html_caption(connector.image)]]
@ -148,7 +152,7 @@ class Harness:
pinhtml.append(f' <td>{pinlabel}</td>') pinhtml.append(f' <td>{pinlabel}</td>')
if connector.pincolors: if connector.pincolors:
if pincolor in wv_colors._color_hex.keys(): if pincolor in wv_colors._color_hex.keys():
pinhtml.append(f' <td sides="tbl">{pincolor}</td>') pinhtml.append(f' <td sides="tbl">{translate_color(pincolor, self.options.color_mode)}</td>')
pinhtml.append( ' <td sides="tbr">') pinhtml.append( ' <td sides="tbr">')
pinhtml.append( ' <table border="0" cellborder="1"><tr>') pinhtml.append( ' <table border="0" cellborder="1"><tr>')
pinhtml.append(f' <td bgcolor="{wv_colors.translate_color(pincolor, "HEX")}" width="8" height="8" fixedsize="true"></td>') pinhtml.append(f' <td bgcolor="{wv_colors.translate_color(pincolor, "HEX")}" width="8" height="8" fixedsize="true"></td>')
@ -166,7 +170,8 @@ class Harness:
html = [row.replace('<!-- connector table -->', '\n'.join(pinhtml)) for row in html] html = [row.replace('<!-- connector table -->', '\n'.join(pinhtml)) for row in html]
html = '\n'.join(html) html = '\n'.join(html)
dot.node(connector.name, label=f'<\n{html}\n>', shape='none', margin='0', style='filled', fillcolor='white') dot.node(connector.name, label=f'<\n{html}\n>', shape='box', style='filled',
fillcolor=translate_color(self.options.bgcolor_connector, "HEX"))
if len(connector.loops) > 0: if len(connector.loops) > 0:
dot.attr('edge', color='#000000:#ffffff:#000000') dot.attr('edge', color='#000000:#ffffff:#000000')
@ -211,7 +216,8 @@ class Harness:
f'{cable.gauge} {cable.gauge_unit}{awg_fmt}' if cable.gauge else None, f'{cable.gauge} {cable.gauge_unit}{awg_fmt}' if cable.gauge else None,
'+ S' if cable.shield else None, '+ S' if cable.shield else None,
f'{cable.length} {cable.length_unit}' if cable.length > 0 else None, f'{cable.length} {cable.length_unit}' if cable.length > 0 else None,
cable.color, html_colorbar(cable.color)], translate_color(cable.color, self.options.color_mode) if cable.color else None,
html_colorbar(cable.color)],
'<!-- wire table -->', '<!-- wire table -->',
[html_image(cable.image)], [html_image(cable.image)],
[html_caption(cable.image)]] [html_caption(cable.image)]]
@ -232,7 +238,7 @@ class Harness:
wireinfo = [] wireinfo = []
if cable.show_wirenumbers: if cable.show_wirenumbers:
wireinfo.append(str(i)) wireinfo.append(str(i))
colorstr = wv_colors.translate_color(connection_color, self.color_mode) colorstr = wv_colors.translate_color(connection_color, self.options.color_mode)
if colorstr: if colorstr:
wireinfo.append(colorstr) wireinfo.append(colorstr)
if cable.wirelabels: if cable.wirelabels:
@ -332,9 +338,11 @@ class Harness:
to_string = '' to_string = ''
html = [row.replace(f'<!-- {connection.via_port}_out -->', to_string) for row in html] html = [row.replace(f'<!-- {connection.via_port}_out -->', to_string) for row in html]
style, bgcolor = ('filled,dashed', self.options.bgcolor_bundle) if cable.category == 'bundle' else \
('filled', self.options.bgcolor_cable)
html = '\n'.join(html) html = '\n'.join(html)
dot.node(cable.name, label=f'<\n{html}\n>', shape='box', dot.node(cable.name, label=f'<\n{html}\n>', shape='box',
style='filled,dashed' if cable.category == 'bundle' else '', margin='0', fillcolor='white') style=style, fillcolor=translate_color(bgcolor, "HEX"))
return dot return dot
@ -368,7 +376,7 @@ class Harness:
with open_file_write(f'{filename}.bom.tsv') as file: with open_file_write(f'{filename}.bom.tsv') as file:
file.write(tuplelist2tsv(bomlist)) file.write(tuplelist2tsv(bomlist))
# HTML output # HTML output
generate_html_output(filename, bomlist) generate_html_output(filename, bomlist, self.metadata, self.options)
def bom(self): def bom(self):
if not self._bom: if not self._bom:

View File

@ -13,6 +13,7 @@ if __name__ == '__main__':
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from wireviz import __version__ from wireviz import __version__
from wireviz.DataClasses import Metadata, Options
from wireviz.Harness import Harness from wireviz.Harness import Harness
from wireviz.wv_helper import expand, open_file_read from wireviz.wv_helper import expand, open_file_read
@ -34,7 +35,12 @@ def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, st
yaml_data = yaml.safe_load(yaml_input) yaml_data = yaml.safe_load(yaml_input)
harness = Harness() harness = Harness(
metadata = Metadata(**yaml_data.get('metadata', {})),
options = Options(**yaml_data.get('options', {})),
)
if 'title' not in harness.metadata:
harness.metadata['title'] = Path(file_out).stem
# add items # add items
sections = ['connectors', 'cables', 'connections'] sections = ['connectors', 'cables', 'connections']

View File

@ -6,6 +6,7 @@ from itertools import groupby
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
from wireviz.DataClasses import AdditionalComponent, Connector, Cable from wireviz.DataClasses import AdditionalComponent, Connector, Cable
from wireviz.wv_colors import translate_color
from wireviz.wv_gv_html import html_line_breaks from wireviz.wv_gv_html import html_line_breaks
from wireviz.wv_helper import clean_whitespace from wireviz.wv_helper import clean_whitespace
@ -32,7 +33,7 @@ def get_additional_component_table(harness: "Harness", component: Union[Connecto
'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier), 'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier),
'unit': part.unit, 'unit': part.unit,
} }
if harness.mini_bom_mode: if harness.options.mini_bom_mode:
id = get_bom_index(harness.bom(), bom_entry_key({**asdict(part), 'description': part.description})) id = get_bom_index(harness.bom(), bom_entry_key({**asdict(part), 'description': part.description}))
rows.append(component_table_entry(f'#{id} ({part.type.rstrip()})', **common_args)) rows.append(component_table_entry(f'#{id} ({part.type.rstrip()})', **common_args))
else: else:
@ -69,7 +70,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
+ (f', {connector.type}' if connector.type else '') + (f', {connector.type}' if connector.type else '')
+ (f', {connector.subtype}' if connector.subtype else '') + (f', {connector.subtype}' if connector.subtype else '')
+ (f', {connector.pincount} pins' if connector.show_pincount else '') + (f', {connector.pincount} pins' if connector.show_pincount else '')
+ (f', {connector.color}' if connector.color else '')) + (f', {translate_color(connector.color, harness.options.color_mode)}' if connector.color else ''))
bom_entries.append({ bom_entries.append({
'description': description, 'designators': connector.name if connector.show_name else None, 'description': description, 'designators': connector.name if connector.show_name else None,
**optional_fields(connector), **optional_fields(connector),
@ -88,7 +89,8 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
+ (f', {cable.type}' if cable.type else '') + (f', {cable.type}' if cable.type else '')
+ (f', {cable.wirecount}') + (f', {cable.wirecount}')
+ (f' x {cable.gauge} {cable.gauge_unit}' if cable.gauge else ' wires') + (f' x {cable.gauge} {cable.gauge_unit}' if cable.gauge else ' wires')
+ (' shielded' if cable.shield else '')) + ( ' shielded' if cable.shield else '')
+ (f', {translate_color(cable.color, harness.options.color_mode)}' if cable.color else ''))
bom_entries.append({ bom_entries.append({
'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, 'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None,
**optional_fields(cable), **optional_fields(cable),
@ -99,7 +101,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
description = ('Wire' description = ('Wire'
+ (f', {cable.type}' if cable.type else '') + (f', {cable.type}' if cable.type else '')
+ (f', {cable.gauge} {cable.gauge_unit}' if cable.gauge else '') + (f', {cable.gauge} {cable.gauge_unit}' if cable.gauge else '')
+ (f', {color}' if color else '')) + (f', {translate_color(color, harness.options.color_mode)}' if color else ''))
bom_entries.append({ bom_entries.append({
'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, 'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None,
**{k: index_if_list(v, index) for k, v in optional_fields(cable).items()}, **{k: index_if_list(v, index) for k, v in optional_fields(cable).items()},

View File

@ -2,21 +2,28 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from pathlib import Path from pathlib import Path
from typing import List, Union
import re import re
from wireviz import __version__, APP_NAME, APP_URL from wireviz import __version__, APP_NAME, APP_URL, wv_colors
from wireviz.DataClasses import Metadata, Options
from wireviz.wv_helper import flatten2d, open_file_read, open_file_write from wireviz.wv_helper import flatten2d, open_file_read, open_file_write
def generate_html_output(filename: (str, Path), bom_list): def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]], metadata: Metadata, options: Options):
with open_file_write(f'{filename}.html') as file: with open_file_write(f'{filename}.html') as file:
file.write('<!DOCTYPE html>\n') file.write('<!DOCTYPE html>\n')
file.write('<html lang="en"><head>\n') file.write('<html lang="en"><head>\n')
file.write(' <meta charset="UTF-8">\n') file.write(' <meta charset="UTF-8">\n')
file.write(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n') file.write(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n')
file.write(f' <title>{APP_NAME} Diagram and BOM</title>\n') file.write(f' <title>{metadata["title"]}</title>\n')
file.write('</head><body style="font-family:Arial">\n') file.write(f'</head><body style="font-family:{options.fontname};background-color:'
f'{wv_colors.translate_color(options.bgcolor, "HEX")}">\n')
file.write('<h1>Diagram</h1>') file.write(f'<h1>{metadata["title"]}</h1>\n')
description = metadata.get('description')
if description:
file.write(f'<p>{description}</p>\n')
file.write('<h2>Diagram</h2>\n')
with open_file_read(f'{filename}.svg') as svg: with open_file_read(f'{filename}.svg') as svg:
file.write(re.sub( file.write(re.sub(
'^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>', '^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>',
@ -25,20 +32,24 @@ def generate_html_output(filename: (str, Path), bom_list):
for svgdata in svg: for svgdata in svg:
file.write(svgdata) file.write(svgdata)
file.write('<h1>Bill of Materials</h1>') file.write('<h2>Bill of Materials</h2>\n')
listy = flatten2d(bom_list) listy = flatten2d(bom_list)
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">') file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">\n')
file.write('<tr>') file.write(' <tr>\n')
for item in listy[0]: for item in listy[0]:
file.write(f'<th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>') file.write(f' <th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>\n')
file.write('</tr>') file.write(' </tr>\n')
for row in listy[1:]: for row in listy[1:]:
file.write('<tr>') file.write(' <tr>\n')
for i, item in enumerate(row): for i, item in enumerate(row):
item_str = item.replace('\u00b2', '&sup2;') item_str = item.replace('\u00b2', '&sup2;')
align = 'text-align:right; ' if listy[0][i] == 'Qty' else '' align = '; text-align:right' if listy[0][i] == 'Qty' else ''
file.write(f'<td style="{align}border:1px solid #000000; padding: 4px">{item_str}</td>') file.write(f' <td style="border:1px solid #000000; padding: 4px{align}">{item_str}</td>\n')
file.write('</tr>') file.write(' </tr>\n')
file.write('</table>') file.write('</table>\n')
file.write('</body></html>') notes = metadata.get('notes')
if notes:
file.write(f'<h2>Notes</h2>\n<p>{notes}</p>\n')
file.write('</body></html>\n')