Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic options and metadata #214

Merged
merged 12 commits into from
Aug 25, 2021
55 changes: 55 additions & 0 deletions docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
## Main sections

```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
<str> : # unique connector designator/name
... # connector attributes (see below)
Expand Down Expand Up @@ -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

```yaml
Expand Down
30 changes: 29 additions & 1 deletion src/wireviz/DataClasses.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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:
Expand Down
46 changes: 27 additions & 19 deletions src/wireviz/Harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
from graphviz import Graph
from collections import Counter
from typing import List, Union
kvid marked this conversation as resolved.
Show resolved Hide resolved
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
kvid marked this conversation as resolved.
Show resolved Hide resolved
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, \
Expand All @@ -20,11 +21,12 @@
open_file_read, open_file_write


@dataclass
class Harness:
metadata: Metadata
kvid marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down Expand Up @@ -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():
Expand All @@ -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)],
'<!-- connector table -->' if connector.style != 'simple' else None,
[html_image(connector.image)],
[html_caption(connector.image)]]
Expand All @@ -148,7 +152,7 @@ def create_graph(self) -> Graph:
pinhtml.append(f' <td>{pinlabel}</td>')
if connector.pincolors:
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( ' <table border="0" cellborder="1"><tr>')
pinhtml.append(f' <td bgcolor="{wv_colors.translate_color(pincolor, "HEX")}" width="8" height="8" fixedsize="true"></td>')
Expand All @@ -166,7 +170,8 @@ def create_graph(self) -> Graph:
html = [row.replace('<!-- connector table -->', '\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')
Expand Down Expand Up @@ -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)],
'<!-- wire table -->',
[html_image(cable.image)],
[html_caption(cable.image)]]
Expand All @@ -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:
Expand Down Expand Up @@ -332,9 +338,11 @@ def create_graph(self) -> Graph:
to_string = ''
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)
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

Expand Down Expand Up @@ -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:
Expand Down
8 changes: 7 additions & 1 deletion src/wireviz/wireviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
kvid marked this conversation as resolved.
Show resolved Hide resolved
from wireviz.Harness import Harness
from wireviz.wv_helper import expand, open_file_read

Expand All @@ -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']
Expand Down
10 changes: 6 additions & 4 deletions src/wireviz/wv_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -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()},
Expand Down
43 changes: 27 additions & 16 deletions src/wireviz/wv_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
kvid marked this conversation as resolved.
Show resolved Hide resolved
with open_file_write(f'{filename}.html') as file:
file.write('<!DOCTYPE html>\n')
file.write('<html lang="en"><head>\n')
file.write(' <meta charset="UTF-8">\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('</head><body style="font-family:Arial">\n')
file.write(f' <title>{metadata["title"]}</title>\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:
file.write(re.sub(
'^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>',
Expand All @@ -25,20 +32,24 @@ def generate_html_output(filename: (str, Path), bom_list):
for svgdata in svg:
file.write(svgdata)

file.write('<h1>Bill of Materials</h1>')
file.write('<h2>Bill of Materials</h2>\n')
listy = flatten2d(bom_list)
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">')
file.write('<tr>')
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">\n')
file.write(' <tr>\n')
for item in listy[0]:
file.write(f'<th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>')
file.write('</tr>')
file.write(f' <th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>\n')
file.write(' </tr>\n')
for row in listy[1:]:
file.write('<tr>')
file.write(' <tr>\n')
for i, item in enumerate(row):
item_str = item.replace('\u00b2', '&sup2;')
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('</tr>')
file.write('</table>')
align = '; text-align:right' if listy[0][i] == 'Qty' else ''
file.write(f' <td style="border:1px solid #000000; padding: 4px{align}">{item_str}</td>\n')
file.write(' </tr>\n')
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')