Initialize template action

This commit is contained in:
Shrikanth Upadhayaya 2024-09-11 15:27:31 -04:00
commit 47a2a1419b
No known key found for this signature in database
6 changed files with 467 additions and 0 deletions

8
Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM python:3.11-bookworm
COPY requirements.txt /requirements.txt
COPY generate_wireviz_template.py /generate_wireviz_template.py
RUN pip install -r /requirements.txt
ENTRYPOINT [ "/generate_wireviz_template.py" ]

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 AllSpice
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

66
README.md Normal file
View File

@ -0,0 +1,66 @@
# Generate WireViz Template for a Multiboard Assembly
Generate a WireViz template YAML file for a multiboard assembly using
[AllSpice Actions](https://learn.allspice.io/docs/actions-cicd).
The template file includes all the connectors and their pins, but does NOT
include any wires or connections.
## Usage
Add the following step to your actions:
```yaml
- name: Generate WireViz Template
uses: https://hub.allspice.io/Actions/generate-wireviz-template@v0.1
with:
# The path to the assembly YAML file
assembly_file_path: path/to/assembly.yaml
# The variant of the assembly to generate the template for (optional)
variant: ""
# The name of the output file (optional)
output_file: wireviz.yaml
```
### Important Notes
- This action generates a WireViz template YAML file based on a multiboard
assembly configuration. You can then edit the template to set up the wiring
connections between the boards, and use the wireviz tool to generate a
visualization.
- You do not need to check out the repository for this action to work.
### Customizing the Template Generation
1. `assembly_file_path`: Path to the assembly YAML file. If not specified,
"assembly.yaml" at the root of the repository is used.
2. `variant`: Specific variant of the assembly to generate the template for. If
not specified, the default variant is used.
3. `output_file`: Name of the output file for the generated template. If not
specified, it will be saved as "wireviz.yaml" in the root.
Example with custom inputs:
```yaml
- name: Generate WireViz Template
uses: https://hub.allspice.io/Actions/generate-wireviz-template@v0.1
with:
assembly_file_path: configs/my_assembly.yaml
variant: Double
output_file: my_wireviz_template.yaml
```
### Debugging
If you encounter any issues or need more detailed information about the
action's execution, you can set the `log_level` input to 'DEBUG' for more
verbose logging:
```yaml
- name: Generate WireViz Template
uses: https://hub.allspice.io/Actions/generate-wireviz-template@v0.1
with:
assembly_file_path: path/to/assembly.yaml
log_level: DEBUG
```
For more information about AllSpice Actions and how to use them in your workflows, please refer to the [AllSpice Documentation](https://learn.allspice.io/docs/actions-cicd).

45
action.yml Normal file
View File

@ -0,0 +1,45 @@
name: "Generate WireViz template for a Multiboard Assembly"
description: >
Generate a WireViz template YAML file for a multiboard assembly.
The template file includes all the connectors and their pins, but does NOT
include any wires or connections.
inputs:
assembly_file_path:
description: The path to the assembly YAML file.
required: false
default: "assembly.yaml"
variant:
description: The variant of the assembly to generate the template for.
required: false
default: ""
output_file:
description: The name of the file to output.
required: false
default: "wireviz.yaml"
log_level:
description: The log level used by the action. Used for debugging.
required: false
default: "INFO"
runs:
using: "docker"
image: "Dockerfile"
args:
- "--assembly-file"
- "${{ github.workspace}}/${{ inputs.assembly_file_path }}"
- "--variant"
- ${{ inputs.variant }}
- "--output-file"
- "${{ github.workspace}}/${{ inputs.output_file }}"
- "--repository"
- ${{ github.repository }}
- "--ref"
- ${{ github.ref }}
- "--allspice-hub-url"
- ${{ github.server_url }}
- "--log-level"
- ${{ inputs.log_level }}
env:
ALLSPICE_AUTH_TOKEN: ${{ github.token }}

325
generate_wireviz_template.py Executable file
View File

@ -0,0 +1,325 @@
#! /usr/bin/env python3
"""
generate_wireviz_template.py: Generate a wireviz template for the connections
in the given multiboard assembly.
"""
import argparse
import io
import logging
import os
import re
import yaml
from dataclasses import dataclass, field
from typing import Mapping, Sequence, TextIO, Optional
from allspice import AllSpice, Repository
from allspice.utils.list_components import list_components, ComponentAttributes
ASSEMBLY_YAML_FILE_PATH = "assembly.yaml"
ATTRIBUTES_TO_CHECK = [
"_reference",
"_logical_reference",
"Designator",
"Part Reference",
"Reference",
]
"""
Attributes to check for in the component attributes to determine if the
component is a connector.
"""
CONNECTOR_DESIGNATOR_REGEX = r"^(J\d+|P\d+|CN\d+|X\w\d+)$"
"""
If a designator starts with:
- J: Jack
- P: Plug
- CN: Connector
- X(A-Z): Socket connector. The second character has to be present and be
immediately followed by a digit distinguish it form X(TAL).
Source: https://en.wikipedia.org/wiki/Reference_designator
In each case, we accept designators of the pattern (letter)(numbers).
We do not need to consider multi-part components here as we request combined
components from py-allspice.
"""
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
@dataclass
class MultiboardAssembly:
@dataclass
class Dependency:
repo: str
"""Repository URL"""
source_path: str
reference: Optional[str] = None
variant: Optional[str] = None
metadata: Optional[Mapping[str, str]] = None
quantity: int = 1
@property
def owner(self) -> str:
return self.repo.split("/")[-2]
@property
def name(self) -> str:
return self.repo.split("/")[-1]
Variants = Mapping[str, Sequence[Dependency]]
name: str
description: str
dependencies: Sequence[Dependency] = field(default_factory=list)
variants: Variants = field(default_factory=dict)
@staticmethod
def load_yaml(file: TextIO) -> "MultiboardAssembly":
data = yaml.safe_load(file)
dependencies = [
MultiboardAssembly.Dependency(**dep) for dep in data.get("dependencies", [])
]
variants = {}
for variant_name, variant_deps in data.get("variants", {}).items():
variant_dependencies = [
MultiboardAssembly.Dependency(**dep) for dep in variant_deps
]
variants[variant_name] = variant_dependencies
return MultiboardAssembly(
name=data["name"],
description=data["description"],
dependencies=dependencies,
variants=variants,
)
@dataclass
class WirevizConnector:
designator: str
notes: str
pin_labels: Sequence[str]
def to_dict(self) -> dict:
"""
Returns a dict that can be used to generate a wireviz template.
"""
return {
self.designator: {
"notes": self.notes,
"pinlabels": list(self.pin_labels),
}
}
def get_dependency_components(
client: AllSpice,
dependency: MultiboardAssembly.Dependency,
) -> list[ComponentAttributes]:
"""
Get the components list of a dependency.
Right now, this assumes the dependency is on the same AllSpice Hub instance
as the client.
"""
try:
repo_name, repo_owner = dependency.repo.split("/")[-1:-3:-1]
except ValueError:
raise ValueError(f"Invalid repository URL: {dependency.repo}")
repo = Repository.request(client, repo_owner, repo_name)
return list_components(
client,
repo,
dependency.source_path,
variant=dependency.variant,
ref=dependency.reference or repo.default_branch,
combine_multi_part=True,
)
def get_component_designator(component: ComponentAttributes) -> str:
for attribute in ATTRIBUTES_TO_CHECK:
attr_value = component.get(attribute)
if attr_value is None or not isinstance(attr_value, str):
continue
else:
return attr_value
return ""
def find_connector_components(
components: list[ComponentAttributes],
) -> list[ComponentAttributes]:
connector_components = []
for component in components:
for attribute in ATTRIBUTES_TO_CHECK:
attr_value = component.get(attribute)
if attr_value is None or not isinstance(attr_value, str):
continue
if re.match(CONNECTOR_DESIGNATOR_REGEX, attr_value):
connector_components.append(component)
break
return connector_components
def convert_component_to_wireviz_connector(
component: ComponentAttributes,
source_dependency: MultiboardAssembly.Dependency,
) -> list[WirevizConnector]:
component_designator = get_component_designator(component)
combined_designator = f"{source_dependency.name}_{component_designator}"
# For Altium components, we get a better result indexing by part_id instead
# of name, so we'll use fallbacks.
component_name = component.get("_part_id") or component.get("_name") or ""
notes = f"{component_name} from {source_dependency.repo}"
if source_dependency.variant:
notes += f" (variant {source_dependency.variant})"
# The ComponentAttributes type doesn't know that pins is a list
pins: list[dict] = component.get("_pins", []) # type: ignore
logger.debug("Found %d pins for component %s", len(pins), component_designator)
pin_labels = []
if pins:
for pin in pins:
pin_name = pin.get("name") or pin.get("designator")
if pin_name:
pin_labels.append(pin_name)
else:
logger.warn("Pin %s does not have a name or designator; skipping.", pin)
if source_dependency.quantity > 1:
return [
WirevizConnector(
designator=f"{combined_designator}_{i}",
notes=notes + f" (part {i})",
pin_labels=pin_labels,
)
for i in range(1, source_dependency.quantity + 1)
]
else:
return [
WirevizConnector(
designator=combined_designator,
notes=notes,
pin_labels=pin_labels,
)
]
def main():
arg_parser = argparse.ArgumentParser(description=__doc__)
arg_parser.add_argument(
"--assembly-file",
required=True,
help="Path to the assembly file to generate the wireviz template for.",
)
arg_parser.add_argument(
"--variant",
required=False,
help="Variant to use for the assembly file. If blank or not given, the default variant is used.",
)
arg_parser.add_argument(
"--output-file",
required=True,
help="Path to the output file to write the wireviz template to.",
)
arg_parser.add_argument(
"--repository",
required=True,
help="Repository name in owner/repo formmat.",
)
arg_parser.add_argument(
"--ref",
required=True,
help="Reference to use for the repository.",
)
arg_parser.add_argument(
"--allspice-hub-url",
required=True,
help="URL of the AllSpice Hub instance to use.",
)
arg_parser.add_argument(
"--log-level",
required=False,
default="INFO",
help="Level of logger to use, default INFO.",
)
args = arg_parser.parse_args()
logger.setLevel(args.log_level.upper())
logger.info("Starting wireviz template generator, with args %s", vars(args))
token_text = os.getenv("ALLSPICE_AUTH_TOKEN")
if not token_text:
raise ValueError("ALLSPICE_AUTH_TOKEN environment variable not set.")
client = AllSpice(
allspice_hub_url=args.allspice_hub_url,
token_text=token_text,
log_level=args.log_level.upper(),
)
owner_name, repo_name = args.repository.split("/")
repository = Repository.request(client, owner_name, repo_name)
assembly_data = repository.get_raw_file(args.assembly_file, ref=args.ref)
assembly = MultiboardAssembly.load_yaml(io.StringIO(assembly_data.decode("utf-8")))
variant = args.variant
logger.info(
"Loaded assembly file %s from repository %s",
args.assembly_file,
args.repository,
)
dependencies = list(assembly.dependencies)
if variant:
try:
dependencies += assembly.variants[variant]
except KeyError:
raise ValueError(f"Variant {variant} not found in assembly file.")
logger.info(
"Found %d dependencies to generate wireviz template for",
len(dependencies),
)
wireviz_connectors = {}
for dependency in dependencies:
components = get_dependency_components(client, dependency)
connector_components = find_connector_components(components)
for component in connector_components:
connectors = convert_component_to_wireviz_connector(
component,
dependency,
)
for connector in connectors:
wireviz_connectors |= connector.to_dict()
wireviz_output = {"connectors": wireviz_connectors}
with open(args.output_file, "w") as output_file:
yaml.dump(wireviz_output, output_file)
logger.info("Wireviz template generated and written to %s", args.output_file)
if __name__ == "__main__":
main()

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
py-allspice @ git+https://github.com/AllSpiceIO/py-allspice@dd317edb066b7e3248333784e4acac7b535696c5
pyyaml~=6.0.2