From 92354e68522d6dad0e2474455f640962603c632f Mon Sep 17 00:00:00 2001 From: kvid Date: Wed, 25 Aug 2021 19:46:37 +0200 Subject: [PATCH] Add basic options and metadata (#214) --- docs/syntax.md | 55 ++++++++++++++++++++++++++++++++++++++ src/wireviz/DataClasses.py | 30 ++++++++++++++++++++- src/wireviz/Harness.py | 46 ++++++++++++++++++------------- src/wireviz/wireviz.py | 8 +++++- src/wireviz/wv_bom.py | 10 ++++--- src/wireviz/wv_html.py | 43 ++++++++++++++++++----------- 6 files changed, 151 insertions(+), 41 deletions(-) diff --git a/docs/syntax.md b/docs/syntax.md index 2d557511..4a7a52ac 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -3,6 +3,12 @@ ## Main sections ```yaml +metadata: # dictionary of meta-information describing the harness + : # any number of key value pairs (see below) + ... +options: # dictionary of common attributes for the whole harness + : # optional harness attributes (see below) + ... connectors: # dictionary of all used connectors : # unique connector designator/name ... # 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. + : # 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: # Default = 'WH' + + # Background color of other diagram elements + bgcolor_node: # Default = 'WH' + bgcolor_connector: # Default = bgcolor_node + bgcolor_cable: # Default = bgcolor_node + bgcolor_bundle: # 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: # Default = 'SHORT' + + # Fontname to use in diagram and HTML output + fontname: # 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: # Default = True +``` + ## Connector attributes ```yaml diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py index 87dc93ba..e80437b2 100644 --- a/src/wireviz/DataClasses.py +++ b/src/wireviz/DataClasses.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- 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 pathlib import Path @@ -20,6 +20,7 @@ CableMultiplier = PlainText # = Literal['wirecount', 'terminations', 'length', 'total_length'] ImageScale = PlainText # = Literal['false', 'true', 'width', 'height', 'both'] 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()] # Type combinations @@ -30,6 +31,33 @@ 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 +# 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 class Image: diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index 29f5556b..39180500 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -4,13 +4,14 @@ from graphviz import Graph from collections import Counter from typing import List, Union +from dataclasses import dataclass from pathlib import Path from itertools import zip_longest import re from wireviz import wv_colors, __version__, APP_NAME, APP_URL -from wireviz.DataClasses import Connector, Cable -from wireviz.wv_colors import get_color_hex +from wireviz.DataClasses import Metadata, Options, Connector, Cable +from wireviz.wv_colors import get_color_hex, translate_color from wireviz.wv_gv_html import nested_html_table, html_colorbar, html_image, \ html_caption, remove_links, html_line_breaks from wireviz.wv_bom import manufacturer_info_field, component_table_entry, \ @@ -20,11 +21,12 @@ open_file_read, open_file_write +@dataclass class Harness: + metadata: Metadata + options: Options - def __init__(self): - self.color_mode = 'SHORT' - self.mini_bom_mode = True + def __post_init__(self): self.connectors = {} self.cables = {} self._bom = [] # Internal Cache for generated bom @@ -91,18 +93,19 @@ def create_graph(self) -> Graph: dot = Graph() dot.body.append(f'// Graph generated by {APP_NAME} {__version__}') dot.body.append(f'// {APP_URL}') - font = 'arial' dot.attr('graph', rankdir='LR', ranksep='2', - bgcolor='white', + bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"), nodesep='0.33', - fontname=font) - dot.attr('node', shape='record', + fontname=self.options.fontname) + dot.attr('node', + shape='none', + width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label. style='filled', - fillcolor='white', - fontname=font) + fillcolor=wv_colors.translate_color(self.options.bgcolor_node, "HEX"), + fontname=self.options.fontname) dot.attr('edge', style='bold', - fontname=font) + fontname=self.options.fontname) # prepare ports on connectors depending on which side they will connect for _, cable in self.cables.items(): @@ -126,7 +129,8 @@ def create_graph(self) -> Graph: [html_line_breaks(connector.type), html_line_breaks(connector.subtype), 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)], '' if connector.style != 'simple' else None, [html_image(connector.image)], [html_caption(connector.image)]] @@ -148,7 +152,7 @@ def create_graph(self) -> Graph: pinhtml.append(f' {pinlabel}') if connector.pincolors: if pincolor in wv_colors._color_hex.keys(): - pinhtml.append(f' {pincolor}') + pinhtml.append(f' {translate_color(pincolor, self.options.color_mode)}') pinhtml.append( ' ') pinhtml.append( ' ') pinhtml.append(f' ') @@ -166,7 +170,8 @@ def create_graph(self) -> Graph: html = [row.replace('', '\n'.join(pinhtml)) for row in 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: dot.attr('edge', color='#000000:#ffffff:#000000') @@ -211,7 +216,8 @@ def create_graph(self) -> Graph: f'{cable.gauge} {cable.gauge_unit}{awg_fmt}' if cable.gauge else None, '+ S' if cable.shield 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)], '', [html_image(cable.image)], [html_caption(cable.image)]] @@ -232,7 +238,7 @@ def create_graph(self) -> Graph: wireinfo = [] if cable.show_wirenumbers: 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: wireinfo.append(colorstr) if cable.wirelabels: @@ -332,9 +338,11 @@ def create_graph(self) -> Graph: to_string = '' html = [row.replace(f'', 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) 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 @@ -368,7 +376,7 @@ def output(self, filename: (str, Path), view: bool = False, cleanup: bool = True with open_file_write(f'{filename}.bom.tsv') as file: file.write(tuplelist2tsv(bomlist)) # HTML output - generate_html_output(filename, bomlist) + generate_html_output(filename, bomlist, self.metadata, self.options) def bom(self): if not self._bom: diff --git a/src/wireviz/wireviz.py b/src/wireviz/wireviz.py index 9ecef5a0..6ba77eec 100755 --- a/src/wireviz/wireviz.py +++ b/src/wireviz/wireviz.py @@ -13,6 +13,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from wireviz import __version__ +from wireviz.DataClasses import Metadata, Options from wireviz.Harness import Harness 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) - 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 sections = ['connectors', 'cables', 'connections'] diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index fce42b87..76ba4a1d 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union 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_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), '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})) rows.append(component_table_entry(f'#{id} ({part.type.rstrip()})', **common_args)) else: @@ -69,7 +70,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: + (f', {connector.type}' if connector.type else '') + (f', {connector.subtype}' if connector.subtype 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({ 'description': description, 'designators': connector.name if connector.show_name else None, **optional_fields(connector), @@ -88,7 +89,8 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: + (f', {cable.type}' if cable.type else '') + (f', {cable.wirecount}') + (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({ 'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, **optional_fields(cable), @@ -99,7 +101,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: description = ('Wire' + (f', {cable.type}' if cable.type 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({ '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()}, diff --git a/src/wireviz/wv_html.py b/src/wireviz/wv_html.py index b328ba3d..cc55af85 100644 --- a/src/wireviz/wv_html.py +++ b/src/wireviz/wv_html.py @@ -2,21 +2,28 @@ # -*- coding: utf-8 -*- from pathlib import Path +from typing import List, Union 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 -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: file.write('\n') file.write('\n') file.write(' \n') file.write(f' \n') - file.write(f' {APP_NAME} Diagram and BOM\n') - file.write('\n') + file.write(f' {metadata["title"]}\n') + file.write(f'\n') - file.write('

Diagram

') + file.write(f'

{metadata["title"]}

\n') + description = metadata.get('description') + if description: + file.write(f'

{description}

\n') + file.write('

Diagram

\n') with open_file_read(f'{filename}.svg') as svg: file.write(re.sub( '^<[?]xml [^?>]*[?]>[^<]*]*>', @@ -25,20 +32,24 @@ def generate_html_output(filename: (str, Path), bom_list): for svgdata in svg: file.write(svgdata) - file.write('

Bill of Materials

') + file.write('

Bill of Materials

\n') listy = flatten2d(bom_list) - file.write('
') - file.write('') + file.write('
\n') + file.write(' \n') for item in listy[0]: - file.write(f'') - file.write('') + file.write(f' \n') + file.write(' \n') for row in listy[1:]: - file.write('') + file.write(' \n') for i, item in enumerate(row): item_str = item.replace('\u00b2', '²') - align = 'text-align:right; ' if listy[0][i] == 'Qty' else '' - file.write(f'') - file.write('') - file.write('
{item}
{item}
{item_str}
') + align = '; text-align:right' if listy[0][i] == 'Qty' else '' + file.write(f' {item_str}\n') + file.write(' \n') + file.write('\n') - file.write('') + notes = metadata.get('notes') + if notes: + file.write(f'

Notes

\n

{notes}

\n') + + file.write('\n')