diff --git a/README.md b/README.md
index e973b634..b445ae2e 100644
--- a/README.md
+++ b/README.md
@@ -100,10 +100,11 @@ $ wireviz ~/path/to/file/mywire.yml
This will output the following files
```
-mywire.gv GraphViz output
+mywire.gv Raw GraphViz DOT file output of wiring diagram
mywire.svg Wiring diagram as vector image
mywire.png Wiring diagram as raster image
mywire.bom.tsv BOM (bill of materials) as tab-separated text file
+mywire.bom.csv BOM (bill of materials) as comma-separated, excel-format text file
mywire.html HTML page with wiring diagram and BOM embedded
```
diff --git a/requirements.txt b/requirements.txt
index 93394812..5a6667c4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
.
+click
graphviz
pyyaml
setuptools
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 09621bb7..f642a8be 100644
--- a/setup.py
+++ b/setup.py
@@ -6,6 +6,12 @@
project_name = 'wireviz'
+# utility function to read the requirements.txt file
+with open('requirements.txt', 'r') as f:
+ requirements = f.readlines()
+ requirements = [r.strip() for r in requirements]
+ requirements = [r for r in requirements if r != '.']
+
# Utility function to read the README file.
# Used for the long_description. It's nice, because now 1) we have a top level
# README file and 2) it's easier to type in the README file than to put a raw
@@ -13,6 +19,7 @@
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
+
setup(
name=project_name,
version='0.1',
@@ -21,10 +28,7 @@ def read(fname):
description='Easily document cables and wiring harnesses',
long_description=read(os.path.join(os.path.dirname(__file__), 'README.md')),
long_description_content_type='text/markdown',
- install_requires=[
- 'pyyaml',
- 'graphviz',
- ],
+ install_requires=requirements,
license='GPLv3',
keywords='cable connector hardware harness wiring wiring-diagram wiring-harness',
url='https://github.com/formatc1702/WireViz',
diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py
index 9cc6b96d..9f668316 100644
--- a/src/wireviz/Harness.py
+++ b/src/wireviz/Harness.py
@@ -3,9 +3,9 @@
from wireviz.DataClasses import Connector, Cable
from graphviz import Graph
-from wireviz import wv_colors, wv_helper
+from wireviz import wv_colors, wv_helper, bom_helper
from wireviz.wv_colors import get_color_hex
-from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, \
+from wireviz.wv_helper import awg_equiv, mm2_equiv, \
nested_html_table, flatten2d, index_if_list, html_line_breaks, \
graphviz_line_breaks, remove_line_breaks, open_file_read, open_file_write, \
manufacturer_info_field
@@ -68,7 +68,7 @@ def create_graph(self) -> Graph:
font = 'arial'
dot.attr('graph', rankdir='LR',
ranksep='2',
- bgcolor='white',
+ bgcolor=wv_colors.COLOR_BACKGROUND,
nodesep='0.33',
fontname=font)
dot.attr('node', shape='record',
@@ -276,6 +276,11 @@ def create_graph(self) -> Graph:
return dot
+ @property
+ def gv(self):
+ # self.create_graph().save()
+ raise NotImplementedError
+
@property
def png(self):
from io import BytesIO
@@ -294,50 +299,80 @@ def svg(self):
data.seek(0)
return data.read()
- def output(self, filename: (str, Path), view: bool = False, cleanup: bool = True, fmt: tuple = ('pdf', )) -> None:
- # graphical output
- graph = self.create_graph()
- for f in fmt:
- graph.format = f
- graph.render(filename=filename, view=view, cleanup=cleanup)
- graph.save(filename=f'{filename}.gv')
- # bom output
- bom_list = self.bom_list()
- with open_file_write(f'{filename}.bom.tsv') as file:
- file.write(tuplelist2tsv(bom_list))
- # HTML output
- with open_file_write(f'{filename}.html') as file:
- file.write('\n')
- file.write('
')
-
- file.write('Diagram
')
- with open_file_read(f'{filename}.svg') as svg:
- file.write(re.sub(
- '^<[?]xml [^?>]*[?]>[^<]*]*>',
- '',
- svg.read(1024), 1))
- for svgdata in svg:
- file.write(svgdata)
-
- file.write('Bill of Materials
')
- listy = flatten2d(bom_list)
- file.write('')
- file.write('')
- for item in listy[0]:
- file.write(f'{item} | ')
- file.write('
')
- for row in listy[1:]:
- file.write('')
- for i, item in enumerate(row):
- item_str = item.replace('\u00b2', '²')
- align = 'align="right"' if listy[0][i] == 'Qty' else ''
- file.write(f'{item_str} | ')
- file.write('
')
- file.write('
')
-
- file.write('')
-
- def bom(self):
+ @property
+ def html(self):
+ bom_list = self._bom_list()
+ string = ''
+
+ string += '\n'
+ string += f''
+
+ string += 'Diagram
'
+
+ svg = self.svg.decode('utf-8')
+ svg = re.sub(
+ '^<[?]xml [^?>]*[?]>[^<]*]*>',
+ '',
+ svg)
+ for svgdata in svg:
+ string += svgdata
+
+ string += 'Bill of Materials
'
+ listy = flatten2d(bom_list)
+ string += ''
+ string += ''
+ for item in listy[0]:
+ string += f'{item} | '
+ string += '
'
+ for row in listy[1:]:
+ string += ''
+ for i, item in enumerate(row):
+ item_str = item.replace('\u00b2', '²')
+ align = 'align="right"' if listy[0][i] == 'Qty' else ''
+ string += f'{item_str} | '
+ string += '
'
+ string += '
'
+
+ string += ''
+
+ return bytes(string, encoding='utf-8')
+
+ @property
+ def csv(self):
+ return bom_helper.generate_bom_outputs(
+ self._bom_list(),
+ 'csv'
+ )
+
+ @property
+ def csv_excel(self):
+ return bom_helper.generate_bom_outputs(
+ self._bom_list(),
+ 'csv_excel'
+ )
+
+ @property
+ def csv_unix(self):
+ return bom_helper.generate_bom_outputs(
+ self._bom_list(),
+ 'csv_unix'
+ )
+
+ @property
+ def tsv(self):
+ return bom_helper.generate_bom_outputs(
+ self._bom_list(),
+ 'tsv'
+ )
+
+ @property
+ def tsv_excel(self):
+ return bom_helper.generate_bom_outputs(
+ self._bom_list(),
+ 'tsv_excel'
+ )
+
+ def _bom(self):
bom = []
bom_connectors = []
bom_cables = []
@@ -417,8 +452,8 @@ def bom(self):
bom.extend(bom_extra)
return bom
- def bom_list(self):
- bom = self.bom()
+ def _bom_list(self):
+ bom = self._bom()
keys = ['item', 'qty', 'unit', 'designators'] # these BOM columns will always be included
for fieldname in ['pn', 'manufacturer', 'mpn']: # these optional BOM columns will only be included if at least one BOM item actually uses them
if any(fieldname in x and x.get(fieldname, None) for x in bom):
diff --git a/src/wireviz/bom_helper.py b/src/wireviz/bom_helper.py
new file mode 100644
index 00000000..c6b3cbcd
--- /dev/null
+++ b/src/wireviz/bom_helper.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import csv
+from io import StringIO
+
+from wireviz import wv_helper
+
+WIREVIZ_TSV = type('Wireviz BOM', (csv.Dialect, object), dict(
+ delimiter='\t',
+ doublequote=True,
+ escapechar=None,
+ lineterminator='\n',
+ quoting=0,
+ skipinitialspace=False,
+ strict=False,
+ quotechar='"'
+))
+csv.register_dialect('Wireviz BOM', WIREVIZ_TSV)
+
+
+def generate_bom_outputs(bomdata, dialect='tsv'):
+ dialect = dialect.strip().lower()
+ dialect_lookup = {
+ 'csv': csv.unix_dialect,
+ 'csv_unix': csv.unix_dialect,
+ 'csv_excel': csv.excel,
+ 'tsv': WIREVIZ_TSV,
+ 'tsv_excel': csv.excel_tab
+ }
+ valid_dialects = [k for k in dialect_lookup.keys()]
+ if dialect not in valid_dialects:
+ raise ValueError(f'dialect "{dialect}" not supported')
+
+ output = StringIO()
+ writer = csv.writer(output, dialect=dialect_lookup[dialect])
+ writer.writerows(wv_helper.flatten2d(bomdata))
+
+ output.seek(0)
+
+ return bytes(output.read(), encoding='utf-8')
+
+# TODO: Possibly refactor other BOM output operations, such as HTML, into here?
\ No newline at end of file
diff --git a/src/wireviz/build_examples.py b/src/wireviz/build_examples.py
index 10a4b1bf..6a4751e8 100755
--- a/src/wireviz/build_examples.py
+++ b/src/wireviz/build_examples.py
@@ -10,7 +10,7 @@
sys.path.insert(0, str(script_path.parent.parent)) # to find wireviz module
from wireviz import wireviz
-from wv_helper import open_file_write, open_file_read, open_file_append
+from wireviz.wv_helper import open_file_write, open_file_read, open_file_append
readme = 'readme.md'
@@ -61,7 +61,7 @@ def build_generated(groupkeys):
# collect and iterate input YAML files
for yaml_file in collect_filenames('Building', key, input_extensions):
print(f' "{yaml_file}"')
- wireviz.parse_file(yaml_file)
+ wireviz.main(yaml_file, prepend=None, out=['png', 'svg', 'html', 'csv'])
if build_readme:
i = ''.join(filter(str.isdigit, yaml_file.stem))
diff --git a/src/wireviz/wireviz.py b/src/wireviz/wireviz.py
index 3adc0782..d589a8ba 100755
--- a/src/wireviz/wireviz.py
+++ b/src/wireviz/wireviz.py
@@ -1,12 +1,12 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-import argparse
import os
from pathlib import Path
import sys
from typing import Any, Tuple
+import click
import yaml
if __name__ == '__main__':
@@ -17,12 +17,11 @@
from wireviz.wv_helper import expand, open_file_read
-def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, str, Tuple[str]) = None) -> Any:
+def parse(yaml_input: str, return_types: (None, str, Tuple[str]) = None) -> Any:
"""
Parses yaml input string and does the high-level harness conversion
:param yaml_input: a string containing the yaml input data
- :param file_out:
:param return_types: if None, then returns None; if the value is a string, then a
corresponding data format will be returned; if the value is a tuple of strings,
then for every valid format in the `return_types` tuple, another return type
@@ -57,14 +56,6 @@ def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, st
elif ty == list:
yaml_data[sec] = []
- # add connections
-
- def check_designators(what, where): # helper function
- for i, x in enumerate(what):
- if x not in yaml_data[where[i]]:
- return False
- return True
-
autogenerated_ids = {}
for connection in yaml_data['connections']:
# find first component (potentially nested inside list or dict)
@@ -174,9 +165,6 @@ def check_designators(what, where): # helper function
for line in yaml_data["additional_bom_items"]:
harness.add_bom_item(line)
- if file_out is not None:
- harness.output(filename=file_out, fmt=('png', 'svg'), view=False)
-
if return_types is not None:
returns = []
if isinstance(return_types, str): # only one return type speficied
@@ -187,66 +175,83 @@ def check_designators(what, where): # helper function
for rt in return_types:
if rt == 'png':
returns.append(harness.png)
- if rt == 'svg':
+ elif rt == 'svg':
returns.append(harness.svg)
- if rt == 'harness':
+ elif rt == 'html':
+ returns.append(harness.html)
+ elif rt == 'csv':
+ returns.append(harness.csv)
+ elif rt == 'csv_excel':
+ returns.append(harness.csv_excel)
+ elif rt == 'csv_unix':
+ returns.append(harness.csv_unix)
+ elif rt == 'tsv':
+ returns.append(harness.tsv)
+ elif rt == 'tsv_excel':
+ returns.append(harness.tsv_excel)
+ elif rt == 'gv':
+ returns.append(harness.gv)
+ elif rt == 'harness':
returns.append(harness)
+ else:
+ raise ValueError(f'return type "{rt}" not supported')
return tuple(returns) if len(returns) != 1 else returns[0]
-def parse_file(yaml_file: str, file_out: (str, Path) = None) -> None:
- with open_file_read(yaml_file) as file:
- yaml_input = file.read()
-
- if not file_out:
- fn, fext = os.path.splitext(yaml_file)
- file_out = fn
- file_out = os.path.abspath(file_out)
-
- parse(yaml_input, file_out=file_out)
-
-
-def parse_cmdline():
- parser = argparse.ArgumentParser(
- description='Generate cable and wiring harness documentation from YAML descriptions',
- )
- parser.add_argument('input_file', action='store', type=str, metavar='YAML_FILE')
- parser.add_argument('-o', '--output_file', action='store', type=str, metavar='OUTPUT')
- parser.add_argument('--generate-bom', action='store_true', default=True)
- parser.add_argument('--prepend-file', action='store', type=str, metavar='YAML_FILE')
- return parser.parse_args()
+@click.command()
+@click.argument('input_file', required=True)
+@click.option('--prepend', '-p', default=None, help='a YAML file containing a library of templates and parts that may be referenced in the `input_file`')
+@click.option('--out', '-o', multiple=True, default=['png'], help='specify one or more output types to be generated; currently supports "png" "svg", "html", "csv", "csv_excel", "csv_unix", "tsv", "tsv_excel"')
+def cli(input_file, prepend, out):
+ """
+ Take a YAML file containing a harness specification and, utilizing GraphViz,
+ translate that specification into various output formats.
+ Example:
-def main():
+ $>wireviz my_input_file.yml -o png -o svg
+ """
+ main(input_file, prepend, out)
- args = parse_cmdline()
- if not os.path.exists(args.input_file):
- print(f'Error: input file {args.input_file} inaccessible or does not exist, check path')
+def main(input_file, prepend, out):
+ if not os.path.exists(input_file):
+ print(f'Error: input file "{input_file}" inaccessible or does not exist, check path')
sys.exit(1)
- with open_file_read(args.input_file) as fh:
+ with open_file_read(input_file) as fh:
yaml_input = fh.read()
- if args.prepend_file:
- if not os.path.exists(args.prepend_file):
- print(f'Error: prepend input file {args.prepend_file} inaccessible or does not exist, check path')
+ if prepend:
+ if not os.path.exists(prepend):
+ print(f'Error: prepend input file {prepend} inaccessible or does not exist, check path')
sys.exit(1)
- with open_file_read(args.prepend_file) as fh:
+ with open_file_read(prepend) as fh:
prepend = fh.read()
yaml_input = prepend + yaml_input
- if not args.output_file:
- file_out = args.input_file
- pre, _ = os.path.splitext(file_out)
- file_out = pre # extension will be added by graphviz output function
- else:
- file_out = args.output_file
- file_out = os.path.abspath(file_out)
+ input_file = Path(input_file)
+ base_file_name = input_file.name.replace(input_file.suffix, '')
+
+ # the parse function may return a single instance or a tuple, thus, the
+ # if/then determines if there is only one thing that must be saved or multiple
+ filedatas = parse(yaml_input, return_types=out)
+ if not isinstance(filedatas, tuple):
+ filedatas = (filedatas, )
+
+ for ext, data in zip(out, filedatas):
+ if 'csv' in ext:
+ extension = 'bom.csv'
+ elif 'tsv' in ext:
+ extension = 'bom.tsv'
+ else:
+ extension = ext
- parse(yaml_input, file_out=file_out)
+ fname = f'{base_file_name}.{extension}'
+ with open(fname, 'wb') as f:
+ f.write(data)
if __name__ == '__main__':
- main()
+ cli()
diff --git a/src/wireviz/wv_colors.py b/src/wireviz/wv_colors.py
index 41575437..c34e7fe0 100644
--- a/src/wireviz/wv_colors.py
+++ b/src/wireviz/wv_colors.py
@@ -98,12 +98,13 @@
}
-color_default = '#ffffff'
+COLOR_DEFAULT = '#ffffff'
+COLOR_BACKGROUND = '#ffffff'
def get_color_hex(input, pad=False):
if input is None or input == '':
- return [color_default]
+ return [COLOR_DEFAULT]
if len(input) == 4: # give wires with EXACTLY 2 colors that striped/banded look
input = input + input[:2]
# hacky style fix: give single color wires a triple-up so that wires are the same size
@@ -113,7 +114,7 @@ def get_color_hex(input, pad=False):
output = [_color_hex[input[i:i + 2]] for i in range(0, len(input), 2)]
except KeyError:
print("Unknown color specified")
- output = [color_default]
+ output = [COLOR_DEFAULT]
return output
diff --git a/tutorial/tutorial08.md b/tutorial/tutorial08.md
index 1fc884e7..8369e90f 100644
--- a/tutorial/tutorial08.md
+++ b/tutorial/tutorial08.md
@@ -3,4 +3,3 @@
* Part number information can be added to parts
* Only provided fields will be added to the diagram and bom
* Bundles can have part information specified by wire
-* Additional parts can be added to the bom
diff --git a/tutorial/tutorial08.yml b/tutorial/tutorial08.yml
index 2568f29b..56a38005 100644
--- a/tutorial/tutorial08.yml
+++ b/tutorial/tutorial08.yml
@@ -3,11 +3,11 @@ connectors:
type: Molex KK 254
pincount: 4
subtype: female
- manufacturer: Molex # set manufacter name
- mpn: 22013047 # set manufacturer part number
+ manufacturer: Molex
+ manufacturer_part_number: 22013047
X2:
<<: *template1 # reuse template
- pn: CON4 # set an internal part number
+ internal_part_number: CON4
X3:
<<: *template1 # reuse template
@@ -18,16 +18,16 @@ cables:
gauge: 0.25 mm2
color_code: IEC
manufacturer: CablesCo
- mpn: ABC123
- pn: CAB1
+ manufacturer_part_number: ABC123
+ internal_part_number: CAB1
W2:
category: bundle
length: 1
gauge: 0.25 mm2
colors: [YE, BK, BK, RD]
- manufacturer: [WiresCo,WiresCo,WiresCo,WiresCo] # set a manufacter per wire
- mpn: [W1-YE,W1-BK,W1-BK,W1-RD]
- pn: [WIRE1,WIRE2,WIRE2,WIRE3]
+ manufacturer: [WiresCo,WiresCo,WiresCo,WiresCo]
+ manufacturer_part_number: [W1-YE,W1-BK,W1-BK,W1-RD]
+ internal_part_number: [WIRE1,WIRE2,WIRE2,WIRE3]
connections:
@@ -39,14 +39,3 @@ connections:
- X1: [1-4]
- W2: [1-4]
- X3: [1-4]
-
-additional_bom_items:
- - # define an additional item to add to the bill of materials
- description: Label, pinout information
- qty: 2
- designators:
- - X2
- - X3
- manufacturer: generic company
- mpn: Label1
- pn: Label-ID-1