Apply black
This commit is contained in:
parent
344615483a
commit
f92985a61c
45
setup.py
45
setup.py
@ -7,40 +7,39 @@ from setuptools import find_packages, setup
|
||||
|
||||
from src.wireviz import APP_URL, CMD_NAME, __version__
|
||||
|
||||
README_PATH = Path(__file__).parent / 'docs' / 'README.md'
|
||||
README_PATH = Path(__file__).parent / "docs" / "README.md"
|
||||
|
||||
setup(
|
||||
name=CMD_NAME,
|
||||
version=__version__,
|
||||
author='Daniel Rojas',
|
||||
#author_email='',
|
||||
description='Easily document cables and wiring harnesses',
|
||||
author="Daniel Rojas",
|
||||
# author_email='',
|
||||
description="Easily document cables and wiring harnesses",
|
||||
long_description=open(README_PATH).read(),
|
||||
long_description_content_type='text/markdown',
|
||||
long_description_content_type="text/markdown",
|
||||
install_requires=[
|
||||
'click',
|
||||
'pyyaml',
|
||||
'pillow',
|
||||
'graphviz',
|
||||
"click",
|
||||
"pyyaml",
|
||||
"pillow",
|
||||
"graphviz",
|
||||
],
|
||||
license='GPLv3',
|
||||
keywords='cable connector hardware harness wiring wiring-diagram wiring-harness',
|
||||
license="GPLv3",
|
||||
keywords="cable connector hardware harness wiring wiring-diagram wiring-harness",
|
||||
url=APP_URL,
|
||||
package_dir={'': 'src'},
|
||||
packages=find_packages('src'),
|
||||
package_dir={"": "src"},
|
||||
packages=find_packages("src"),
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'wireviz=wireviz.wv_cli:wireviz',
|
||||
"console_scripts": [
|
||||
"wireviz=wireviz.wv_cli:wireviz",
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Console',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Topic :: Utilities',
|
||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Topic :: Utilities",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
],
|
||||
|
||||
)
|
||||
|
||||
@ -11,42 +11,53 @@ from wireviz.wv_helper import aspect_ratio, int2tuple
|
||||
# Each type alias have their legal values described in comments - validation might be implemented in the future
|
||||
PlainText = str # Text not containing HTML tags nor newlines
|
||||
Hypertext = str # Text possibly including HTML hyperlinks that are removed in all outputs except HTML output
|
||||
MultilineHypertext = str # Hypertext possibly also including newlines to break lines in diagram output
|
||||
MultilineHypertext = (
|
||||
str # Hypertext possibly also including newlines to break lines in diagram output
|
||||
)
|
||||
|
||||
Designator = PlainText # Case insensitive unique name of connector or cable
|
||||
|
||||
# Literal type aliases below are commented to avoid requiring python 3.8
|
||||
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']
|
||||
|
||||
# Type combinations
|
||||
Pin = Union[int, PlainText] # Pin identifier
|
||||
PinIndex = int # Zero-based pin index
|
||||
Wire = Union[int, PlainText] # Wire number or Literal['s'] for shield
|
||||
NoneOrMorePins = Union[Pin, Tuple[Pin, ...], None] # None, one, or a tuple of pin identifiers
|
||||
NoneOrMorePinIndices = Union[PinIndex, Tuple[PinIndex, ...], None] # None, one, or a tuple of zero-based pin indices
|
||||
NoneOrMorePins = Union[
|
||||
Pin, Tuple[Pin, ...], None
|
||||
] # None, one, or a tuple of pin identifiers
|
||||
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 Side(Enum):
|
||||
LEFT = auto()
|
||||
RIGHT = auto()
|
||||
|
||||
|
||||
class Metadata(dict):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Options:
|
||||
fontname: PlainText = 'arial'
|
||||
bgcolor: Color = 'WH'
|
||||
bgcolor_node: Optional[Color] = 'WH'
|
||||
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'
|
||||
color_mode: ColorMode = "SHORT"
|
||||
mini_bom_mode: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
@ -87,9 +98,13 @@ class Image:
|
||||
self.fixedsize = (self.width or self.height) and self.scale is None
|
||||
|
||||
if self.scale is None:
|
||||
self.scale = "false" if not self.width and not self.height \
|
||||
else "both" if self.width and self.height \
|
||||
else "true" # When only one dimension is specified.
|
||||
self.scale = (
|
||||
"false"
|
||||
if not self.width and not self.height
|
||||
else "both"
|
||||
if self.width and self.height
|
||||
else "true"
|
||||
) # When only one dimension is specified.
|
||||
|
||||
if self.fixedsize:
|
||||
# If only one dimension is specified, compute the other
|
||||
@ -118,7 +133,9 @@ class AdditionalComponent:
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return self.type.rstrip() + (f', {self.subtype.rstrip()}' if self.subtype else '')
|
||||
return self.type.rstrip() + (
|
||||
f", {self.subtype.rstrip()}" if self.subtype else ""
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -158,36 +175,44 @@ class Connector:
|
||||
self.ports_right = False
|
||||
self.visible_pins = {}
|
||||
|
||||
if self.style == 'simple':
|
||||
if self.style == "simple":
|
||||
if self.pincount and self.pincount > 1:
|
||||
raise Exception('Connectors with style set to simple may only have one pin')
|
||||
raise Exception(
|
||||
"Connectors with style set to simple may only have one pin"
|
||||
)
|
||||
self.pincount = 1
|
||||
|
||||
if not self.pincount:
|
||||
self.pincount = max(len(self.pins), len(self.pinlabels), len(self.pincolors))
|
||||
self.pincount = max(
|
||||
len(self.pins), len(self.pinlabels), len(self.pincolors)
|
||||
)
|
||||
if not self.pincount:
|
||||
raise Exception('You need to specify at least one, pincount, pins, pinlabels, or pincolors')
|
||||
raise Exception(
|
||||
"You need to specify at least one, pincount, pins, pinlabels, or pincolors"
|
||||
)
|
||||
|
||||
# create default list for pins (sequential) if not specified
|
||||
if not self.pins:
|
||||
self.pins = list(range(1, self.pincount + 1))
|
||||
|
||||
if len(self.pins) != len(set(self.pins)):
|
||||
raise Exception('Pins are not unique')
|
||||
raise Exception("Pins are not unique")
|
||||
|
||||
if self.show_name is None:
|
||||
# hide designators for simple and for auto-generated connectors by default
|
||||
self.show_name = (self.style != 'simple' and self.name[0:2] != '__')
|
||||
self.show_name = self.style != "simple" and self.name[0:2] != "__"
|
||||
|
||||
if self.show_pincount is None:
|
||||
self.show_pincount = self.style != 'simple' # hide pincount for simple (1 pin) connectors by default
|
||||
self.show_pincount = (
|
||||
self.style != "simple"
|
||||
) # hide pincount for simple (1 pin) connectors by default
|
||||
|
||||
for loop in self.loops:
|
||||
# TODO: check that pins to connect actually exist
|
||||
# TODO: allow using pin labels in addition to pin numbers, just like when defining regular connections
|
||||
# TODO: include properties of wire used to create the loop
|
||||
if len(loop) != 2:
|
||||
raise Exception('Loops must be between exactly two pins!')
|
||||
raise Exception("Loops must be between exactly two pins!")
|
||||
|
||||
for i, item in enumerate(self.additional_components):
|
||||
if isinstance(item, dict):
|
||||
@ -203,12 +228,14 @@ class Connector:
|
||||
def get_qty_multiplier(self, qty_multiplier: Optional[ConnectorMultiplier]) -> int:
|
||||
if not qty_multiplier:
|
||||
return 1
|
||||
elif qty_multiplier == 'pincount':
|
||||
elif qty_multiplier == "pincount":
|
||||
return self.pincount
|
||||
elif qty_multiplier == 'populated':
|
||||
elif qty_multiplier == "populated":
|
||||
return sum(self.visible_pins.values())
|
||||
else:
|
||||
raise ValueError(f'invalid qty multiplier parameter for connector {qty_multiplier}')
|
||||
raise ValueError(
|
||||
f"invalid qty multiplier parameter for connector {qty_multiplier}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -249,65 +276,79 @@ class Cable:
|
||||
|
||||
if isinstance(self.gauge, str): # gauge and unit specified
|
||||
try:
|
||||
g, u = self.gauge.split(' ')
|
||||
g, u = self.gauge.split(" ")
|
||||
except Exception:
|
||||
raise Exception(f'Cable {self.name} gauge={self.gauge} - Gauge must be a number, or number and unit separated by a space')
|
||||
raise Exception(
|
||||
f"Cable {self.name} gauge={self.gauge} - Gauge must be a number, or number and unit separated by a space"
|
||||
)
|
||||
self.gauge = g
|
||||
|
||||
if self.gauge_unit is not None:
|
||||
print(f'Warning: Cable {self.name} gauge_unit={self.gauge_unit} is ignored because its gauge contains {u}')
|
||||
if u.upper() == 'AWG':
|
||||
print(
|
||||
f"Warning: Cable {self.name} gauge_unit={self.gauge_unit} is ignored because its gauge contains {u}"
|
||||
)
|
||||
if u.upper() == "AWG":
|
||||
self.gauge_unit = u.upper()
|
||||
else:
|
||||
self.gauge_unit = u.replace('mm2', 'mm\u00B2')
|
||||
self.gauge_unit = u.replace("mm2", "mm\u00B2")
|
||||
|
||||
elif self.gauge is not None: # gauge specified, assume mm2
|
||||
if self.gauge_unit is None:
|
||||
self.gauge_unit = 'mm\u00B2'
|
||||
self.gauge_unit = "mm\u00B2"
|
||||
else:
|
||||
pass # gauge not specified
|
||||
|
||||
if isinstance(self.length, str): # length and unit specified
|
||||
try:
|
||||
L, u = self.length.split(' ')
|
||||
L, u = self.length.split(" ")
|
||||
L = float(L)
|
||||
except Exception:
|
||||
raise Exception(f'Cable {self.name} length={self.length} - Length must be a number, or number and unit separated by a space')
|
||||
raise Exception(
|
||||
f"Cable {self.name} length={self.length} - Length must be a number, or number and unit separated by a space"
|
||||
)
|
||||
self.length = L
|
||||
if self.length_unit is not None:
|
||||
print(f'Warning: Cable {self.name} length_unit={self.length_unit} is ignored because its length contains {u}')
|
||||
print(
|
||||
f"Warning: Cable {self.name} length_unit={self.length_unit} is ignored because its length contains {u}"
|
||||
)
|
||||
self.length_unit = u
|
||||
elif not any(isinstance(self.length, t) for t in [int, float]):
|
||||
raise Exception(f'Cable {self.name} length has a non-numeric value')
|
||||
raise Exception(f"Cable {self.name} length has a non-numeric value")
|
||||
elif self.length_unit is None:
|
||||
self.length_unit = 'm'
|
||||
self.length_unit = "m"
|
||||
|
||||
self.connections = []
|
||||
|
||||
if self.wirecount: # number of wires explicitly defined
|
||||
if self.colors: # use custom color palette (partly or looped if needed)
|
||||
pass
|
||||
elif self.color_code: # use standard color palette (partly or looped if needed)
|
||||
elif (
|
||||
self.color_code
|
||||
): # use standard color palette (partly or looped if needed)
|
||||
if self.color_code not in COLOR_CODES:
|
||||
raise Exception('Unknown color code')
|
||||
raise Exception("Unknown color code")
|
||||
self.colors = COLOR_CODES[self.color_code]
|
||||
else: # no colors defined, add dummy colors
|
||||
self.colors = [''] * self.wirecount
|
||||
self.colors = [""] * self.wirecount
|
||||
|
||||
# make color code loop around if more wires than colors
|
||||
if self.wirecount > len(self.colors):
|
||||
m = self.wirecount // len(self.colors) + 1
|
||||
self.colors = self.colors * int(m)
|
||||
# cut off excess after looping
|
||||
self.colors = self.colors[:self.wirecount]
|
||||
self.colors = self.colors[: self.wirecount]
|
||||
else: # wirecount implicit in length of color list
|
||||
if not self.colors:
|
||||
raise Exception('Unknown number of wires. Must specify wirecount or colors (implicit length)')
|
||||
raise Exception(
|
||||
"Unknown number of wires. Must specify wirecount or colors (implicit length)"
|
||||
)
|
||||
self.wirecount = len(self.colors)
|
||||
|
||||
if self.wirelabels:
|
||||
if self.shield and 's' in self.wirelabels:
|
||||
raise Exception('"s" may not be used as a wire label for a shielded cable.')
|
||||
if self.shield and "s" in self.wirelabels:
|
||||
raise Exception(
|
||||
'"s" may not be used as a wire label for a shielded cable.'
|
||||
)
|
||||
|
||||
# if lists of part numbers are provided check this is a bundle and that it matches the wirecount.
|
||||
for idfield in [self.manufacturer, self.mpn, self.supplier, self.spn, self.pn]:
|
||||
@ -315,44 +356,58 @@ class Cable:
|
||||
if self.category == "bundle":
|
||||
# check the length
|
||||
if len(idfield) != self.wirecount:
|
||||
raise Exception('lists of part data must match wirecount')
|
||||
raise Exception("lists of part data must match wirecount")
|
||||
else:
|
||||
raise Exception('lists of part data are only supported for bundles')
|
||||
raise Exception("lists of part data are only supported for bundles")
|
||||
|
||||
if self.show_name is None:
|
||||
self.show_name = self.name[0:2] != '__' # hide designators for auto-generated cables by default
|
||||
self.show_name = (
|
||||
self.name[0:2] != "__"
|
||||
) # hide designators for auto-generated cables by default
|
||||
|
||||
if not self.show_wirenumbers:
|
||||
self.show_wirenumbers = self.category != 'bundle' # by default, show wire numbers for cables, hide for bundles
|
||||
self.show_wirenumbers = (
|
||||
self.category != "bundle"
|
||||
) # by default, show wire numbers for cables, hide for bundles
|
||||
|
||||
for i, item in enumerate(self.additional_components):
|
||||
if isinstance(item, dict):
|
||||
self.additional_components[i] = AdditionalComponent(**item)
|
||||
|
||||
# The *_pin arguments accept a tuple, but it seems not in use with the current code.
|
||||
def connect(self, from_name: Optional[Designator], from_pin: NoneOrMorePinIndices, via_wire: OneOrMoreWires,
|
||||
to_name: Optional[Designator], to_pin: NoneOrMorePinIndices) -> None:
|
||||
def connect(
|
||||
self,
|
||||
from_name: Optional[Designator],
|
||||
from_pin: NoneOrMorePinIndices,
|
||||
via_wire: OneOrMoreWires,
|
||||
to_name: Optional[Designator],
|
||||
to_pin: NoneOrMorePinIndices,
|
||||
) -> None:
|
||||
from_pin = int2tuple(from_pin)
|
||||
via_wire = int2tuple(via_wire)
|
||||
to_pin = int2tuple(to_pin)
|
||||
if len(from_pin) != len(to_pin):
|
||||
raise Exception('from_pin must have the same number of elements as to_pin')
|
||||
raise Exception("from_pin must have the same number of elements as to_pin")
|
||||
for i, _ in enumerate(from_pin):
|
||||
self.connections.append(Connection(from_name, from_pin[i], via_wire[i], to_name, to_pin[i]))
|
||||
self.connections.append(
|
||||
Connection(from_name, from_pin[i], via_wire[i], to_name, to_pin[i])
|
||||
)
|
||||
|
||||
def get_qty_multiplier(self, qty_multiplier: Optional[CableMultiplier]) -> float:
|
||||
if not qty_multiplier:
|
||||
return 1
|
||||
elif qty_multiplier == 'wirecount':
|
||||
elif qty_multiplier == "wirecount":
|
||||
return self.wirecount
|
||||
elif qty_multiplier == 'terminations':
|
||||
elif qty_multiplier == "terminations":
|
||||
return len(self.connections)
|
||||
elif qty_multiplier == 'length':
|
||||
elif qty_multiplier == "length":
|
||||
return self.length
|
||||
elif qty_multiplier == 'total_length':
|
||||
elif qty_multiplier == "total_length":
|
||||
return self.length * self.wirecount
|
||||
else:
|
||||
raise ValueError(f'invalid qty multiplier parameter for cable {qty_multiplier}')
|
||||
raise ValueError(
|
||||
f"invalid qty multiplier parameter for cable {qty_multiplier}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -363,6 +418,7 @@ class Connection:
|
||||
to_name: Optional[Designator]
|
||||
to_pin: Optional[Pin]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MatePin:
|
||||
from_name: Designator
|
||||
@ -371,6 +427,7 @@ class MatePin:
|
||||
to_pin: Pin
|
||||
shape: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class MateComponent:
|
||||
from_name: Designator
|
||||
|
||||
@ -83,7 +83,15 @@ class Harness:
|
||||
def add_bom_item(self, item: dict) -> None:
|
||||
self.additional_bom_items.append(item)
|
||||
|
||||
def connect(self, from_name: str, from_pin: (int, str), via_name: str, via_wire: (int, str), to_name: str, to_pin: (int, str)) -> None:
|
||||
def connect(
|
||||
self,
|
||||
from_name: str,
|
||||
from_pin: (int, str),
|
||||
via_name: str,
|
||||
via_wire: (int, str),
|
||||
to_name: str,
|
||||
to_pin: (int, str),
|
||||
) -> None:
|
||||
# check from and to connectors
|
||||
for (name, pin) in zip([from_name, to_name], [from_pin, to_pin]):
|
||||
if name is not None and name in self.connectors:
|
||||
@ -91,11 +99,13 @@ class Harness:
|
||||
# check if provided name is ambiguous
|
||||
if pin in connector.pins and pin in connector.pinlabels:
|
||||
if connector.pins.index(pin) != connector.pinlabels.index(pin):
|
||||
raise Exception(f'{name}:{pin} is defined both in pinlabels and pins, for different pins.')
|
||||
raise Exception(
|
||||
f"{name}:{pin} is defined both in pinlabels and pins, for different pins."
|
||||
)
|
||||
# TODO: Maybe issue a warning if present in both lists but referencing the same pin?
|
||||
if pin in connector.pinlabels:
|
||||
if connector.pinlabels.count(pin) > 1:
|
||||
raise Exception(f'{name}:{pin} is defined more than once.')
|
||||
raise Exception(f"{name}:{pin} is defined more than once.")
|
||||
index = connector.pinlabels.index(pin)
|
||||
pin = connector.pins[index] # map pin name to pin number
|
||||
if name == from_name:
|
||||
@ -103,7 +113,7 @@ class Harness:
|
||||
if name == to_name:
|
||||
to_pin = pin
|
||||
if not pin in connector.pins:
|
||||
raise Exception(f'{name}:{pin} not found.')
|
||||
raise Exception(f"{name}:{pin} not found.")
|
||||
|
||||
# check via cable
|
||||
if via_name in self.cables:
|
||||
@ -111,16 +121,26 @@ class Harness:
|
||||
# check if provided name is ambiguous
|
||||
if via_wire in cable.colors and via_wire in cable.wirelabels:
|
||||
if cable.colors.index(via_wire) != cable.wirelabels.index(via_wire):
|
||||
raise Exception(f'{via_name}:{via_wire} is defined both in colors and wirelabels, for different wires.')
|
||||
raise Exception(
|
||||
f"{via_name}:{via_wire} is defined both in colors and wirelabels, for different wires."
|
||||
)
|
||||
# TODO: Maybe issue a warning if present in both lists but referencing the same wire?
|
||||
if via_wire in cable.colors:
|
||||
if cable.colors.count(via_wire) > 1:
|
||||
raise Exception(f'{via_name}:{via_wire} is used for more than one wire.')
|
||||
via_wire = cable.colors.index(via_wire) + 1 # list index starts at 0, wire IDs start at 1
|
||||
raise Exception(
|
||||
f"{via_name}:{via_wire} is used for more than one wire."
|
||||
)
|
||||
via_wire = (
|
||||
cable.colors.index(via_wire) + 1
|
||||
) # list index starts at 0, wire IDs start at 1
|
||||
elif via_wire in cable.wirelabels:
|
||||
if cable.wirelabels.count(via_wire) > 1:
|
||||
raise Exception(f'{via_name}:{via_wire} is used for more than one wire.')
|
||||
via_wire = cable.wirelabels.index(via_wire) + 1 # list index starts at 0, wire IDs start at 1
|
||||
raise Exception(
|
||||
f"{via_name}:{via_wire} is used for more than one wire."
|
||||
)
|
||||
via_wire = (
|
||||
cable.wirelabels.index(via_wire) + 1
|
||||
) # list index starts at 0, wire IDs start at 1
|
||||
|
||||
# perform the actual connection
|
||||
self.cables[via_name].connect(from_name, from_pin, via_wire, to_name, to_pin)
|
||||
@ -129,24 +149,29 @@ class Harness:
|
||||
if to_name in self.connectors:
|
||||
self.connectors[to_name].activate_pin(to_pin, Side.LEFT)
|
||||
|
||||
|
||||
def create_graph(self) -> Graph:
|
||||
dot = Graph()
|
||||
dot.body.append(f'// Graph generated by {APP_NAME} {__version__}')
|
||||
dot.body.append(f'// {APP_URL}')
|
||||
dot.attr('graph', rankdir='LR',
|
||||
ranksep='2',
|
||||
dot.body.append(f"// Graph generated by {APP_NAME} {__version__}")
|
||||
dot.body.append(f"// {APP_URL}")
|
||||
dot.attr(
|
||||
"graph",
|
||||
rankdir="LR",
|
||||
ranksep="2",
|
||||
bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"),
|
||||
nodesep='0.33',
|
||||
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',
|
||||
nodesep="0.33",
|
||||
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=wv_colors.translate_color(self.options.bgcolor_node, "HEX"),
|
||||
fontname=self.options.fontname)
|
||||
dot.attr('edge', style='bold',
|
||||
fontname=self.options.fontname)
|
||||
fontname=self.options.fontname,
|
||||
)
|
||||
dot.attr("edge", style="bold", fontname=self.options.fontname)
|
||||
|
||||
for connector in self.connectors.values():
|
||||
|
||||
@ -156,325 +181,496 @@ class Harness:
|
||||
|
||||
html = []
|
||||
|
||||
rows = [[f'{html_bgcolor(connector.bgcolor_title)}{remove_links(connector.name)}'
|
||||
if connector.show_name else None],
|
||||
[pn_info_string(HEADER_PN, None, remove_links(connector.pn)),
|
||||
html_line_breaks(pn_info_string(HEADER_MPN, connector.manufacturer, connector.mpn)),
|
||||
html_line_breaks(pn_info_string(HEADER_SPN, connector.supplier, connector.spn))],
|
||||
[html_line_breaks(connector.type),
|
||||
rows = [
|
||||
[
|
||||
f"{html_bgcolor(connector.bgcolor_title)}{remove_links(connector.name)}"
|
||||
if connector.show_name
|
||||
else None
|
||||
],
|
||||
[
|
||||
pn_info_string(HEADER_PN, None, remove_links(connector.pn)),
|
||||
html_line_breaks(
|
||||
pn_info_string(
|
||||
HEADER_MPN, connector.manufacturer, connector.mpn
|
||||
)
|
||||
),
|
||||
html_line_breaks(
|
||||
pn_info_string(HEADER_SPN, connector.supplier, connector.spn)
|
||||
),
|
||||
],
|
||||
[
|
||||
html_line_breaks(connector.type),
|
||||
html_line_breaks(connector.subtype),
|
||||
f'{connector.pincount}-pin' if connector.show_pincount else None,
|
||||
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,
|
||||
f"{connector.pincount}-pin" if connector.show_pincount else None,
|
||||
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)]]
|
||||
[html_caption(connector.image)],
|
||||
]
|
||||
rows.extend(get_additional_component_table(self, connector))
|
||||
rows.append([html_line_breaks(connector.notes)])
|
||||
html.extend(nested_html_table(rows, html_bgcolor_attr(connector.bgcolor)))
|
||||
|
||||
if connector.style != 'simple':
|
||||
if connector.style != "simple":
|
||||
pinhtml = []
|
||||
pinhtml.append('<table border="0" cellspacing="0" cellpadding="3" cellborder="1">')
|
||||
pinhtml.append(
|
||||
'<table border="0" cellspacing="0" cellpadding="3" cellborder="1">'
|
||||
)
|
||||
|
||||
for pinindex, (pinname, pinlabel, pincolor) in enumerate(zip_longest(connector.pins, connector.pinlabels, connector.pincolors)):
|
||||
if connector.hide_disconnected_pins and not connector.visible_pins.get(pinname, False):
|
||||
for pinindex, (pinname, pinlabel, pincolor) in enumerate(
|
||||
zip_longest(
|
||||
connector.pins, connector.pinlabels, connector.pincolors
|
||||
)
|
||||
):
|
||||
if (
|
||||
connector.hide_disconnected_pins
|
||||
and not connector.visible_pins.get(pinname, False)
|
||||
):
|
||||
continue
|
||||
pinhtml.append(' <tr>')
|
||||
pinhtml.append(" <tr>")
|
||||
if connector.ports_left:
|
||||
pinhtml.append(f' <td port="p{pinindex+1}l">{pinname}</td>')
|
||||
if pinlabel:
|
||||
pinhtml.append(f' <td>{pinlabel}</td>')
|
||||
pinhtml.append(f" <td>{pinlabel}</td>")
|
||||
if connector.pincolors:
|
||||
if pincolor in wv_colors._color_hex.keys():
|
||||
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>')
|
||||
pinhtml.append( ' </tr></table>')
|
||||
pinhtml.append( ' </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>'
|
||||
)
|
||||
pinhtml.append(" </tr></table>")
|
||||
pinhtml.append(" </td>")
|
||||
else:
|
||||
pinhtml.append( ' <td colspan="2"></td>')
|
||||
pinhtml.append(' <td colspan="2"></td>')
|
||||
|
||||
if connector.ports_right:
|
||||
pinhtml.append(f' <td port="p{pinindex+1}r">{pinname}</td>')
|
||||
pinhtml.append(' </tr>')
|
||||
pinhtml.append(" </tr>")
|
||||
|
||||
pinhtml.append(' </table>')
|
||||
pinhtml.append(" </table>")
|
||||
|
||||
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)
|
||||
dot.node(connector.name, label=f'<\n{html}\n>', shape='box', style='filled',
|
||||
fillcolor=translate_color(self.options.bgcolor_connector, "HEX"))
|
||||
html = "\n".join(html)
|
||||
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')
|
||||
dot.attr("edge", color="#000000:#ffffff:#000000")
|
||||
if connector.ports_left:
|
||||
loop_side = 'l'
|
||||
loop_dir = 'w'
|
||||
loop_side = "l"
|
||||
loop_dir = "w"
|
||||
elif connector.ports_right:
|
||||
loop_side = 'r'
|
||||
loop_dir = 'e'
|
||||
loop_side = "r"
|
||||
loop_dir = "e"
|
||||
else:
|
||||
raise Exception('No side for loops')
|
||||
raise Exception("No side for loops")
|
||||
for loop in connector.loops:
|
||||
dot.edge(f'{connector.name}:p{loop[0]}{loop_side}:{loop_dir}',
|
||||
f'{connector.name}:p{loop[1]}{loop_side}:{loop_dir}')
|
||||
|
||||
dot.edge(
|
||||
f"{connector.name}:p{loop[0]}{loop_side}:{loop_dir}",
|
||||
f"{connector.name}:p{loop[1]}{loop_side}:{loop_dir}",
|
||||
)
|
||||
|
||||
# determine if there are double- or triple-colored wires in the harness;
|
||||
# if so, pad single-color wires to make all wires of equal thickness
|
||||
pad = any(len(colorstr) > 2 for cable in self.cables.values() for colorstr in cable.colors)
|
||||
pad = any(
|
||||
len(colorstr) > 2
|
||||
for cable in self.cables.values()
|
||||
for colorstr in cable.colors
|
||||
)
|
||||
|
||||
for cable in self.cables.values():
|
||||
|
||||
html = []
|
||||
|
||||
awg_fmt = ''
|
||||
awg_fmt = ""
|
||||
if cable.show_equiv:
|
||||
# Only convert units we actually know about, i.e. currently
|
||||
# mm2 and awg --- other units _are_ technically allowed,
|
||||
# and passed through as-is.
|
||||
if cable.gauge_unit =='mm\u00B2':
|
||||
awg_fmt = f' ({awg_equiv(cable.gauge)} AWG)'
|
||||
elif cable.gauge_unit.upper() == 'AWG':
|
||||
awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)'
|
||||
if cable.gauge_unit == "mm\u00B2":
|
||||
awg_fmt = f" ({awg_equiv(cable.gauge)} AWG)"
|
||||
elif cable.gauge_unit.upper() == "AWG":
|
||||
awg_fmt = f" ({mm2_equiv(cable.gauge)} mm\u00B2)"
|
||||
|
||||
rows = [[f'{html_bgcolor(cable.bgcolor_title)}{remove_links(cable.name)}'
|
||||
if cable.show_name else None],
|
||||
[pn_info_string(HEADER_PN, None,
|
||||
remove_links(cable.pn)) if not isinstance(cable.pn, list) else None,
|
||||
html_line_breaks(pn_info_string(HEADER_MPN,
|
||||
cable.manufacturer if not isinstance(cable.manufacturer, list) else None,
|
||||
cable.mpn if not isinstance(cable.mpn, list) else None)),
|
||||
html_line_breaks(pn_info_string(HEADER_SPN,
|
||||
cable.supplier if not isinstance(cable.supplier, list) else None,
|
||||
cable.spn if not isinstance(cable.spn, list) else None))],
|
||||
[html_line_breaks(cable.type),
|
||||
f'{cable.wirecount}x' if cable.show_wirecount else None,
|
||||
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,
|
||||
translate_color(cable.color, self.options.color_mode) if cable.color else None,
|
||||
html_colorbar(cable.color)],
|
||||
'<!-- wire table -->',
|
||||
rows = [
|
||||
[
|
||||
f"{html_bgcolor(cable.bgcolor_title)}{remove_links(cable.name)}"
|
||||
if cable.show_name
|
||||
else None
|
||||
],
|
||||
[
|
||||
pn_info_string(HEADER_PN, None, remove_links(cable.pn))
|
||||
if not isinstance(cable.pn, list)
|
||||
else None,
|
||||
html_line_breaks(
|
||||
pn_info_string(
|
||||
HEADER_MPN,
|
||||
cable.manufacturer
|
||||
if not isinstance(cable.manufacturer, list)
|
||||
else None,
|
||||
cable.mpn if not isinstance(cable.mpn, list) else None,
|
||||
)
|
||||
),
|
||||
html_line_breaks(
|
||||
pn_info_string(
|
||||
HEADER_SPN,
|
||||
cable.supplier
|
||||
if not isinstance(cable.supplier, list)
|
||||
else None,
|
||||
cable.spn if not isinstance(cable.spn, list) else None,
|
||||
)
|
||||
),
|
||||
],
|
||||
[
|
||||
html_line_breaks(cable.type),
|
||||
f"{cable.wirecount}x" if cable.show_wirecount else None,
|
||||
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,
|
||||
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)]]
|
||||
[html_caption(cable.image)],
|
||||
]
|
||||
|
||||
rows.extend(get_additional_component_table(self, cable))
|
||||
rows.append([html_line_breaks(cable.notes)])
|
||||
html.extend(nested_html_table(rows, html_bgcolor_attr(cable.bgcolor)))
|
||||
|
||||
wirehtml = []
|
||||
wirehtml.append('<table border="0" cellspacing="0" cellborder="0">') # conductor table
|
||||
wirehtml.append(' <tr><td> </td></tr>')
|
||||
wirehtml.append(
|
||||
'<table border="0" cellspacing="0" cellborder="0">'
|
||||
) # conductor table
|
||||
wirehtml.append(" <tr><td> </td></tr>")
|
||||
|
||||
for i, (connection_color, wirelabel) in enumerate(zip_longest(cable.colors, cable.wirelabels), 1):
|
||||
wirehtml.append(' <tr>')
|
||||
wirehtml.append(f' <td><!-- {i}_in --></td>')
|
||||
wirehtml.append(f' <td>')
|
||||
for i, (connection_color, wirelabel) in enumerate(
|
||||
zip_longest(cable.colors, cable.wirelabels), 1
|
||||
):
|
||||
wirehtml.append(" <tr>")
|
||||
wirehtml.append(f" <td><!-- {i}_in --></td>")
|
||||
wirehtml.append(f" <td>")
|
||||
|
||||
wireinfo = []
|
||||
if cable.show_wirenumbers:
|
||||
wireinfo.append(str(i))
|
||||
colorstr = wv_colors.translate_color(connection_color, self.options.color_mode)
|
||||
colorstr = wv_colors.translate_color(
|
||||
connection_color, self.options.color_mode
|
||||
)
|
||||
if colorstr:
|
||||
wireinfo.append(colorstr)
|
||||
if cable.wirelabels:
|
||||
wireinfo.append(wirelabel if wirelabel is not None else '')
|
||||
wireinfo.append(wirelabel if wirelabel is not None else "")
|
||||
wirehtml.append(f' {":".join(wireinfo)}')
|
||||
|
||||
wirehtml.append(f' </td>')
|
||||
wirehtml.append(f' <td><!-- {i}_out --></td>')
|
||||
wirehtml.append(' </tr>')
|
||||
wirehtml.append(f" </td>")
|
||||
wirehtml.append(f" <td><!-- {i}_out --></td>")
|
||||
wirehtml.append(" </tr>")
|
||||
|
||||
bgcolors = ['#000000'] + get_color_hex(connection_color, pad=pad) + ['#000000']
|
||||
wirehtml.append(f' <tr>')
|
||||
wirehtml.append(f' <td colspan="3" border="0" cellspacing="0" cellpadding="0" port="w{i}" height="{(2 * len(bgcolors))}">')
|
||||
wirehtml.append(' <table cellspacing="0" cellborder="0" border="0">')
|
||||
for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors
|
||||
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" height="2" bgcolor="{bgcolor if bgcolor != "" else wv_colors.default_color}" border="0"></td></tr>')
|
||||
wirehtml.append(' </table>')
|
||||
wirehtml.append(' </td>')
|
||||
wirehtml.append(' </tr>')
|
||||
if cable.category == 'bundle': # for bundles individual wires can have part information
|
||||
bgcolors = (
|
||||
["#000000"] + get_color_hex(connection_color, pad=pad) + ["#000000"]
|
||||
)
|
||||
wirehtml.append(f" <tr>")
|
||||
wirehtml.append(
|
||||
f' <td colspan="3" border="0" cellspacing="0" cellpadding="0" port="w{i}" height="{(2 * len(bgcolors))}">'
|
||||
)
|
||||
wirehtml.append(
|
||||
' <table cellspacing="0" cellborder="0" border="0">'
|
||||
)
|
||||
for j, bgcolor in enumerate(
|
||||
bgcolors[::-1]
|
||||
): # Reverse to match the curved wires when more than 2 colors
|
||||
wirehtml.append(
|
||||
f' <tr><td colspan="3" cellpadding="0" height="2" bgcolor="{bgcolor if bgcolor != "" else wv_colors.default_color}" border="0"></td></tr>'
|
||||
)
|
||||
wirehtml.append(" </table>")
|
||||
wirehtml.append(" </td>")
|
||||
wirehtml.append(" </tr>")
|
||||
if (
|
||||
cable.category == "bundle"
|
||||
): # for bundles individual wires can have part information
|
||||
# create a list of wire parameters
|
||||
wireidentification = []
|
||||
if isinstance(cable.pn, list):
|
||||
wireidentification.append(pn_info_string(HEADER_PN, None, remove_links(cable.pn[i - 1])))
|
||||
manufacturer_info = pn_info_string(HEADER_MPN,
|
||||
cable.manufacturer[i - 1] if isinstance(cable.manufacturer, list) else None,
|
||||
cable.mpn[i - 1] if isinstance(cable.mpn, list) else None)
|
||||
supplier_info = pn_info_string(HEADER_SPN,
|
||||
cable.supplier[i - 1] if isinstance(cable.supplier, list) else None,
|
||||
cable.spn[i - 1] if isinstance(cable.spn, list) else None)
|
||||
wireidentification.append(
|
||||
pn_info_string(
|
||||
HEADER_PN, None, remove_links(cable.pn[i - 1])
|
||||
)
|
||||
)
|
||||
manufacturer_info = pn_info_string(
|
||||
HEADER_MPN,
|
||||
cable.manufacturer[i - 1]
|
||||
if isinstance(cable.manufacturer, list)
|
||||
else None,
|
||||
cable.mpn[i - 1] if isinstance(cable.mpn, list) else None,
|
||||
)
|
||||
supplier_info = pn_info_string(
|
||||
HEADER_SPN,
|
||||
cable.supplier[i - 1]
|
||||
if isinstance(cable.supplier, list)
|
||||
else None,
|
||||
cable.spn[i - 1] if isinstance(cable.spn, list) else None,
|
||||
)
|
||||
if manufacturer_info:
|
||||
wireidentification.append(html_line_breaks(manufacturer_info))
|
||||
if supplier_info:
|
||||
wireidentification.append(html_line_breaks(supplier_info))
|
||||
# print parameters into a table row under the wire
|
||||
if len(wireidentification) > 0 :
|
||||
if len(wireidentification) > 0:
|
||||
wirehtml.append(' <tr><td colspan="3">')
|
||||
wirehtml.append(' <table border="0" cellspacing="0" cellborder="0"><tr>')
|
||||
wirehtml.append(
|
||||
' <table border="0" cellspacing="0" cellborder="0"><tr>'
|
||||
)
|
||||
for attrib in wireidentification:
|
||||
wirehtml.append(f' <td>{attrib}</td>')
|
||||
wirehtml.append(' </tr></table>')
|
||||
wirehtml.append(' </td></tr>')
|
||||
wirehtml.append(f" <td>{attrib}</td>")
|
||||
wirehtml.append(" </tr></table>")
|
||||
wirehtml.append(" </td></tr>")
|
||||
|
||||
if cable.shield:
|
||||
wirehtml.append(' <tr><td> </td></tr>') # spacer
|
||||
wirehtml.append(' <tr>')
|
||||
wirehtml.append(' <td><!-- s_in --></td>')
|
||||
wirehtml.append(' <td>Shield</td>')
|
||||
wirehtml.append(' <td><!-- s_out --></td>')
|
||||
wirehtml.append(' </tr>')
|
||||
wirehtml.append(" <tr><td> </td></tr>") # spacer
|
||||
wirehtml.append(" <tr>")
|
||||
wirehtml.append(" <td><!-- s_in --></td>")
|
||||
wirehtml.append(" <td>Shield</td>")
|
||||
wirehtml.append(" <td><!-- s_out --></td>")
|
||||
wirehtml.append(" </tr>")
|
||||
if isinstance(cable.shield, str):
|
||||
# shield is shown with specified color and black borders
|
||||
shield_color_hex = wv_colors.get_color_hex(cable.shield)[0]
|
||||
attributes = f'height="6" bgcolor="{shield_color_hex}" border="2" sides="tb"'
|
||||
attributes = (
|
||||
f'height="6" bgcolor="{shield_color_hex}" border="2" sides="tb"'
|
||||
)
|
||||
else:
|
||||
# shield is shown as a thin black wire
|
||||
attributes = f'height="2" bgcolor="#000000" border="0"'
|
||||
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" {attributes} port="ws"></td></tr>')
|
||||
wirehtml.append(
|
||||
f' <tr><td colspan="3" cellpadding="0" {attributes} port="ws"></td></tr>'
|
||||
)
|
||||
|
||||
wirehtml.append(' <tr><td> </td></tr>')
|
||||
wirehtml.append(' </table>')
|
||||
wirehtml.append(" <tr><td> </td></tr>")
|
||||
wirehtml.append(" </table>")
|
||||
|
||||
html = [row.replace('<!-- wire table -->', '\n'.join(wirehtml)) for row in html]
|
||||
html = [
|
||||
row.replace("<!-- wire table -->", "\n".join(wirehtml)) for row in html
|
||||
]
|
||||
|
||||
# connections
|
||||
for connection in cable.connections:
|
||||
if isinstance(connection.via_port, int): # check if it's an actual wire and not a shield
|
||||
dot.attr('edge', color=':'.join(['#000000'] + wv_colors.get_color_hex(cable.colors[connection.via_port - 1], pad=pad) + ['#000000']))
|
||||
if isinstance(
|
||||
connection.via_port, int
|
||||
): # check if it's an actual wire and not a shield
|
||||
dot.attr(
|
||||
"edge",
|
||||
color=":".join(
|
||||
["#000000"]
|
||||
+ wv_colors.get_color_hex(
|
||||
cable.colors[connection.via_port - 1], pad=pad
|
||||
)
|
||||
+ ["#000000"]
|
||||
),
|
||||
)
|
||||
else: # it's a shield connection
|
||||
# shield is shown with specified color and black borders, or as a thin black wire otherwise
|
||||
dot.attr('edge', color=':'.join(['#000000', shield_color_hex, '#000000']) if isinstance(cable.shield, str) else '#000000')
|
||||
dot.attr(
|
||||
"edge",
|
||||
color=":".join(["#000000", shield_color_hex, "#000000"])
|
||||
if isinstance(cable.shield, str)
|
||||
else "#000000",
|
||||
)
|
||||
if connection.from_pin is not None: # connect to left
|
||||
from_connector = self.connectors[connection.from_name]
|
||||
from_pin_index = from_connector.pins.index(connection.from_pin)
|
||||
from_port_str = f':p{from_pin_index+1}r' if from_connector.style != 'simple' else ''
|
||||
code_left_1 = f'{connection.from_name}{from_port_str}:e'
|
||||
code_left_2 = f'{cable.name}:w{connection.via_port}:w'
|
||||
from_port_str = (
|
||||
f":p{from_pin_index+1}r"
|
||||
if from_connector.style != "simple"
|
||||
else ""
|
||||
)
|
||||
code_left_1 = f"{connection.from_name}{from_port_str}:e"
|
||||
code_left_2 = f"{cable.name}:w{connection.via_port}:w"
|
||||
dot.edge(code_left_1, code_left_2)
|
||||
if from_connector.show_name:
|
||||
from_info = [str(connection.from_name), str(connection.from_pin)]
|
||||
from_info = [
|
||||
str(connection.from_name),
|
||||
str(connection.from_pin),
|
||||
]
|
||||
if from_connector.pinlabels:
|
||||
pinlabel = from_connector.pinlabels[from_pin_index]
|
||||
if pinlabel != '':
|
||||
if pinlabel != "":
|
||||
from_info.append(pinlabel)
|
||||
from_string = ':'.join(from_info)
|
||||
from_string = ":".join(from_info)
|
||||
else:
|
||||
from_string = ''
|
||||
html = [row.replace(f'<!-- {connection.via_port}_in -->', from_string) for row in html]
|
||||
from_string = ""
|
||||
html = [
|
||||
row.replace(f"<!-- {connection.via_port}_in -->", from_string)
|
||||
for row in html
|
||||
]
|
||||
if connection.to_pin is not None: # connect to right
|
||||
to_connector = self.connectors[connection.to_name]
|
||||
to_pin_index = to_connector.pins.index(connection.to_pin)
|
||||
to_port_str = f':p{to_pin_index+1}l' if to_connector.style != 'simple' else ''
|
||||
code_right_1 = f'{cable.name}:w{connection.via_port}:e'
|
||||
code_right_2 = f'{connection.to_name}{to_port_str}:w'
|
||||
to_port_str = (
|
||||
f":p{to_pin_index+1}l" if to_connector.style != "simple" else ""
|
||||
)
|
||||
code_right_1 = f"{cable.name}:w{connection.via_port}:e"
|
||||
code_right_2 = f"{connection.to_name}{to_port_str}:w"
|
||||
dot.edge(code_right_1, code_right_2)
|
||||
if to_connector.show_name:
|
||||
to_info = [str(connection.to_name), str(connection.to_pin)]
|
||||
if to_connector.pinlabels:
|
||||
pinlabel = to_connector.pinlabels[to_pin_index]
|
||||
if pinlabel != '':
|
||||
if pinlabel != "":
|
||||
to_info.append(pinlabel)
|
||||
to_string = ':'.join(to_info)
|
||||
to_string = ":".join(to_info)
|
||||
else:
|
||||
to_string = ''
|
||||
html = [row.replace(f'<!-- {connection.via_port}_out -->', to_string) for row in html]
|
||||
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=style, fillcolor=translate_color(bgcolor, "HEX"))
|
||||
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=style,
|
||||
fillcolor=translate_color(bgcolor, "HEX"),
|
||||
)
|
||||
|
||||
def typecheck(name: str, value: Any, expect: type) -> None:
|
||||
if not isinstance(value, expect):
|
||||
raise Exception(f'Unexpected value type of {name}: Expected {expect}, got {type(value)}\n{value}')
|
||||
raise Exception(
|
||||
f"Unexpected value type of {name}: Expected {expect}, got {type(value)}\n{value}"
|
||||
)
|
||||
|
||||
# TODO?: Differ between override attributes and HTML?
|
||||
if self.tweak.override is not None:
|
||||
typecheck('tweak.override', self.tweak.override, dict)
|
||||
typecheck("tweak.override", self.tweak.override, dict)
|
||||
for k, d in self.tweak.override.items():
|
||||
typecheck(f'tweak.override.{k} key', k, str)
|
||||
typecheck(f'tweak.override.{k} value', d, dict)
|
||||
typecheck(f"tweak.override.{k} key", k, str)
|
||||
typecheck(f"tweak.override.{k} value", d, dict)
|
||||
for a, v in d.items():
|
||||
typecheck(f'tweak.override.{k}.{a} key', a, str)
|
||||
typecheck(f'tweak.override.{k}.{a} value', v, (str, type(None)))
|
||||
typecheck(f"tweak.override.{k}.{a} key", a, str)
|
||||
typecheck(f"tweak.override.{k}.{a} value", v, (str, type(None)))
|
||||
|
||||
# Override generated attributes of selected entries matching tweak.override.
|
||||
for i, entry in enumerate(dot.body):
|
||||
if isinstance(entry, str):
|
||||
# Find a possibly quoted keyword after leading TAB(s) and followed by [ ].
|
||||
match = re.match(r'^\t*(")?((?(1)[^"]|[^ "])+)(?(1)") \[.*\]$', entry, re.S)
|
||||
match = re.match(
|
||||
r'^\t*(")?((?(1)[^"]|[^ "])+)(?(1)") \[.*\]$', entry, re.S
|
||||
)
|
||||
keyword = match and match[2]
|
||||
if keyword in self.tweak.override.keys():
|
||||
for attr, value in self.tweak.override[keyword].items():
|
||||
if value is None:
|
||||
entry, n_subs = re.subn(f'( +)?{attr}=("[^"]*"|[^] ]*)(?(1)| *)', '', entry)
|
||||
entry, n_subs = re.subn(
|
||||
f'( +)?{attr}=("[^"]*"|[^] ]*)(?(1)| *)', "", entry
|
||||
)
|
||||
if n_subs < 1:
|
||||
print(f'Harness.create_graph() warning: {attr} not found in {keyword}!')
|
||||
print(
|
||||
f"Harness.create_graph() warning: {attr} not found in {keyword}!"
|
||||
)
|
||||
elif n_subs > 1:
|
||||
print(f'Harness.create_graph() warning: {attr} removed {n_subs} times in {keyword}!')
|
||||
print(
|
||||
f"Harness.create_graph() warning: {attr} removed {n_subs} times in {keyword}!"
|
||||
)
|
||||
continue
|
||||
|
||||
if len(value) == 0 or ' ' in value:
|
||||
value = value.replace('"', r'\"')
|
||||
if len(value) == 0 or " " in value:
|
||||
value = value.replace('"', r"\"")
|
||||
value = f'"{value}"'
|
||||
entry, n_subs = re.subn(f'{attr}=("[^"]*"|[^] ]*)', f'{attr}={value}', entry)
|
||||
entry, n_subs = re.subn(
|
||||
f'{attr}=("[^"]*"|[^] ]*)', f"{attr}={value}", entry
|
||||
)
|
||||
if n_subs < 1:
|
||||
# If attr not found, then append it
|
||||
entry = re.sub(r'\]$', f' {attr}={value}]', entry)
|
||||
entry = re.sub(r"\]$", f" {attr}={value}]", entry)
|
||||
elif n_subs > 1:
|
||||
print(f'Harness.create_graph() warning: {attr} overridden {n_subs} times in {keyword}!')
|
||||
print(
|
||||
f"Harness.create_graph() warning: {attr} overridden {n_subs} times in {keyword}!"
|
||||
)
|
||||
|
||||
dot.body[i] = entry
|
||||
|
||||
if self.tweak.append is not None:
|
||||
if isinstance(self.tweak.append, list):
|
||||
for i, element in enumerate(self.tweak.append, 1):
|
||||
typecheck(f'tweak.append[{i}]', element, str)
|
||||
typecheck(f"tweak.append[{i}]", element, str)
|
||||
dot.body.extend(self.tweak.append)
|
||||
else:
|
||||
typecheck('tweak.append', self.tweak.append, str)
|
||||
typecheck("tweak.append", self.tweak.append, str)
|
||||
dot.body.append(self.tweak.append)
|
||||
|
||||
for mate in self.mates:
|
||||
if mate.shape[0] == '<' and mate.shape[-1] == '>':
|
||||
dir = 'both'
|
||||
elif mate.shape[0] == '<':
|
||||
dir = 'back'
|
||||
elif mate.shape[-1] == '>':
|
||||
dir = 'forward'
|
||||
if mate.shape[0] == "<" and mate.shape[-1] == ">":
|
||||
dir = "both"
|
||||
elif mate.shape[0] == "<":
|
||||
dir = "back"
|
||||
elif mate.shape[-1] == ">":
|
||||
dir = "forward"
|
||||
else:
|
||||
dir = 'none'
|
||||
dir = "none"
|
||||
|
||||
if isinstance(mate, MatePin):
|
||||
color = '#000000'
|
||||
color = "#000000"
|
||||
elif isinstance(mate, MateComponent):
|
||||
color = '#000000:#000000'
|
||||
color = "#000000:#000000"
|
||||
else:
|
||||
raise Exception(f'{mate} is an unknown mate')
|
||||
raise Exception(f"{mate} is an unknown mate")
|
||||
|
||||
from_connector = self.connectors[mate.from_name]
|
||||
if isinstance(mate, MatePin) and self.connectors[mate.from_name].style != 'simple':
|
||||
if (
|
||||
isinstance(mate, MatePin)
|
||||
and self.connectors[mate.from_name].style != "simple"
|
||||
):
|
||||
from_pin_index = from_connector.pins.index(mate.from_pin)
|
||||
from_port_str = f':p{from_pin_index+1}r'
|
||||
from_port_str = f":p{from_pin_index+1}r"
|
||||
else: # MateComponent or style == 'simple'
|
||||
from_port_str = ''
|
||||
if isinstance(mate, MatePin) and self.connectors[mate.to_name].style != 'simple':
|
||||
from_port_str = ""
|
||||
if (
|
||||
isinstance(mate, MatePin)
|
||||
and self.connectors[mate.to_name].style != "simple"
|
||||
):
|
||||
to_pin_index = to_connector.pins.index(mate.to_pin)
|
||||
to_port_str = f':p{to_pin_index+1}l' if isinstance(mate, MatePin) and self.connectors[mate.to_name].style != 'simple' else ''
|
||||
to_port_str = (
|
||||
f":p{to_pin_index+1}l"
|
||||
if isinstance(mate, MatePin)
|
||||
and self.connectors[mate.to_name].style != "simple"
|
||||
else ""
|
||||
)
|
||||
else: # MateComponent or style == 'simple'
|
||||
to_port_str = ''
|
||||
code_from = f'{mate.from_name}{from_port_str}:e'
|
||||
to_port_str = ""
|
||||
code_from = f"{mate.from_name}{from_port_str}:e"
|
||||
to_connector = self.connectors[mate.to_name]
|
||||
code_to = f'{mate.to_name}{to_port_str}:w'
|
||||
code_to = f"{mate.to_name}{to_port_str}:w"
|
||||
|
||||
dot.attr('edge', color=color, style='dashed', dir=dir)
|
||||
dot.attr("edge", color=color, style="dashed", dir=dir)
|
||||
dot.edge(code_from, code_to)
|
||||
|
||||
return dot
|
||||
@ -492,52 +688,64 @@ class Harness:
|
||||
@property
|
||||
def png(self):
|
||||
from io import BytesIO
|
||||
|
||||
graph = self.graph
|
||||
data = BytesIO()
|
||||
data.write(graph.pipe(format='png'))
|
||||
data.write(graph.pipe(format="png"))
|
||||
data.seek(0)
|
||||
return data.read()
|
||||
|
||||
@property
|
||||
def svg(self):
|
||||
from io import BytesIO
|
||||
|
||||
graph = self.graph
|
||||
data = BytesIO()
|
||||
data.write(graph.pipe(format='svg'))
|
||||
data.write(graph.pipe(format="svg"))
|
||||
data.seek(0)
|
||||
return data.read()
|
||||
|
||||
def output(self, filename: (str, Path), view: bool = False, cleanup: bool = True, fmt: tuple = ('html','png','svg','tsv')) -> None:
|
||||
def output(
|
||||
self,
|
||||
filename: (str, Path),
|
||||
view: bool = False,
|
||||
cleanup: bool = True,
|
||||
fmt: tuple = ("html", "png", "svg", "tsv"),
|
||||
) -> None:
|
||||
# graphical output
|
||||
graph = self.graph
|
||||
svg_already_exists = Path(f'{filename}.svg').exists() # if SVG already exists, do not delete later
|
||||
svg_already_exists = Path(
|
||||
f"{filename}.svg"
|
||||
).exists() # if SVG already exists, do not delete later
|
||||
# graphical output
|
||||
for f in fmt:
|
||||
if f in ('png', 'svg', 'html'):
|
||||
if f == 'html': # if HTML format is specified,
|
||||
f = 'svg' # generate SVG for embedding into HTML
|
||||
if f in ("png", "svg", "html"):
|
||||
if f == "html": # if HTML format is specified,
|
||||
f = "svg" # generate SVG for embedding into HTML
|
||||
# TODO: prevent rendering SVG twice when both SVG and HTML are specified
|
||||
graph.format = f
|
||||
graph.render(filename=filename, view=view, cleanup=cleanup)
|
||||
# GraphViz output
|
||||
if 'gv' in fmt:
|
||||
graph.save(filename=f'{filename}.gv')
|
||||
if "gv" in fmt:
|
||||
graph.save(filename=f"{filename}.gv")
|
||||
# BOM output
|
||||
bomlist = bom_list(self.bom())
|
||||
if 'tsv' in fmt:
|
||||
with open_file_write(f'{filename}.bom.tsv') as file:
|
||||
if "tsv" in fmt:
|
||||
with open_file_write(f"{filename}.bom.tsv") as file:
|
||||
file.write(tuplelist2tsv(bomlist))
|
||||
if 'csv' in fmt:
|
||||
print('CSV output is not yet supported') # TODO: implement CSV output (preferrably using CSV library)
|
||||
if "csv" in fmt:
|
||||
print(
|
||||
"CSV output is not yet supported"
|
||||
) # TODO: implement CSV output (preferrably using CSV library)
|
||||
# HTML output
|
||||
if 'html' in fmt:
|
||||
if "html" in fmt:
|
||||
generate_html_output(filename, bomlist, self.metadata, self.options)
|
||||
# PDF output
|
||||
if 'pdf' in fmt:
|
||||
print('PDF output is not yet supported') # TODO: implement PDF output
|
||||
if "pdf" in fmt:
|
||||
print("PDF output is not yet supported") # TODO: implement PDF output
|
||||
# delete SVG if not needed
|
||||
if 'html' in fmt and not 'svg' in fmt and not svg_already_exists:
|
||||
Path(f'{filename}.svg').unlink()
|
||||
if "html" in fmt and not "svg" in fmt and not svg_already_exists:
|
||||
Path(f"{filename}.svg").unlink()
|
||||
|
||||
def bom(self):
|
||||
if not self._bom:
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Please don't import anything in this file to avoid issues when it is imported in setup.py
|
||||
|
||||
__version__ = '0.4-dev'
|
||||
__version__ = "0.4-dev"
|
||||
|
||||
CMD_NAME = 'wireviz' # Lower case command and module name
|
||||
APP_NAME = 'WireViz' # Application name in texts meant to be human readable
|
||||
APP_URL = 'https://github.com/formatc1702/WireViz'
|
||||
CMD_NAME = "wireviz" # Lower case command and module name
|
||||
APP_NAME = "WireViz" # Application name in texts meant to be human readable
|
||||
APP_URL = "https://github.com/formatc1702/WireViz"
|
||||
|
||||
@ -14,34 +14,36 @@ from wv_helper import open_file_append, open_file_read, open_file_write
|
||||
from wireviz import APP_NAME, __version__, wireviz
|
||||
|
||||
dir = script_path.parent.parent.parent
|
||||
readme = 'readme.md'
|
||||
readme = "readme.md"
|
||||
groups = {
|
||||
'examples': {
|
||||
'path': dir / 'examples',
|
||||
'prefix': 'ex',
|
||||
"examples": {
|
||||
"path": dir / "examples",
|
||||
"prefix": "ex",
|
||||
readme: [], # Include no files
|
||||
'title': 'Example Gallery',
|
||||
"title": "Example Gallery",
|
||||
},
|
||||
'tutorial' : {
|
||||
'path': dir / 'tutorial',
|
||||
'prefix': 'tutorial',
|
||||
readme: ['md', 'yml'], # Include .md and .yml files
|
||||
'title': f'{APP_NAME} Tutorial',
|
||||
"tutorial": {
|
||||
"path": dir / "tutorial",
|
||||
"prefix": "tutorial",
|
||||
readme: ["md", "yml"], # Include .md and .yml files
|
||||
"title": f"{APP_NAME} Tutorial",
|
||||
},
|
||||
'demos' : {
|
||||
'path': dir / 'examples',
|
||||
'prefix': 'demo',
|
||||
"demos": {
|
||||
"path": dir / "examples",
|
||||
"prefix": "demo",
|
||||
},
|
||||
}
|
||||
|
||||
input_extensions = ['.yml']
|
||||
extensions_not_containing_graphviz_output = ['.gv', '.bom.tsv']
|
||||
extensions_containing_graphviz_output = ['.png', '.svg', '.html']
|
||||
generated_extensions = extensions_not_containing_graphviz_output + extensions_containing_graphviz_output
|
||||
input_extensions = [".yml"]
|
||||
extensions_not_containing_graphviz_output = [".gv", ".bom.tsv"]
|
||||
extensions_containing_graphviz_output = [".png", ".svg", ".html"]
|
||||
generated_extensions = (
|
||||
extensions_not_containing_graphviz_output + extensions_containing_graphviz_output
|
||||
)
|
||||
|
||||
|
||||
def collect_filenames(description, groupkey, ext_list):
|
||||
path = groups[groupkey]['path']
|
||||
path = groups[groupkey]["path"]
|
||||
patterns = [f"{groups[groupkey]['prefix']}*{ext}" for ext in ext_list]
|
||||
if ext_list != input_extensions and readme in groups[groupkey]:
|
||||
patterns.append(readme)
|
||||
@ -52,107 +54,141 @@ def collect_filenames(description, groupkey, ext_list):
|
||||
def build_generated(groupkeys):
|
||||
for key in groupkeys:
|
||||
# preparation
|
||||
path = groups[key]['path']
|
||||
path = groups[key]["path"]
|
||||
build_readme = readme in groups[key]
|
||||
if build_readme:
|
||||
include_readme = 'md' in groups[key][readme]
|
||||
include_source = 'yml' in groups[key][readme]
|
||||
include_readme = "md" in groups[key][readme]
|
||||
include_source = "yml" in groups[key][readme]
|
||||
with open_file_write(path / readme) as out:
|
||||
out.write(f'# {groups[key]["title"]}\n\n')
|
||||
# collect and iterate input YAML files
|
||||
for yaml_file in collect_filenames('Building', key, input_extensions):
|
||||
for yaml_file in collect_filenames("Building", key, input_extensions):
|
||||
print(f' "{yaml_file}"')
|
||||
wireviz.parse_file(yaml_file)
|
||||
|
||||
if build_readme:
|
||||
i = ''.join(filter(str.isdigit, yaml_file.stem))
|
||||
i = "".join(filter(str.isdigit, yaml_file.stem))
|
||||
|
||||
with open_file_append(path / readme) as out:
|
||||
if include_readme:
|
||||
with open_file_read(yaml_file.with_suffix('.md')) as info:
|
||||
with open_file_read(yaml_file.with_suffix(".md")) as info:
|
||||
for line in info:
|
||||
out.write(line.replace('## ', f'## {i} - '))
|
||||
out.write('\n\n')
|
||||
out.write(line.replace("## ", f"## {i} - "))
|
||||
out.write("\n\n")
|
||||
else:
|
||||
out.write(f'## Example {i}\n')
|
||||
out.write(f"## Example {i}\n")
|
||||
|
||||
if include_source:
|
||||
with open_file_read(yaml_file) as src:
|
||||
out.write('```yaml\n')
|
||||
out.write("```yaml\n")
|
||||
for line in src:
|
||||
out.write(line)
|
||||
out.write('```\n')
|
||||
out.write('\n')
|
||||
out.write("```\n")
|
||||
out.write("\n")
|
||||
|
||||
out.write(f'\n\n')
|
||||
out.write(f'[Source]({yaml_file.name}) - [Bill of Materials]({yaml_file.stem}.bom.tsv)\n\n\n')
|
||||
out.write(f"\n\n")
|
||||
out.write(
|
||||
f"[Source]({yaml_file.name}) - [Bill of Materials]({yaml_file.stem}.bom.tsv)\n\n\n"
|
||||
)
|
||||
|
||||
|
||||
def clean_generated(groupkeys):
|
||||
for key in groupkeys:
|
||||
# collect and remove files
|
||||
for filename in collect_filenames('Cleaning', key, generated_extensions):
|
||||
for filename in collect_filenames("Cleaning", key, generated_extensions):
|
||||
if filename.is_file():
|
||||
print(f' rm "{filename}"')
|
||||
Path(filename).unlink()
|
||||
|
||||
|
||||
def compare_generated(groupkeys, branch = '', include_graphviz_output = False):
|
||||
def compare_generated(groupkeys, branch="", include_graphviz_output=False):
|
||||
if branch:
|
||||
branch = f' {branch.strip()}'
|
||||
compare_extensions = generated_extensions if include_graphviz_output else extensions_not_containing_graphviz_output
|
||||
branch = f" {branch.strip()}"
|
||||
compare_extensions = (
|
||||
generated_extensions
|
||||
if include_graphviz_output
|
||||
else extensions_not_containing_graphviz_output
|
||||
)
|
||||
for key in groupkeys:
|
||||
# collect and compare files
|
||||
for filename in collect_filenames('Comparing', key, compare_extensions):
|
||||
for filename in collect_filenames("Comparing", key, compare_extensions):
|
||||
cmd = f'git --no-pager diff{branch} -- "{filename}"'
|
||||
print(f' {cmd}')
|
||||
print(f" {cmd}")
|
||||
os.system(cmd)
|
||||
|
||||
|
||||
def restore_generated(groupkeys, branch = ''):
|
||||
def restore_generated(groupkeys, branch=""):
|
||||
if branch:
|
||||
branch = f' {branch.strip()}'
|
||||
branch = f" {branch.strip()}"
|
||||
for key in groupkeys:
|
||||
# collect input YAML files
|
||||
filename_list = collect_filenames('Restoring', key, input_extensions)
|
||||
filename_list = collect_filenames("Restoring", key, input_extensions)
|
||||
# collect files to restore
|
||||
filename_list = [fn.with_suffix(ext) for fn in filename_list for ext in generated_extensions]
|
||||
filename_list = [
|
||||
fn.with_suffix(ext) for fn in filename_list for ext in generated_extensions
|
||||
]
|
||||
if readme in groups[key]:
|
||||
filename_list.append(groups[key]['path'] / readme)
|
||||
filename_list.append(groups[key]["path"] / readme)
|
||||
# restore files
|
||||
for filename in filename_list:
|
||||
cmd = f'git checkout{branch} -- "{filename}"'
|
||||
print(f' {cmd}')
|
||||
print(f" {cmd}")
|
||||
os.system(cmd)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description=f'{APP_NAME} Example Manager',)
|
||||
parser.add_argument('-V', '--version', action='version', version=f'%(prog)s - {APP_NAME} {__version__}')
|
||||
parser.add_argument('action', nargs='?', action='store',
|
||||
choices=['build','clean','compare','diff','restore'], default='build',
|
||||
help='what to do with the generated files (default: build)')
|
||||
parser.add_argument('-c', '--compare-graphviz-output', action='store_true',
|
||||
help='the Graphviz output is also compared (default: False)')
|
||||
parser.add_argument('-b', '--branch', action='store', default='',
|
||||
help='branch or commit to compare with or restore from')
|
||||
parser.add_argument('-g', '--groups', nargs='+',
|
||||
choices=groups.keys(), default=groups.keys(),
|
||||
help='the groups of generated files (default: all)')
|
||||
parser = argparse.ArgumentParser(
|
||||
description=f"{APP_NAME} Example Manager",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-V",
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"%(prog)s - {APP_NAME} {__version__}",
|
||||
)
|
||||
parser.add_argument(
|
||||
"action",
|
||||
nargs="?",
|
||||
action="store",
|
||||
choices=["build", "clean", "compare", "diff", "restore"],
|
||||
default="build",
|
||||
help="what to do with the generated files (default: build)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--compare-graphviz-output",
|
||||
action="store_true",
|
||||
help="the Graphviz output is also compared (default: False)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-b",
|
||||
"--branch",
|
||||
action="store",
|
||||
default="",
|
||||
help="branch or commit to compare with or restore from",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-g",
|
||||
"--groups",
|
||||
nargs="+",
|
||||
choices=groups.keys(),
|
||||
default=groups.keys(),
|
||||
help="the groups of generated files (default: all)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
if args.action == 'build':
|
||||
if args.action == "build":
|
||||
build_generated(args.groups)
|
||||
elif args.action == 'clean':
|
||||
elif args.action == "clean":
|
||||
clean_generated(args.groups)
|
||||
elif args.action == 'compare' or args.action == 'diff':
|
||||
elif args.action == "compare" or args.action == "diff":
|
||||
compare_generated(args.groups, args.branch, args.compare_graphviz_output)
|
||||
elif args.action == 'restore':
|
||||
elif args.action == "restore":
|
||||
restore_generated(args.groups, args.branch)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@ -7,7 +7,7 @@ from typing import Any, Dict, List, Tuple
|
||||
|
||||
import yaml
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent)) # add src/wireviz to PATH
|
||||
|
||||
from wireviz.DataClasses import Metadata, Options, Tweak
|
||||
@ -21,7 +21,13 @@ from wireviz.wv_helper import (
|
||||
)
|
||||
|
||||
|
||||
def parse_text(yaml_str: str, file_out: (str, Path) = None, output_formats: (None, str, Tuple[str]) = ('html','png','svg','tsv'), return_types: (None, str, Tuple[str]) = None, image_paths: List = []) -> Any:
|
||||
def parse_text(
|
||||
yaml_str: str,
|
||||
file_out: (str, Path) = None,
|
||||
output_formats: (None, str, Tuple[str]) = ("html", "png", "svg", "tsv"),
|
||||
return_types: (None, str, Tuple[str]) = None,
|
||||
image_paths: List = [],
|
||||
) -> Any:
|
||||
"""
|
||||
Parses a YAML input string and does the high-level harness conversion
|
||||
|
||||
@ -37,9 +43,22 @@ def parse_text(yaml_str: str, file_out: (str, Path) = None, output_formats: (Non
|
||||
- "harness" - will return the `Harness` instance
|
||||
"""
|
||||
yaml_data = yaml.safe_load(yaml_str)
|
||||
return parse(yaml_data=yaml_data, file_out=file_out, output_formats=output_formats, return_types=return_types, image_paths=image_paths)
|
||||
return parse(
|
||||
yaml_data=yaml_data,
|
||||
file_out=file_out,
|
||||
output_formats=output_formats,
|
||||
return_types=return_types,
|
||||
image_paths=image_paths,
|
||||
)
|
||||
|
||||
def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, str, Tuple[str]) = ('html','png','svg','tsv'), return_types: (None, str, Tuple[str]) = None, image_paths: List = []) -> Any:
|
||||
|
||||
def parse(
|
||||
yaml_data: Dict,
|
||||
file_out: (str, Path) = None,
|
||||
output_formats: (None, str, Tuple[str]) = ("html", "png", "svg", "tsv"),
|
||||
return_types: (None, str, Tuple[str]) = None,
|
||||
image_paths: List = [],
|
||||
) -> Any:
|
||||
"""
|
||||
Parses a YAML dictionary and does the high-level harness conversion
|
||||
|
||||
@ -55,7 +74,6 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
|
||||
- "harness" - will return the `Harness` instance
|
||||
"""
|
||||
|
||||
|
||||
# define variables =========================================================
|
||||
# containers for parsed component data and connection sets
|
||||
template_connectors = {}
|
||||
@ -63,21 +81,25 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
|
||||
connection_sets = []
|
||||
# actual harness
|
||||
harness = Harness(
|
||||
metadata = Metadata(**yaml_data.get('metadata', {})),
|
||||
options = Options(**yaml_data.get('options', {})),
|
||||
tweak = Tweak(**yaml_data.get('tweak', {})),
|
||||
metadata=Metadata(**yaml_data.get("metadata", {})),
|
||||
options=Options(**yaml_data.get("options", {})),
|
||||
tweak=Tweak(**yaml_data.get("tweak", {})),
|
||||
)
|
||||
# others
|
||||
designators_and_templates = {} # store mapping of components to their respective template
|
||||
autogenerated_designators = {} # keep track of auto-generated designators to avoid duplicates
|
||||
designators_and_templates = (
|
||||
{}
|
||||
) # store mapping of components to their respective template
|
||||
autogenerated_designators = (
|
||||
{}
|
||||
) # keep track of auto-generated designators to avoid duplicates
|
||||
|
||||
if 'title' not in harness.metadata:
|
||||
harness.metadata['title'] = Path(file_out).stem
|
||||
if "title" not in harness.metadata:
|
||||
harness.metadata["title"] = Path(file_out).stem
|
||||
|
||||
# add items
|
||||
# parse YAML input file ====================================================
|
||||
|
||||
sections = ['connectors', 'cables', 'connections']
|
||||
sections = ["connectors", "cables", "connections"]
|
||||
types = [dict, dict, list]
|
||||
for sec, ty in zip(sections, types):
|
||||
if sec in yaml_data and type(yaml_data[sec]) == ty: # section exists
|
||||
@ -85,14 +107,18 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
|
||||
if ty == dict:
|
||||
for key, attribs in yaml_data[sec].items():
|
||||
# The Image dataclass might need to open an image file with a relative path.
|
||||
image = attribs.get('image')
|
||||
image = attribs.get("image")
|
||||
if isinstance(image, dict):
|
||||
image_path = image['src']
|
||||
if image_path and not Path(image_path).is_absolute(): # resolve relative image path
|
||||
image['src'] = smart_file_resolve(image_path, image_paths)
|
||||
if sec == 'connectors':
|
||||
image_path = image["src"]
|
||||
if (
|
||||
image_path and not Path(image_path).is_absolute()
|
||||
): # resolve relative image path
|
||||
image["src"] = smart_file_resolve(
|
||||
image_path, image_paths
|
||||
)
|
||||
if sec == "connectors":
|
||||
template_connectors[key] = attribs
|
||||
elif sec == 'cables':
|
||||
elif sec == "cables":
|
||||
template_cables[key] = attribs
|
||||
else: # section exists but is empty
|
||||
pass
|
||||
@ -102,24 +128,28 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
|
||||
elif ty == list:
|
||||
yaml_data[sec] = []
|
||||
|
||||
connection_sets = yaml_data['connections']
|
||||
connection_sets = yaml_data["connections"]
|
||||
|
||||
# go through connection sets, generate and connect components ==============
|
||||
|
||||
template_separator_char = '.' # TODO: make user-configurable (in case user wants to use `.` as part of their template/component names)
|
||||
template_separator_char = "." # TODO: make user-configurable (in case user wants to use `.` as part of their template/component names)
|
||||
|
||||
def resolve_designator(inp, separator):
|
||||
if separator in inp: # generate a new instance of an item
|
||||
if inp.count(separator) > 1:
|
||||
raise Exception(f'{inp} - Found more than one separator ({separator})')
|
||||
raise Exception(f"{inp} - Found more than one separator ({separator})")
|
||||
template, designator = inp.split(separator)
|
||||
if designator == '':
|
||||
autogenerated_designators[template] = autogenerated_designators.get(template, 0) + 1
|
||||
designator = f'__{template}_{autogenerated_designators[template]}'
|
||||
if designator == "":
|
||||
autogenerated_designators[template] = (
|
||||
autogenerated_designators.get(template, 0) + 1
|
||||
)
|
||||
designator = f"__{template}_{autogenerated_designators[template]}"
|
||||
# check if redefining existing component to different template
|
||||
if designator in designators_and_templates:
|
||||
if designators_and_templates[designator] != template:
|
||||
raise Exception(f'Trying to redefine {designator} from {designators_and_templates[designator]} to {template}')
|
||||
raise Exception(
|
||||
f"Trying to redefine {designator} from {designators_and_templates[designator]} to {template}"
|
||||
)
|
||||
else:
|
||||
designators_and_templates[designator] = template
|
||||
else:
|
||||
@ -132,7 +162,7 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
|
||||
|
||||
# utilities to check for alternating connectors and cables/arrows ==========
|
||||
|
||||
alternating_types = ['connector','cable/arrow']
|
||||
alternating_types = ["connector", "cable/arrow"]
|
||||
expected_type = None
|
||||
|
||||
def check_type(designator, template, actual_type):
|
||||
@ -141,7 +171,9 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
|
||||
expected_type = actual_type
|
||||
|
||||
if actual_type != expected_type: # did not alternate
|
||||
raise Exception(f'Expected {expected_type}, but "{designator}" ("{template}") is {actual_type}')
|
||||
raise Exception(
|
||||
f'Expected {expected_type}, but "{designator}" ("{template}") is {actual_type}'
|
||||
)
|
||||
|
||||
def alternate_type(): # flip between connector and cable/arrow
|
||||
nonlocal expected_type
|
||||
@ -155,7 +187,9 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
|
||||
if isinstance(entry, list):
|
||||
connectioncount.append(len(entry))
|
||||
elif isinstance(entry, dict):
|
||||
connectioncount.append(len(expand(list(entry.values())[0]))) # - X1: [1-4,6] yields 5
|
||||
connectioncount.append(
|
||||
len(expand(list(entry.values())[0]))
|
||||
) # - X1: [1-4,6] yields 5
|
||||
else:
|
||||
pass # strings do not reveal connectioncount
|
||||
if not any(connectioncount):
|
||||
@ -172,7 +206,9 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
|
||||
|
||||
# check that all entries are the same length
|
||||
if len(set(connectioncount)) > 1:
|
||||
raise Exception('All items in connection set must reference the same number of connections')
|
||||
raise Exception(
|
||||
"All items in connection set must reference the same number of connections"
|
||||
)
|
||||
# all entries are the same length, connection count is set
|
||||
connectioncount = connectioncount[0]
|
||||
|
||||
@ -185,7 +221,9 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
|
||||
for index, entry in enumerate(connection_set):
|
||||
if isinstance(entry, list):
|
||||
for subindex, item in enumerate(entry):
|
||||
template, designator = resolve_designator(item, template_separator_char)
|
||||
template, designator = resolve_designator(
|
||||
item, template_separator_char
|
||||
)
|
||||
connection_set[index][subindex] = designator
|
||||
elif isinstance(entry, dict):
|
||||
key = list(entry.keys())[0]
|
||||
@ -219,22 +257,30 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
|
||||
template = designators_and_templates[designator]
|
||||
|
||||
if designator in harness.connectors: # existing connector instance
|
||||
check_type(designator, template, 'connector')
|
||||
elif template in template_connectors.keys(): # generate new connector instance from template
|
||||
check_type(designator, template, 'connector')
|
||||
harness.add_connector(name = designator, **template_connectors[template])
|
||||
check_type(designator, template, "connector")
|
||||
elif (
|
||||
template in template_connectors.keys()
|
||||
): # generate new connector instance from template
|
||||
check_type(designator, template, "connector")
|
||||
harness.add_connector(
|
||||
name=designator, **template_connectors[template]
|
||||
)
|
||||
|
||||
elif designator in harness.cables: # existing cable instance
|
||||
check_type(designator, template, 'cable/arrow')
|
||||
elif template in template_cables.keys(): # generate new cable instance from template
|
||||
check_type(designator, template, 'cable/arrow')
|
||||
harness.add_cable(name = designator, **template_cables[template])
|
||||
check_type(designator, template, "cable/arrow")
|
||||
elif (
|
||||
template in template_cables.keys()
|
||||
): # generate new cable instance from template
|
||||
check_type(designator, template, "cable/arrow")
|
||||
harness.add_cable(name=designator, **template_cables[template])
|
||||
|
||||
elif is_arrow(designator):
|
||||
check_type(designator, template, 'cable/arrow')
|
||||
check_type(designator, template, "cable/arrow")
|
||||
# arrows do not need to be generated here
|
||||
else:
|
||||
raise Exception(f'{template} is an unknown template/designator/arrow.')
|
||||
raise Exception(
|
||||
f"{template} is an unknown template/designator/arrow."
|
||||
)
|
||||
|
||||
alternate_type() # entries in connection set must alternate between connectors and cables/arrows
|
||||
|
||||
@ -249,29 +295,49 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
|
||||
designator = list(item.keys())[0]
|
||||
|
||||
if designator in harness.cables:
|
||||
if index_item == 0: # list started with a cable, no connector to join on left side
|
||||
if (
|
||||
index_item == 0
|
||||
): # list started with a cable, no connector to join on left side
|
||||
from_name, from_pin = (None, None)
|
||||
else:
|
||||
from_name, from_pin = get_single_key_and_value(entry[index_item-1])
|
||||
from_name, from_pin = get_single_key_and_value(
|
||||
entry[index_item - 1]
|
||||
)
|
||||
via_name, via_pin = (designator, item[designator])
|
||||
if index_item == len(entry) - 1: # list ends with a cable, no connector to join on right side
|
||||
if (
|
||||
index_item == len(entry) - 1
|
||||
): # list ends with a cable, no connector to join on right side
|
||||
to_name, to_pin = (None, None)
|
||||
else:
|
||||
to_name, to_pin = get_single_key_and_value(entry[index_item+1])
|
||||
harness.connect(from_name, from_pin, via_name, via_pin, to_name, to_pin)
|
||||
to_name, to_pin = get_single_key_and_value(
|
||||
entry[index_item + 1]
|
||||
)
|
||||
harness.connect(
|
||||
from_name, from_pin, via_name, via_pin, to_name, to_pin
|
||||
)
|
||||
|
||||
elif is_arrow(designator):
|
||||
if index_item == 0: # list starts with an arrow
|
||||
raise Exception('An arrow cannot be at the start of a connection set')
|
||||
raise Exception(
|
||||
"An arrow cannot be at the start of a connection set"
|
||||
)
|
||||
elif index_item == len(entry) - 1: # list ends with an arrow
|
||||
raise Exception('An arrow cannot be at the end of a connection set')
|
||||
raise Exception(
|
||||
"An arrow cannot be at the end of a connection set"
|
||||
)
|
||||
|
||||
from_name, from_pin = get_single_key_and_value(entry[index_item-1])
|
||||
from_name, from_pin = get_single_key_and_value(
|
||||
entry[index_item - 1]
|
||||
)
|
||||
via_name, via_pin = (designator, None)
|
||||
to_name, to_pin = get_single_key_and_value(entry[index_item+1])
|
||||
if '-' in designator: # mate pin by pin
|
||||
harness.add_mate_pin(from_name, from_pin, to_name, to_pin, designator)
|
||||
elif '=' in designator and index_entry == 0: # mate two connectors as a whole
|
||||
to_name, to_pin = get_single_key_and_value(entry[index_item + 1])
|
||||
if "-" in designator: # mate pin by pin
|
||||
harness.add_mate_pin(
|
||||
from_name, from_pin, to_name, to_pin, designator
|
||||
)
|
||||
elif (
|
||||
"=" in designator and index_entry == 0
|
||||
): # mate two connectors as a whole
|
||||
harness.add_mate_component(from_name, to_name, designator)
|
||||
|
||||
# harness population completed =============================================
|
||||
@ -291,11 +357,11 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
|
||||
return_types = [t.lower() for t in return_types]
|
||||
|
||||
for rt in return_types:
|
||||
if rt == 'png':
|
||||
if rt == "png":
|
||||
returns.append(harness.png)
|
||||
if rt == 'svg':
|
||||
if rt == "svg":
|
||||
returns.append(harness.svg)
|
||||
if rt == 'harness':
|
||||
if rt == "harness":
|
||||
returns.append(harness)
|
||||
|
||||
return tuple(returns) if len(returns) != 1 else returns[0]
|
||||
@ -314,8 +380,10 @@ def parse_file(yaml_file: str, file_out: (str, Path) = None) -> None:
|
||||
|
||||
parse_text(yaml_str, file_out=file_out, image_paths=[Path(yaml_file).parent])
|
||||
|
||||
def main():
|
||||
print('When running from the command line, please use wv_cli.py instead.')
|
||||
|
||||
if __name__ == '__main__':
|
||||
def main():
|
||||
print("When running from the command line, please use wv_cli.py instead.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@ -9,76 +9,108 @@ from wireviz.wv_colors import translate_color
|
||||
from wireviz.wv_gv_html import html_bgcolor_attr, html_line_breaks
|
||||
from wireviz.wv_helper import clean_whitespace
|
||||
|
||||
BOM_COLUMNS_ALWAYS = ('id', 'description', 'qty', 'unit', 'designators')
|
||||
BOM_COLUMNS_OPTIONAL = ('pn', 'manufacturer', 'mpn', 'supplier', 'spn')
|
||||
BOM_COLUMNS_IN_KEY = ('description', 'unit') + BOM_COLUMNS_OPTIONAL
|
||||
BOM_COLUMNS_ALWAYS = ("id", "description", "qty", "unit", "designators")
|
||||
BOM_COLUMNS_OPTIONAL = ("pn", "manufacturer", "mpn", "supplier", "spn")
|
||||
BOM_COLUMNS_IN_KEY = ("description", "unit") + BOM_COLUMNS_OPTIONAL
|
||||
|
||||
HEADER_PN = 'P/N'
|
||||
HEADER_MPN = 'MPN'
|
||||
HEADER_SPN = 'SPN'
|
||||
HEADER_PN = "P/N"
|
||||
HEADER_MPN = "MPN"
|
||||
HEADER_SPN = "SPN"
|
||||
|
||||
BOMKey = Tuple[str, ...]
|
||||
BOMColumn = str # = Literal[*BOM_COLUMNS_ALWAYS, *BOM_COLUMNS_OPTIONAL]
|
||||
BOMEntry = Dict[BOMColumn, Union[str, int, float, List[str], None]]
|
||||
|
||||
|
||||
def optional_fields(part: Union[Connector, Cable, AdditionalComponent]) -> BOMEntry:
|
||||
"""Return part field values for the optional BOM columns as a dict."""
|
||||
part = asdict(part)
|
||||
return {field: part.get(field) for field in BOM_COLUMNS_OPTIONAL}
|
||||
|
||||
def get_additional_component_table(harness: "Harness", component: Union[Connector, Cable]) -> List[str]:
|
||||
|
||||
def get_additional_component_table(
|
||||
harness: "Harness", component: Union[Connector, Cable]
|
||||
) -> List[str]:
|
||||
"""Return a list of diagram node table row strings with additional components."""
|
||||
rows = []
|
||||
if component.additional_components:
|
||||
rows.append(["Additional components"])
|
||||
for part in component.additional_components:
|
||||
common_args = {
|
||||
'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier),
|
||||
'unit': part.unit,
|
||||
'bgcolor': part.bgcolor,
|
||||
"qty": part.qty * component.get_qty_multiplier(part.qty_multiplier),
|
||||
"unit": part.unit,
|
||||
"bgcolor": part.bgcolor,
|
||||
}
|
||||
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))
|
||||
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:
|
||||
rows.append(component_table_entry(part.description, **common_args, **optional_fields(part)))
|
||||
rows.append(
|
||||
component_table_entry(
|
||||
part.description, **common_args, **optional_fields(part)
|
||||
)
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOMEntry]:
|
||||
"""Return a list of BOM entries with additional components."""
|
||||
bom_entries = []
|
||||
for part in component.additional_components:
|
||||
bom_entries.append({
|
||||
'description': part.description,
|
||||
'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier),
|
||||
'unit': part.unit,
|
||||
'designators': component.name if component.show_name else None,
|
||||
bom_entries.append(
|
||||
{
|
||||
"description": part.description,
|
||||
"qty": part.qty * component.get_qty_multiplier(part.qty_multiplier),
|
||||
"unit": part.unit,
|
||||
"designators": component.name if component.show_name else None,
|
||||
**optional_fields(part),
|
||||
})
|
||||
}
|
||||
)
|
||||
return bom_entries
|
||||
|
||||
|
||||
def bom_entry_key(entry: BOMEntry) -> BOMKey:
|
||||
"""Return a tuple of string values from the dict that must be equal to join BOM entries."""
|
||||
if 'key' not in entry:
|
||||
entry['key'] = tuple(clean_whitespace(make_str(entry.get(c))) for c in BOM_COLUMNS_IN_KEY)
|
||||
return entry['key']
|
||||
if "key" not in entry:
|
||||
entry["key"] = tuple(
|
||||
clean_whitespace(make_str(entry.get(c))) for c in BOM_COLUMNS_IN_KEY
|
||||
)
|
||||
return entry["key"]
|
||||
|
||||
|
||||
def generate_bom(harness: "Harness") -> List[BOMEntry]:
|
||||
"""Return a list of BOM entries generated from the harness."""
|
||||
from wireviz.Harness import Harness # Local import to avoid circular imports
|
||||
|
||||
bom_entries = []
|
||||
# connectors
|
||||
for connector in harness.connectors.values():
|
||||
if not connector.ignore_in_bom:
|
||||
description = ('Connector'
|
||||
+ (f', {connector.type}' if connector.type else '')
|
||||
+ (f', {connector.subtype}' if connector.subtype else '')
|
||||
+ (f', {connector.pincount} pins' if connector.show_pincount 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,
|
||||
description = (
|
||||
"Connector"
|
||||
+ (f", {connector.type}" if connector.type else "")
|
||||
+ (f", {connector.subtype}" if connector.subtype else "")
|
||||
+ (f", {connector.pincount} pins" if connector.show_pincount 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),
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
# add connectors aditional components to bom
|
||||
bom_entries.extend(get_additional_component_bom(connector))
|
||||
@ -87,29 +119,58 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
|
||||
# TODO: If category can have other non-empty values than 'bundle', maybe it should be part of description?
|
||||
for cable in harness.cables.values():
|
||||
if not cable.ignore_in_bom:
|
||||
if cable.category != 'bundle':
|
||||
if cable.category != "bundle":
|
||||
# process cable as a single entity
|
||||
description = ('Cable'
|
||||
+ (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 '')
|
||||
+ (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,
|
||||
description = (
|
||||
"Cable"
|
||||
+ (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 "")
|
||||
+ (
|
||||
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),
|
||||
})
|
||||
}
|
||||
)
|
||||
else:
|
||||
# add each wire from the bundle to the bom
|
||||
for index, color in enumerate(cable.colors):
|
||||
description = ('Wire'
|
||||
+ (f', {cable.type}' if cable.type else '')
|
||||
+ (f', {cable.gauge} {cable.gauge_unit}' if cable.gauge 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()},
|
||||
})
|
||||
description = (
|
||||
"Wire"
|
||||
+ (f", {cable.type}" if cable.type else "")
|
||||
+ (f", {cable.gauge} {cable.gauge_unit}" if cable.gauge 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()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# add cable/bundles aditional components to bom
|
||||
bom_entries.extend(get_additional_component_bom(cable))
|
||||
@ -118,41 +179,61 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
|
||||
bom_entries.extend(harness.additional_bom_items)
|
||||
|
||||
# remove line breaks if present and cleanup any resulting whitespace issues
|
||||
bom_entries = [{k: clean_whitespace(v) for k, v in entry.items()} for entry in bom_entries]
|
||||
bom_entries = [
|
||||
{k: clean_whitespace(v) for k, v in entry.items()} for entry in bom_entries
|
||||
]
|
||||
|
||||
# deduplicate bom
|
||||
bom = []
|
||||
for _, group in groupby(sorted(bom_entries, key=bom_entry_key), key=bom_entry_key):
|
||||
group_entries = list(group)
|
||||
designators = sum((make_list(entry.get('designators')) for entry in group_entries), [])
|
||||
total_qty = sum(entry.get('qty', 1) for entry in group_entries)
|
||||
bom.append({**group_entries[0], 'qty': round(total_qty, 3), 'designators': sorted(set(designators))})
|
||||
designators = sum(
|
||||
(make_list(entry.get("designators")) for entry in group_entries), []
|
||||
)
|
||||
total_qty = sum(entry.get("qty", 1) for entry in group_entries)
|
||||
bom.append(
|
||||
{
|
||||
**group_entries[0],
|
||||
"qty": round(total_qty, 3),
|
||||
"designators": sorted(set(designators)),
|
||||
}
|
||||
)
|
||||
|
||||
# add an incrementing id to each bom entry
|
||||
return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)]
|
||||
return [{**entry, "id": index} for index, entry in enumerate(bom, 1)]
|
||||
|
||||
|
||||
def get_bom_index(bom: List[BOMEntry], target: BOMKey) -> int:
|
||||
"""Return id of BOM entry or raise exception if not found."""
|
||||
for entry in bom:
|
||||
if bom_entry_key(entry) == target:
|
||||
return entry['id']
|
||||
raise Exception('Internal error: No BOM entry found matching: ' + '|'.join(target))
|
||||
return entry["id"]
|
||||
raise Exception("Internal error: No BOM entry found matching: " + "|".join(target))
|
||||
|
||||
|
||||
def bom_list(bom: List[BOMEntry]) -> List[List[str]]:
|
||||
"""Return list of BOM rows as lists of column strings with headings in top row."""
|
||||
keys = list(BOM_COLUMNS_ALWAYS) # Always include this fixed set of BOM columns.
|
||||
for fieldname in BOM_COLUMNS_OPTIONAL: # Include only those optional BOM columns that are in use.
|
||||
for (
|
||||
fieldname
|
||||
) in (
|
||||
BOM_COLUMNS_OPTIONAL
|
||||
): # Include only those optional BOM columns that are in use.
|
||||
if any(entry.get(fieldname) for entry in bom):
|
||||
keys.append(fieldname)
|
||||
# Custom mapping from internal name to BOM column headers.
|
||||
# Headers not specified here are generated by capitilising the internal name.
|
||||
bom_headings = {
|
||||
'pn': HEADER_PN,
|
||||
'mpn': HEADER_MPN,
|
||||
'spn': HEADER_SPN,
|
||||
"pn": HEADER_PN,
|
||||
"mpn": HEADER_MPN,
|
||||
"spn": HEADER_SPN,
|
||||
}
|
||||
return ([[bom_headings.get(k, k.capitalize()) for k in keys]] + # Create header row with key names
|
||||
[[make_str(entry.get(k)) for k in keys] for entry in bom]) # Create string list for each entry row
|
||||
return [
|
||||
[bom_headings.get(k, k.capitalize()) for k in keys]
|
||||
] + [ # Create header row with key names
|
||||
[make_str(entry.get(k)) for k in keys] for entry in bom
|
||||
] # Create string list for each entry row
|
||||
|
||||
|
||||
def component_table_entry(
|
||||
type: str,
|
||||
@ -164,40 +245,48 @@ def component_table_entry(
|
||||
mpn: Optional[str] = None,
|
||||
supplier: Optional[str] = None,
|
||||
spn: Optional[str] = None,
|
||||
) -> str:
|
||||
) -> str:
|
||||
"""Return a diagram node table row string with an additional component."""
|
||||
part_number_list = [
|
||||
pn_info_string(HEADER_PN, None, pn),
|
||||
pn_info_string(HEADER_MPN, manufacturer, mpn),
|
||||
pn_info_string(HEADER_SPN, supplier, spn),
|
||||
]
|
||||
output = (f'{qty}'
|
||||
+ (f' {unit}' if unit else '')
|
||||
+ f' x {type}'
|
||||
+ ('<br/>' if any(part_number_list) else '')
|
||||
+ (', '.join([pn for pn in part_number_list if pn])))
|
||||
output = (
|
||||
f"{qty}"
|
||||
+ (f" {unit}" if unit else "")
|
||||
+ f" x {type}"
|
||||
+ ("<br/>" if any(part_number_list) else "")
|
||||
+ (", ".join([pn for pn in part_number_list if pn]))
|
||||
)
|
||||
# format the above output as left aligned text in a single visible cell
|
||||
# indent is set to two to match the indent in the generated html table
|
||||
return f'''<table border="0" cellspacing="0" cellpadding="3" cellborder="1"{html_bgcolor_attr(bgcolor)}><tr>
|
||||
return f"""<table border="0" cellspacing="0" cellpadding="3" cellborder="1"{html_bgcolor_attr(bgcolor)}><tr>
|
||||
<td align="left" balign="left">{html_line_breaks(output)}</td>
|
||||
</tr></table>'''
|
||||
</tr></table>"""
|
||||
|
||||
def pn_info_string(header: str, name: Optional[str], number: Optional[str]) -> Optional[str]:
|
||||
|
||||
def pn_info_string(
|
||||
header: str, name: Optional[str], number: Optional[str]
|
||||
) -> Optional[str]:
|
||||
"""Return the company name and/or the part number in one single string or None otherwise."""
|
||||
number = str(number).strip() if number is not None else ''
|
||||
number = str(number).strip() if number is not None else ""
|
||||
if name or number:
|
||||
return f'{name if name else header}{": " + number if number else ""}'
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def index_if_list(value: Any, index: int) -> Any:
|
||||
"""Return the value indexed if it is a list, or simply the value otherwise."""
|
||||
return value[index] if isinstance(value, list) else value
|
||||
|
||||
|
||||
def make_list(value: Any) -> list:
|
||||
"""Return value if a list, empty list if None, or single element list otherwise."""
|
||||
return value if isinstance(value, list) else [] if value is None else [value]
|
||||
|
||||
|
||||
def make_str(value: Any) -> str:
|
||||
"""Return comma separated elements if a list, empty string if None, or value as a string otherwise."""
|
||||
return ', '.join(str(element) for element in make_list(value))
|
||||
return ", ".join(str(element) for element in make_list(value))
|
||||
|
||||
@ -6,30 +6,64 @@ from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
if __name__ == "__main__":
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
import wireviz.wireviz as wv
|
||||
from wireviz import APP_NAME, __version__
|
||||
from wireviz.wv_helper import open_file_read
|
||||
|
||||
format_codes = {'c': 'csv', 'g': 'gv', 'h': 'html', 'p': 'png', 'P': 'pdf', 's': 'svg', 't': 'tsv'}
|
||||
format_codes = {
|
||||
"c": "csv",
|
||||
"g": "gv",
|
||||
"h": "html",
|
||||
"p": "png",
|
||||
"P": "pdf",
|
||||
"s": "svg",
|
||||
"t": "tsv",
|
||||
}
|
||||
|
||||
epilog = "The -f or --format option accepts a string containing one or more of the following characters to specify which file types to output:\n"
|
||||
epilog += ", ".join([f"{key} ({value.upper()})" for key, value in format_codes.items()])
|
||||
|
||||
epilog = 'The -f or --format option accepts a string containing one or more of the following characters to specify which file types to output:\n'
|
||||
epilog += ', '.join([f'{key} ({value.upper()})' for key, value in format_codes.items()])
|
||||
|
||||
@click.command(epilog=epilog, no_args_is_help=True)
|
||||
@click.argument('file', nargs=-1)
|
||||
@click.option('-f', '--format', default='hpst', type=str, show_default=True, help='Output formats (see below).')
|
||||
@click.option('-p', '--prepend', default=None, type=Path, help='YAML file to prepend to the input file (optional).')
|
||||
@click.option('-o', '--output-file', default=None, type=Path, help='File name (without extension) to use for output, if different from input file name.')
|
||||
@click.option('-V', '--version', is_flag=True, default=False, help=f'Output {APP_NAME} version and exit.')
|
||||
@click.argument("file", nargs=-1)
|
||||
@click.option(
|
||||
"-f",
|
||||
"--format",
|
||||
default="hpst",
|
||||
type=str,
|
||||
show_default=True,
|
||||
help="Output formats (see below).",
|
||||
)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--prepend",
|
||||
default=None,
|
||||
type=Path,
|
||||
help="YAML file to prepend to the input file (optional).",
|
||||
)
|
||||
@click.option(
|
||||
"-o",
|
||||
"--output-file",
|
||||
default=None,
|
||||
type=Path,
|
||||
help="File name (without extension) to use for output, if different from input file name.",
|
||||
)
|
||||
@click.option(
|
||||
"-V",
|
||||
"--version",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help=f"Output {APP_NAME} version and exit.",
|
||||
)
|
||||
def wireviz(file, format, prepend, output_file, version):
|
||||
"""
|
||||
Parses the provided FILE and generates the specified outputs.
|
||||
"""
|
||||
print()
|
||||
print(f'{APP_NAME} {__version__}')
|
||||
print(f"{APP_NAME} {__version__}")
|
||||
if version:
|
||||
return # print version number only and exit
|
||||
|
||||
@ -47,35 +81,39 @@ def wireviz(file, format, prepend, output_file, version):
|
||||
if code in format_codes:
|
||||
output_formats.append(format_codes[code])
|
||||
else:
|
||||
raise Exception(f'Unknown output format: {code}')
|
||||
raise Exception(f"Unknown output format: {code}")
|
||||
output_formats = tuple(sorted(set(output_formats)))
|
||||
output_formats_str = f'[{"|".join(output_formats)}]' if len(output_formats) > 1 else output_formats[0]
|
||||
output_formats_str = (
|
||||
f'[{"|".join(output_formats)}]'
|
||||
if len(output_formats) > 1
|
||||
else output_formats[0]
|
||||
)
|
||||
|
||||
image_paths = []
|
||||
# check prepend file
|
||||
if prepend:
|
||||
prepend = Path(prepend)
|
||||
if not prepend.exists():
|
||||
raise Exception(f'File does not exist:\n{prepend}')
|
||||
print('Prepend file:', prepend)
|
||||
raise Exception(f"File does not exist:\n{prepend}")
|
||||
print("Prepend file:", prepend)
|
||||
|
||||
with open_file_read(prepend) as file_handle:
|
||||
prepend_input = file_handle.read() + '\n'
|
||||
prepend_input = file_handle.read() + "\n"
|
||||
prepend_dir = prepend.parent
|
||||
else:
|
||||
prepend_input = ''
|
||||
prepend_input = ""
|
||||
prepend_dir = None
|
||||
|
||||
# run WireVIz on each input file
|
||||
for file in filepaths:
|
||||
file = Path(file)
|
||||
if not file.exists():
|
||||
raise Exception(f'File does not exist:\n{file}')
|
||||
raise Exception(f"File does not exist:\n{file}")
|
||||
|
||||
file_out = file.with_suffix('') if not output_file else output_file
|
||||
file_out = file.with_suffix("") if not output_file else output_file
|
||||
|
||||
print('Input file: ', file)
|
||||
print('Output file: ', f'{file_out}.{output_formats_str}')
|
||||
print("Input file: ", file)
|
||||
print("Output file: ", f"{file_out}.{output_formats_str}")
|
||||
|
||||
with open_file_read(file) as file_handle:
|
||||
yaml_input = file_handle.read()
|
||||
@ -83,9 +121,15 @@ def wireviz(file, format, prepend, output_file, version):
|
||||
|
||||
yaml_input = prepend_input + yaml_input
|
||||
|
||||
wv.parse_text(yaml_input, file_out=file_out, output_formats=output_formats, image_paths=[file_dir, prepend_dir])
|
||||
wv.parse_text(
|
||||
yaml_input,
|
||||
file_out=file_out,
|
||||
output_formats=output_formats,
|
||||
image_paths=[file_dir, prepend_dir],
|
||||
)
|
||||
|
||||
print()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
wireviz()
|
||||
|
||||
@ -3,142 +3,294 @@
|
||||
from typing import Dict, List
|
||||
|
||||
COLOR_CODES = {
|
||||
'DIN': ['WH', 'BN', 'GN', 'YE', 'GY', 'PK', 'BU', 'RD', 'BK', 'VT', 'GYPK', 'RDBU', 'WHGN', 'BNGN', 'WHYE', 'YEBN',
|
||||
'WHGY', 'GYBN', 'WHPK', 'PKBN', 'WHBU', 'BNBU', 'WHRD', 'BNRD', 'WHBK', 'BNBK', 'GYGN', 'YEGY', 'PKGN',
|
||||
'YEPK', 'GNBU', 'YEBU', 'GNRD', 'YERD', 'GNBK', 'YEBK', 'GYBU', 'PKBU', 'GYRD', 'PKRD', 'GYBK', 'PKBK',
|
||||
'BUBK', 'RDBK', 'WHBNBK', 'YEGNBK', 'GYPKBK', 'RDBUBK', 'WHGNBK', 'BNGNBK', 'WHYEBK', 'YEBNBK', 'WHGYBK',
|
||||
'GYBNBK', 'WHPKBK', 'PKBNBK', 'WHBUBK', 'BNBUBK', 'WHRDBK', 'BNRDBK'],
|
||||
'IEC': ['BN', 'RD', 'OG', 'YE', 'GN', 'BU', 'VT', 'GY', 'WH', 'BK'],
|
||||
'BW': ['BK', 'WH'],
|
||||
"DIN": [
|
||||
"WH",
|
||||
"BN",
|
||||
"GN",
|
||||
"YE",
|
||||
"GY",
|
||||
"PK",
|
||||
"BU",
|
||||
"RD",
|
||||
"BK",
|
||||
"VT",
|
||||
"GYPK",
|
||||
"RDBU",
|
||||
"WHGN",
|
||||
"BNGN",
|
||||
"WHYE",
|
||||
"YEBN",
|
||||
"WHGY",
|
||||
"GYBN",
|
||||
"WHPK",
|
||||
"PKBN",
|
||||
"WHBU",
|
||||
"BNBU",
|
||||
"WHRD",
|
||||
"BNRD",
|
||||
"WHBK",
|
||||
"BNBK",
|
||||
"GYGN",
|
||||
"YEGY",
|
||||
"PKGN",
|
||||
"YEPK",
|
||||
"GNBU",
|
||||
"YEBU",
|
||||
"GNRD",
|
||||
"YERD",
|
||||
"GNBK",
|
||||
"YEBK",
|
||||
"GYBU",
|
||||
"PKBU",
|
||||
"GYRD",
|
||||
"PKRD",
|
||||
"GYBK",
|
||||
"PKBK",
|
||||
"BUBK",
|
||||
"RDBK",
|
||||
"WHBNBK",
|
||||
"YEGNBK",
|
||||
"GYPKBK",
|
||||
"RDBUBK",
|
||||
"WHGNBK",
|
||||
"BNGNBK",
|
||||
"WHYEBK",
|
||||
"YEBNBK",
|
||||
"WHGYBK",
|
||||
"GYBNBK",
|
||||
"WHPKBK",
|
||||
"PKBNBK",
|
||||
"WHBUBK",
|
||||
"BNBUBK",
|
||||
"WHRDBK",
|
||||
"BNRDBK",
|
||||
],
|
||||
"IEC": ["BN", "RD", "OG", "YE", "GN", "BU", "VT", "GY", "WH", "BK"],
|
||||
"BW": ["BK", "WH"],
|
||||
# 25-pair color code - see also https://en.wikipedia.org/wiki/25-pair_color_code
|
||||
# 5 major colors (WH,RD,BK,YE,VT) combined with 5 minor colors (BU,OG,GN,BN,SL).
|
||||
# Each POTS pair tip (+) had major/minor color, and ring (-) had minor/major color.
|
||||
'TEL': [ # 25x2: Ring and then tip of each pair
|
||||
'BUWH', 'WHBU', 'OGWH', 'WHOG', 'GNWH', 'WHGN', 'BNWH', 'WHBN', 'SLWH', 'WHSL',
|
||||
'BURD', 'RDBU', 'OGRD', 'RDOG', 'GNRD', 'RDGN', 'BNRD', 'RDBN', 'SLRD', 'RDSL',
|
||||
'BUBK', 'BKBU', 'OGBK', 'BKOG', 'GNBK', 'BKGN', 'BNBK', 'BKBN', 'SLBK', 'BKSL',
|
||||
'BUYE', 'YEBU', 'OGYE', 'YEOG', 'GNYE', 'YEGN', 'BNYE', 'YEBN', 'SLYE', 'YESL',
|
||||
'BUVT', 'VTBU', 'OGVT', 'VTOG', 'GNVT', 'VTGN', 'BNVT', 'VTBN', 'SLVT', 'VTSL'],
|
||||
'TELALT': [ # 25x2: Tip and then ring of each pair
|
||||
'WHBU', 'BU', 'WHOG', 'OG', 'WHGN', 'GN', 'WHBN', 'BN', 'WHSL', 'SL',
|
||||
'RDBU', 'BURD', 'RDOG', 'OGRD', 'RDGN', 'GNRD', 'RDBN', 'BNRD', 'RDSL', 'SLRD',
|
||||
'BKBU', 'BUBK', 'BKOG', 'OGBK', 'BKGN', 'GNBK', 'BKBN', 'BNBK', 'BKSL', 'SLBK',
|
||||
'YEBU', 'BUYE', 'YEOG', 'OGYE', 'YEGN', 'GNYE', 'YEBN', 'BNYE', 'YESL', 'SLYE',
|
||||
'VTBU', 'BUVT', 'VTOG', 'OGVT', 'VTGN', 'GNVT', 'VTBN', 'BNVT', 'VTSL', 'SLVT'],
|
||||
'T568A': ['WHGN', 'GN', 'WHOG', 'BU', 'WHBU', 'OG', 'WHBN', 'BN'],
|
||||
'T568B': ['WHOG', 'OG', 'WHGN', 'BU', 'WHBU', 'GN', 'WHBN', 'BN'],
|
||||
"TEL": [ # 25x2: Ring and then tip of each pair
|
||||
"BUWH",
|
||||
"WHBU",
|
||||
"OGWH",
|
||||
"WHOG",
|
||||
"GNWH",
|
||||
"WHGN",
|
||||
"BNWH",
|
||||
"WHBN",
|
||||
"SLWH",
|
||||
"WHSL",
|
||||
"BURD",
|
||||
"RDBU",
|
||||
"OGRD",
|
||||
"RDOG",
|
||||
"GNRD",
|
||||
"RDGN",
|
||||
"BNRD",
|
||||
"RDBN",
|
||||
"SLRD",
|
||||
"RDSL",
|
||||
"BUBK",
|
||||
"BKBU",
|
||||
"OGBK",
|
||||
"BKOG",
|
||||
"GNBK",
|
||||
"BKGN",
|
||||
"BNBK",
|
||||
"BKBN",
|
||||
"SLBK",
|
||||
"BKSL",
|
||||
"BUYE",
|
||||
"YEBU",
|
||||
"OGYE",
|
||||
"YEOG",
|
||||
"GNYE",
|
||||
"YEGN",
|
||||
"BNYE",
|
||||
"YEBN",
|
||||
"SLYE",
|
||||
"YESL",
|
||||
"BUVT",
|
||||
"VTBU",
|
||||
"OGVT",
|
||||
"VTOG",
|
||||
"GNVT",
|
||||
"VTGN",
|
||||
"BNVT",
|
||||
"VTBN",
|
||||
"SLVT",
|
||||
"VTSL",
|
||||
],
|
||||
"TELALT": [ # 25x2: Tip and then ring of each pair
|
||||
"WHBU",
|
||||
"BU",
|
||||
"WHOG",
|
||||
"OG",
|
||||
"WHGN",
|
||||
"GN",
|
||||
"WHBN",
|
||||
"BN",
|
||||
"WHSL",
|
||||
"SL",
|
||||
"RDBU",
|
||||
"BURD",
|
||||
"RDOG",
|
||||
"OGRD",
|
||||
"RDGN",
|
||||
"GNRD",
|
||||
"RDBN",
|
||||
"BNRD",
|
||||
"RDSL",
|
||||
"SLRD",
|
||||
"BKBU",
|
||||
"BUBK",
|
||||
"BKOG",
|
||||
"OGBK",
|
||||
"BKGN",
|
||||
"GNBK",
|
||||
"BKBN",
|
||||
"BNBK",
|
||||
"BKSL",
|
||||
"SLBK",
|
||||
"YEBU",
|
||||
"BUYE",
|
||||
"YEOG",
|
||||
"OGYE",
|
||||
"YEGN",
|
||||
"GNYE",
|
||||
"YEBN",
|
||||
"BNYE",
|
||||
"YESL",
|
||||
"SLYE",
|
||||
"VTBU",
|
||||
"BUVT",
|
||||
"VTOG",
|
||||
"OGVT",
|
||||
"VTGN",
|
||||
"GNVT",
|
||||
"VTBN",
|
||||
"BNVT",
|
||||
"VTSL",
|
||||
"SLVT",
|
||||
],
|
||||
"T568A": ["WHGN", "GN", "WHOG", "BU", "WHBU", "OG", "WHBN", "BN"],
|
||||
"T568B": ["WHOG", "OG", "WHGN", "BU", "WHBU", "GN", "WHBN", "BN"],
|
||||
}
|
||||
|
||||
# Convention: Color names should be 2 letters long, to allow for multicolored wires
|
||||
|
||||
_color_hex = {
|
||||
'BK': '#000000',
|
||||
'WH': '#ffffff',
|
||||
'GY': '#999999',
|
||||
'PK': '#ff66cc',
|
||||
'RD': '#ff0000',
|
||||
'OG': '#ff8000',
|
||||
'YE': '#ffff00',
|
||||
'OL': '#708000', # olive green
|
||||
'GN': '#00ff00',
|
||||
'TQ': '#00ffff',
|
||||
'LB': '#a0dfff', # light blue
|
||||
'BU': '#0066ff',
|
||||
'VT': '#8000ff',
|
||||
'BN': '#895956',
|
||||
'BG': '#ceb673', # beige
|
||||
'IV': '#f5f0d0', # ivory
|
||||
'SL': '#708090',
|
||||
'CU': '#d6775e', # Faux-copper look, for bare CU wire
|
||||
'SN': '#aaaaaa', # Silvery look for tinned bare wire
|
||||
'SR': '#84878c', # Darker silver for silvered wire
|
||||
'GD': '#ffcf80', # Golden color for gold
|
||||
"BK": "#000000",
|
||||
"WH": "#ffffff",
|
||||
"GY": "#999999",
|
||||
"PK": "#ff66cc",
|
||||
"RD": "#ff0000",
|
||||
"OG": "#ff8000",
|
||||
"YE": "#ffff00",
|
||||
"OL": "#708000", # olive green
|
||||
"GN": "#00ff00",
|
||||
"TQ": "#00ffff",
|
||||
"LB": "#a0dfff", # light blue
|
||||
"BU": "#0066ff",
|
||||
"VT": "#8000ff",
|
||||
"BN": "#895956",
|
||||
"BG": "#ceb673", # beige
|
||||
"IV": "#f5f0d0", # ivory
|
||||
"SL": "#708090",
|
||||
"CU": "#d6775e", # Faux-copper look, for bare CU wire
|
||||
"SN": "#aaaaaa", # Silvery look for tinned bare wire
|
||||
"SR": "#84878c", # Darker silver for silvered wire
|
||||
"GD": "#ffcf80", # Golden color for gold
|
||||
}
|
||||
|
||||
_color_full = {
|
||||
'BK': 'black',
|
||||
'WH': 'white',
|
||||
'GY': 'grey',
|
||||
'PK': 'pink',
|
||||
'RD': 'red',
|
||||
'OG': 'orange',
|
||||
'YE': 'yellow',
|
||||
'OL': 'olive green',
|
||||
'GN': 'green',
|
||||
'TQ': 'turquoise',
|
||||
'LB': 'light blue',
|
||||
'BU': 'blue',
|
||||
'VT': 'violet',
|
||||
'BN': 'brown',
|
||||
'BG': 'beige',
|
||||
'IV': 'ivory',
|
||||
'SL': 'slate',
|
||||
'CU': 'copper',
|
||||
'SN': 'tin',
|
||||
'SR': 'silver',
|
||||
'GD': 'gold',
|
||||
"BK": "black",
|
||||
"WH": "white",
|
||||
"GY": "grey",
|
||||
"PK": "pink",
|
||||
"RD": "red",
|
||||
"OG": "orange",
|
||||
"YE": "yellow",
|
||||
"OL": "olive green",
|
||||
"GN": "green",
|
||||
"TQ": "turquoise",
|
||||
"LB": "light blue",
|
||||
"BU": "blue",
|
||||
"VT": "violet",
|
||||
"BN": "brown",
|
||||
"BG": "beige",
|
||||
"IV": "ivory",
|
||||
"SL": "slate",
|
||||
"CU": "copper",
|
||||
"SN": "tin",
|
||||
"SR": "silver",
|
||||
"GD": "gold",
|
||||
}
|
||||
|
||||
_color_ger = {
|
||||
'BK': 'sw',
|
||||
'WH': 'ws',
|
||||
'GY': 'gr',
|
||||
'PK': 'rs',
|
||||
'RD': 'rt',
|
||||
'OG': 'or',
|
||||
'YE': 'ge',
|
||||
'OL': 'ol', # olivgrün
|
||||
'GN': 'gn',
|
||||
'TQ': 'tk',
|
||||
'LB': 'hb', # hellblau
|
||||
'BU': 'bl',
|
||||
'VT': 'vi',
|
||||
'BN': 'br',
|
||||
'BG': 'bg', # beige
|
||||
'IV': 'eb', # elfenbeinfarben
|
||||
'SL': 'si', # Schiefer
|
||||
'CU': 'ku', # Kupfer
|
||||
'SN': 'vz', # verzinkt
|
||||
'SR': 'ag', # Silber
|
||||
'GD': 'au', # Gold
|
||||
"BK": "sw",
|
||||
"WH": "ws",
|
||||
"GY": "gr",
|
||||
"PK": "rs",
|
||||
"RD": "rt",
|
||||
"OG": "or",
|
||||
"YE": "ge",
|
||||
"OL": "ol", # olivgrün
|
||||
"GN": "gn",
|
||||
"TQ": "tk",
|
||||
"LB": "hb", # hellblau
|
||||
"BU": "bl",
|
||||
"VT": "vi",
|
||||
"BN": "br",
|
||||
"BG": "bg", # beige
|
||||
"IV": "eb", # elfenbeinfarben
|
||||
"SL": "si", # Schiefer
|
||||
"CU": "ku", # Kupfer
|
||||
"SN": "vz", # verzinkt
|
||||
"SR": "ag", # Silber
|
||||
"GD": "au", # Gold
|
||||
}
|
||||
|
||||
|
||||
color_default = '#ffffff'
|
||||
color_default = "#ffffff"
|
||||
|
||||
_hex_digits = set('0123456789abcdefABCDEF')
|
||||
_hex_digits = set("0123456789abcdefABCDEF")
|
||||
|
||||
|
||||
# Literal type aliases below are commented to avoid requiring python 3.8
|
||||
Color = str # Two-letter color name = Literal[_color_hex.keys()]
|
||||
Colors = str # One or more two-letter color names (Color) concatenated into one string
|
||||
ColorMode = str # = Literal['full', 'FULL', 'hex', 'HEX', 'short', 'SHORT', 'ger', 'GER']
|
||||
ColorMode = (
|
||||
str # = Literal['full', 'FULL', 'hex', 'HEX', 'short', 'SHORT', 'ger', 'GER']
|
||||
)
|
||||
ColorScheme = str # Color scheme name = Literal[COLOR_CODES.keys()]
|
||||
|
||||
|
||||
def get_color_hex(input: Colors, pad: bool = False) -> List[str]:
|
||||
"""Return list of hex colors from either a string of color names or :-separated hex colors."""
|
||||
if input is None or input == '':
|
||||
if input is None or input == "":
|
||||
return [color_default]
|
||||
elif input[0] == '#': # Hex color(s)
|
||||
output = input.split(':')
|
||||
elif input[0] == "#": # Hex color(s)
|
||||
output = input.split(":")
|
||||
for i, c in enumerate(output):
|
||||
if c[0] != '#' or not all(d in _hex_digits for d in c[1:]):
|
||||
if c[0] != "#" or not all(d in _hex_digits for d in c[1:]):
|
||||
if c != input:
|
||||
c += f' in input: {input}'
|
||||
print(f'Invalid hex color: {c}')
|
||||
c += f" in input: {input}"
|
||||
print(f"Invalid hex color: {c}")
|
||||
output[i] = color_default
|
||||
else: # Color name(s)
|
||||
|
||||
def lookup(c: str) -> str:
|
||||
try:
|
||||
return _color_hex[c]
|
||||
except KeyError:
|
||||
if c != input:
|
||||
c += f' in input: {input}'
|
||||
print(f'Unknown color name: {c}')
|
||||
c += f" in input: {input}"
|
||||
print(f"Unknown color name: {c}")
|
||||
return color_default
|
||||
|
||||
output = [lookup(input[i:i + 2]) for i in range(0, len(input), 2)]
|
||||
output = [lookup(input[i : i + 2]) for i in range(0, len(input), 2)]
|
||||
|
||||
if len(output) == 2: # Give wires with EXACTLY 2 colors that striped look.
|
||||
output += output[:1]
|
||||
@ -150,34 +302,38 @@ def get_color_hex(input: Colors, pad: bool = False) -> List[str]:
|
||||
|
||||
def get_color_translation(translate: Dict[Color, str], input: Colors) -> List[str]:
|
||||
"""Return list of colors translations from either a string of color names or :-separated hex colors."""
|
||||
|
||||
def from_hex(hex_input: str) -> str:
|
||||
for color, hex in _color_hex.items():
|
||||
if hex == hex_input:
|
||||
return translate[color]
|
||||
return f'({",".join(str(int(hex_input[i:i+2], 16)) for i in range(1, 6, 2))})'
|
||||
|
||||
return [from_hex(h) for h in input.lower().split(':')] if input[0] == '#' else \
|
||||
[translate.get(input[i:i+2], '??') for i in range(0, len(input), 2)]
|
||||
return (
|
||||
[from_hex(h) for h in input.lower().split(":")]
|
||||
if input[0] == "#"
|
||||
else [translate.get(input[i : i + 2], "??") for i in range(0, len(input), 2)]
|
||||
)
|
||||
|
||||
|
||||
def translate_color(input: Colors, color_mode: ColorMode) -> str:
|
||||
if input == '' or input is None:
|
||||
return ''
|
||||
if input == "" or input is None:
|
||||
return ""
|
||||
upper = color_mode.isupper()
|
||||
if not (color_mode.isupper() or color_mode.islower()):
|
||||
raise Exception('Unknown color mode capitalization')
|
||||
raise Exception("Unknown color mode capitalization")
|
||||
|
||||
color_mode = color_mode.lower()
|
||||
if color_mode == 'full':
|
||||
if color_mode == "full":
|
||||
output = "/".join(get_color_translation(_color_full, input))
|
||||
elif color_mode == 'hex':
|
||||
output = ':'.join(get_color_hex(input, pad=False))
|
||||
elif color_mode == 'ger':
|
||||
elif color_mode == "hex":
|
||||
output = ":".join(get_color_hex(input, pad=False))
|
||||
elif color_mode == "ger":
|
||||
output = "".join(get_color_translation(_color_ger, input))
|
||||
elif color_mode == 'short':
|
||||
elif color_mode == "short":
|
||||
output = input
|
||||
else:
|
||||
raise Exception('Unknown color mode')
|
||||
raise Exception("Unknown color mode")
|
||||
if upper:
|
||||
return output.upper()
|
||||
else:
|
||||
|
||||
@ -8,51 +8,66 @@ from wireviz.wv_colors import translate_color
|
||||
from wireviz.wv_helper import remove_links
|
||||
|
||||
|
||||
def nested_html_table(rows: List[Union[str, List[Optional[str]], None]], table_attrs: str = '') -> str:
|
||||
def nested_html_table(
|
||||
rows: List[Union[str, List[Optional[str]], None]], table_attrs: str = ""
|
||||
) -> str:
|
||||
# input: list, each item may be scalar or list
|
||||
# output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar
|
||||
# purpose: create the appearance of one table, where cell widths are independent between rows
|
||||
# attributes in any leading <tdX> inside a list are injected into to the preceeding <td> tag
|
||||
html = []
|
||||
html.append(f'<table border="0" cellspacing="0" cellpadding="0"{table_attrs or ""}>')
|
||||
html.append(
|
||||
f'<table border="0" cellspacing="0" cellpadding="0"{table_attrs or ""}>'
|
||||
)
|
||||
|
||||
num_rows = 0
|
||||
for row in rows:
|
||||
if isinstance(row, List):
|
||||
if len(row) > 0 and any(row):
|
||||
html.append(' <tr><td>')
|
||||
html.append(' <table border="0" cellspacing="0" cellpadding="3" cellborder="1"><tr>')
|
||||
html.append(" <tr><td>")
|
||||
html.append(
|
||||
' <table border="0" cellspacing="0" cellpadding="3" cellborder="1"><tr>'
|
||||
)
|
||||
for cell in row:
|
||||
if cell is not None:
|
||||
# Inject attributes to the preceeding <td> tag where needed
|
||||
html.append(f' <td balign="left">{cell}</td>'.replace('><tdX', ''))
|
||||
html.append(' </tr></table>')
|
||||
html.append(' </td></tr>')
|
||||
html.append(
|
||||
f' <td balign="left">{cell}</td>'.replace("><tdX", "")
|
||||
)
|
||||
html.append(" </tr></table>")
|
||||
html.append(" </td></tr>")
|
||||
num_rows = num_rows + 1
|
||||
elif row is not None:
|
||||
html.append(' <tr><td>')
|
||||
html.append(f' {row}')
|
||||
html.append(' </td></tr>')
|
||||
html.append(" <tr><td>")
|
||||
html.append(f" {row}")
|
||||
html.append(" </td></tr>")
|
||||
num_rows = num_rows + 1
|
||||
if num_rows == 0: # empty table
|
||||
html.append('<tr><td></td></tr>') # generate empty cell to avoid GraphViz errors
|
||||
html.append('</table>')
|
||||
html.append(
|
||||
"<tr><td></td></tr>"
|
||||
) # generate empty cell to avoid GraphViz errors
|
||||
html.append("</table>")
|
||||
return html
|
||||
|
||||
|
||||
def html_bgcolor_attr(color: Color) -> str:
|
||||
"""Return attributes for bgcolor or '' if no color."""
|
||||
return f' bgcolor="{translate_color(color, "HEX")}"' if color else ''
|
||||
return f' bgcolor="{translate_color(color, "HEX")}"' if color else ""
|
||||
|
||||
def html_bgcolor(color: Color, _extra_attr: str = '') -> str:
|
||||
|
||||
def html_bgcolor(color: Color, _extra_attr: str = "") -> str:
|
||||
"""Return <td> attributes prefix for bgcolor or '' if no color."""
|
||||
return f'<tdX{html_bgcolor_attr(color)}{_extra_attr}>' if color else ''
|
||||
return f"<tdX{html_bgcolor_attr(color)}{_extra_attr}>" if color else ""
|
||||
|
||||
|
||||
def html_colorbar(color: Color) -> str:
|
||||
"""Return <tdX> attributes prefix for bgcolor and minimum width or None if no color."""
|
||||
return html_bgcolor(color, ' width="4"') if color else None
|
||||
|
||||
|
||||
def html_image(image):
|
||||
from wireviz.DataClasses import Image
|
||||
|
||||
if not image:
|
||||
return None
|
||||
# The leading attributes belong to the preceeding tag. See where used below.
|
||||
@ -60,25 +75,38 @@ def html_image(image):
|
||||
if image.fixedsize:
|
||||
# Close the preceeding tag and enclose the image cell in a table without
|
||||
# borders to avoid narrow borders when the fixed width < the node width.
|
||||
html = f'''>
|
||||
html = f""">
|
||||
<table border="0" cellspacing="0" cellborder="0"><tr>
|
||||
<td{html}</td>
|
||||
</tr></table>
|
||||
'''
|
||||
return f'''<tdX{' sides="TLR"' if image.caption else ''}{html_bgcolor_attr(image.bgcolor)}{html}'''
|
||||
"""
|
||||
return f"""<tdX{' sides="TLR"' if image.caption else ''}{html_bgcolor_attr(image.bgcolor)}{html}"""
|
||||
|
||||
|
||||
def html_caption(image):
|
||||
from wireviz.DataClasses import Image
|
||||
return (f'<tdX sides="BLR"{html_bgcolor_attr(image.bgcolor)}>{html_line_breaks(image.caption)}'
|
||||
if image and image.caption else None)
|
||||
|
||||
return (
|
||||
f'<tdX sides="BLR"{html_bgcolor_attr(image.bgcolor)}>{html_line_breaks(image.caption)}'
|
||||
if image and image.caption
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
def html_size_attr(image):
|
||||
from wireviz.DataClasses import Image
|
||||
|
||||
# Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object
|
||||
return ((f' width="{image.width}"' if image.width else '')
|
||||
+ (f' height="{image.height}"' if image.height else '')
|
||||
+ ( ' fixedsize="true"' if image.fixedsize else '')) if image else ''
|
||||
return (
|
||||
(
|
||||
(f' width="{image.width}"' if image.width else "")
|
||||
+ (f' height="{image.height}"' if image.height else "")
|
||||
+ (' fixedsize="true"' if image.fixedsize else "")
|
||||
)
|
||||
if image
|
||||
else ""
|
||||
)
|
||||
|
||||
|
||||
def html_line_breaks(inp):
|
||||
return remove_links(inp).replace('\n', '<br />') if isinstance(inp, str) else inp
|
||||
return remove_links(inp).replace("\n", "<br />") if isinstance(inp, str) else inp
|
||||
|
||||
@ -5,31 +5,33 @@ from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
awg_equiv_table = {
|
||||
'0.09': '28',
|
||||
'0.14': '26',
|
||||
'0.25': '24',
|
||||
'0.34': '22',
|
||||
'0.5': '21',
|
||||
'0.75': '20',
|
||||
'1': '18',
|
||||
'1.5': '16',
|
||||
'2.5': '14',
|
||||
'4': '12',
|
||||
'6': '10',
|
||||
'10': '8',
|
||||
'16': '6',
|
||||
'25': '4',
|
||||
'35': '2',
|
||||
'50': '1',
|
||||
"0.09": "28",
|
||||
"0.14": "26",
|
||||
"0.25": "24",
|
||||
"0.34": "22",
|
||||
"0.5": "21",
|
||||
"0.75": "20",
|
||||
"1": "18",
|
||||
"1.5": "16",
|
||||
"2.5": "14",
|
||||
"4": "12",
|
||||
"6": "10",
|
||||
"10": "8",
|
||||
"16": "6",
|
||||
"25": "4",
|
||||
"35": "2",
|
||||
"50": "1",
|
||||
}
|
||||
|
||||
mm2_equiv_table = {v:k for k,v in awg_equiv_table.items()}
|
||||
mm2_equiv_table = {v: k for k, v in awg_equiv_table.items()}
|
||||
|
||||
|
||||
def awg_equiv(mm2):
|
||||
return awg_equiv_table.get(str(mm2), 'Unknown')
|
||||
return awg_equiv_table.get(str(mm2), "Unknown")
|
||||
|
||||
|
||||
def mm2_equiv(awg):
|
||||
return mm2_equiv_table.get(str(awg), 'Unknown')
|
||||
return mm2_equiv_table.get(str(awg), "Unknown")
|
||||
|
||||
|
||||
def expand(yaml_data):
|
||||
@ -42,8 +44,8 @@ def expand(yaml_data):
|
||||
yaml_data = [yaml_data]
|
||||
for e in yaml_data:
|
||||
e = str(e)
|
||||
if '-' in e:
|
||||
a, b = e.split('-', 1)
|
||||
if "-" in e:
|
||||
a, b = e.split("-", 1)
|
||||
try:
|
||||
a = int(a)
|
||||
b = int(b)
|
||||
@ -56,7 +58,9 @@ def expand(yaml_data):
|
||||
else: # a == b
|
||||
output.append(a) # range of length 1
|
||||
except:
|
||||
output.append(e) # '-' was not a delimiter between two ints, pass e through unchanged
|
||||
output.append(
|
||||
e
|
||||
) # '-' was not a delimiter between two ints, pass e through unchanged
|
||||
else:
|
||||
try:
|
||||
x = int(e) # single int
|
||||
@ -81,36 +85,46 @@ def int2tuple(inp):
|
||||
|
||||
|
||||
def flatten2d(inp):
|
||||
return [[str(item) if not isinstance(item, List) else ', '.join(item) for item in row] for row in inp]
|
||||
return [
|
||||
[str(item) if not isinstance(item, List) else ", ".join(item) for item in row]
|
||||
for row in inp
|
||||
]
|
||||
|
||||
|
||||
def tuplelist2tsv(inp, header=None):
|
||||
output = ''
|
||||
output = ""
|
||||
if header is not None:
|
||||
inp.insert(0, header)
|
||||
inp = flatten2d(inp)
|
||||
for row in inp:
|
||||
output = output + '\t'.join(str(remove_links(item)) for item in row) + '\n'
|
||||
output = output + "\t".join(str(remove_links(item)) for item in row) + "\n"
|
||||
return output
|
||||
|
||||
|
||||
def remove_links(inp):
|
||||
return re.sub(r'<[aA] [^>]*>([^<]*)</[aA]>', r'\1', inp) if isinstance(inp, str) else inp
|
||||
return (
|
||||
re.sub(r"<[aA] [^>]*>([^<]*)</[aA]>", r"\1", inp)
|
||||
if isinstance(inp, str)
|
||||
else inp
|
||||
)
|
||||
|
||||
|
||||
def clean_whitespace(inp):
|
||||
return ' '.join(inp.split()).replace(' ,', ',') if isinstance(inp, str) else inp
|
||||
return " ".join(inp.split()).replace(" ,", ",") if isinstance(inp, str) else inp
|
||||
|
||||
|
||||
def open_file_read(filename):
|
||||
# TODO: Intelligently determine encoding
|
||||
return open(filename, 'r', encoding='UTF-8')
|
||||
return open(filename, "r", encoding="UTF-8")
|
||||
|
||||
|
||||
def open_file_write(filename):
|
||||
return open(filename, 'w', encoding='UTF-8')
|
||||
return open(filename, "w", encoding="UTF-8")
|
||||
|
||||
|
||||
def open_file_append(filename):
|
||||
return open(filename, 'a', encoding='UTF-8')
|
||||
return open(filename, "a", encoding="UTF-8")
|
||||
|
||||
|
||||
def is_arrow(inp):
|
||||
"""
|
||||
@ -122,18 +136,22 @@ def is_arrow(inp):
|
||||
<==, ==, ==>, <=>
|
||||
"""
|
||||
# regex by @shiraneyo
|
||||
return bool(re.match(r"^\s*(?P<leftHead><?)(?P<body>-+|=+)(?P<rightHead>>?)\s*$", inp))
|
||||
return bool(
|
||||
re.match(r"^\s*(?P<leftHead><?)(?P<body>-+|=+)(?P<rightHead>>?)\s*$", inp)
|
||||
)
|
||||
|
||||
|
||||
def aspect_ratio(image_src):
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
image = Image.open(image_src)
|
||||
if image.width > 0 and image.height > 0:
|
||||
return image.width / image.height
|
||||
print(f'aspect_ratio(): Invalid image size {image.width} x {image.height}')
|
||||
print(f"aspect_ratio(): Invalid image size {image.width} x {image.height}")
|
||||
# ModuleNotFoundError and FileNotFoundError are the most expected, but all are handled equally.
|
||||
except Exception as error:
|
||||
print(f'aspect_ratio(): {type(error).__name__}: {error}')
|
||||
print(f"aspect_ratio(): {type(error).__name__}: {error}")
|
||||
return 1 # Assume 1:1 when unable to read actual image size
|
||||
|
||||
|
||||
@ -145,13 +163,17 @@ def smart_file_resolve(filename: str, possible_paths: (str, List[str])) -> Path:
|
||||
if filename.exists():
|
||||
return filename
|
||||
else:
|
||||
raise Exception(f'{filename} does not exist.')
|
||||
raise Exception(f"{filename} does not exist.")
|
||||
else: # search all possible paths in decreasing order of precedence
|
||||
possible_paths = [Path(path).resolve() for path in possible_paths if path is not None]
|
||||
possible_paths = [
|
||||
Path(path).resolve() for path in possible_paths if path is not None
|
||||
]
|
||||
for possible_path in possible_paths:
|
||||
resolved_path = (possible_path / filename).resolve()
|
||||
if resolved_path.exists():
|
||||
return resolved_path
|
||||
else:
|
||||
raise Exception(f'{filename} was not found in any of the following locations: \n' +
|
||||
'\n'.join([str(x) for x in possible_paths]))
|
||||
raise Exception(
|
||||
f"{filename} was not found in any of the following locations: \n"
|
||||
+ "\n".join([str(x) for x in possible_paths])
|
||||
)
|
||||
|
||||
@ -15,83 +15,106 @@ from wireviz.wv_helper import (
|
||||
)
|
||||
|
||||
|
||||
def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]], metadata: Metadata, options: Options):
|
||||
def generate_html_output(
|
||||
filename: Union[str, Path],
|
||||
bom_list: List[List[str]],
|
||||
metadata: Metadata,
|
||||
options: Options,
|
||||
):
|
||||
|
||||
# load HTML template
|
||||
templatename = metadata.get('template',{}).get('name')
|
||||
templatename = metadata.get("template", {}).get("name")
|
||||
if templatename:
|
||||
# if relative path to template was provided, check directory of YAML file first, fall back to built-in template directory
|
||||
templatefile = smart_file_resolve(f'{templatename}.html', [Path(filename).parent, Path(__file__).parent / 'templates'])
|
||||
templatefile = smart_file_resolve(
|
||||
f"{templatename}.html",
|
||||
[Path(filename).parent, Path(__file__).parent / "templates"],
|
||||
)
|
||||
else:
|
||||
# fall back to built-in simple template if no template was provided
|
||||
templatefile = Path(__file__).parent / 'templates/simple.html'
|
||||
templatefile = Path(__file__).parent / "templates/simple.html"
|
||||
|
||||
with open_file_read(templatefile) as file:
|
||||
html = file.read()
|
||||
|
||||
# embed SVG diagram
|
||||
with open_file_read(f'{filename}.svg') as file:
|
||||
with open_file_read(f"{filename}.svg") as file:
|
||||
svgdata = re.sub(
|
||||
'^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>',
|
||||
'<!-- XML and DOCTYPE declarations from SVG file removed -->',
|
||||
file.read(), 1)
|
||||
"^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>",
|
||||
"<!-- XML and DOCTYPE declarations from SVG file removed -->",
|
||||
file.read(),
|
||||
1,
|
||||
)
|
||||
|
||||
# generate BOM table
|
||||
bom = flatten2d(bom_list)
|
||||
|
||||
# generate BOM header (may be at the top or bottom of the table)
|
||||
bom_header_html = ' <tr>\n'
|
||||
bom_header_html = " <tr>\n"
|
||||
for item in bom[0]:
|
||||
th_class = f'bom_col_{item.lower()}'
|
||||
th_class = f"bom_col_{item.lower()}"
|
||||
bom_header_html = f'{bom_header_html} <th class="{th_class}">{item}</th>\n'
|
||||
bom_header_html = f'{bom_header_html} </tr>\n'
|
||||
bom_header_html = f"{bom_header_html} </tr>\n"
|
||||
|
||||
# generate BOM contents
|
||||
bom_contents = []
|
||||
for row in bom[1:]:
|
||||
row_html = ' <tr>\n'
|
||||
row_html = " <tr>\n"
|
||||
for i, item in enumerate(row):
|
||||
td_class = f'bom_col_{bom[0][i].lower()}'
|
||||
td_class = f"bom_col_{bom[0][i].lower()}"
|
||||
row_html = f'{row_html} <td class="{td_class}">{item}</td>\n'
|
||||
row_html = f'{row_html} </tr>\n'
|
||||
row_html = f"{row_html} </tr>\n"
|
||||
bom_contents.append(row_html)
|
||||
|
||||
bom_html = '<table class="bom">\n' + bom_header_html + ''.join(bom_contents) + '</table>\n'
|
||||
bom_html_reversed = '<table class="bom">\n' + ''.join(list(reversed(bom_contents))) + bom_header_html + '</table>\n'
|
||||
bom_html = (
|
||||
'<table class="bom">\n' + bom_header_html + "".join(bom_contents) + "</table>\n"
|
||||
)
|
||||
bom_html_reversed = (
|
||||
'<table class="bom">\n'
|
||||
+ "".join(list(reversed(bom_contents)))
|
||||
+ bom_header_html
|
||||
+ "</table>\n"
|
||||
)
|
||||
|
||||
# prepare simple replacements
|
||||
replacements = {
|
||||
'<!-- %generator% -->': f'{APP_NAME} {__version__} - {APP_URL}',
|
||||
'<!-- %fontname% -->': options.fontname,
|
||||
'<!-- %bgcolor% -->': wv_colors.translate_color(options.bgcolor, "hex"),
|
||||
'<!-- %diagram% -->': svgdata,
|
||||
'<!-- %bom% -->': bom_html,
|
||||
'<!-- %bom_reversed% -->': bom_html_reversed,
|
||||
'<!-- %sheet_current% -->': '1', # TODO: handle multi-page documents
|
||||
'<!-- %sheet_total% -->': '1', # TODO: handle multi-page documents
|
||||
"<!-- %generator% -->": f"{APP_NAME} {__version__} - {APP_URL}",
|
||||
"<!-- %fontname% -->": options.fontname,
|
||||
"<!-- %bgcolor% -->": wv_colors.translate_color(options.bgcolor, "hex"),
|
||||
"<!-- %diagram% -->": svgdata,
|
||||
"<!-- %bom% -->": bom_html,
|
||||
"<!-- %bom_reversed% -->": bom_html_reversed,
|
||||
"<!-- %sheet_current% -->": "1", # TODO: handle multi-page documents
|
||||
"<!-- %sheet_total% -->": "1", # TODO: handle multi-page documents
|
||||
}
|
||||
|
||||
# prepare metadata replacements
|
||||
if metadata:
|
||||
for item, contents in metadata.items():
|
||||
if isinstance(contents, (str, int, float)):
|
||||
replacements[f'<!-- %{item}% -->'] = html_line_breaks(str(contents))
|
||||
replacements[f"<!-- %{item}% -->"] = html_line_breaks(str(contents))
|
||||
elif isinstance(contents, Dict): # useful for authors, revisions
|
||||
for index, (category, entry) in enumerate(contents.items()):
|
||||
if isinstance(entry, Dict):
|
||||
replacements[f'<!-- %{item}_{index+1}% -->'] = str(category)
|
||||
replacements[f"<!-- %{item}_{index+1}% -->"] = str(category)
|
||||
for entry_key, entry_value in entry.items():
|
||||
replacements[f'<!-- %{item}_{index+1}_{entry_key}% -->'] = html_line_breaks(str(entry_value))
|
||||
replacements[
|
||||
f"<!-- %{item}_{index+1}_{entry_key}% -->"
|
||||
] = html_line_breaks(str(entry_value))
|
||||
|
||||
replacements['"sheetsize_default"'] = '"{}"'.format(metadata.get('template',{}).get('sheetsize', '')) # include quotes so no replacement happens within <style> definition
|
||||
replacements['"sheetsize_default"'] = '"{}"'.format(
|
||||
metadata.get("template", {}).get("sheetsize", "")
|
||||
) # include quotes so no replacement happens within <style> definition
|
||||
|
||||
# perform replacements
|
||||
# regex replacement adapted from:
|
||||
# https://gist.github.com/bgusach/a967e0587d6e01e889fd1d776c5f3729
|
||||
replacements_sorted = sorted(replacements, key=len, reverse=True) # longer replacements first, just in case
|
||||
replacements_sorted = sorted(
|
||||
replacements, key=len, reverse=True
|
||||
) # longer replacements first, just in case
|
||||
replacements_escaped = map(re.escape, replacements_sorted)
|
||||
pattern = re.compile("|".join(replacements_escaped))
|
||||
html = pattern.sub(lambda match: replacements[match.group(0)], html)
|
||||
|
||||
with open_file_write(f'{filename}.html') as file:
|
||||
with open_file_write(f"{filename}.html") as file:
|
||||
file.write(html)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user