diff --git a/pyproject.toml b/pyproject.toml index b8e7332..ddbef7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,3 +57,6 @@ dev = [ "Sphinx>=1.8.5", "twine>=1.14.0", ] + +[tool.setuptools.package-data] +tro_utils = ["*.jinja2"] diff --git a/tro_utils/cli.py b/tro_utils/cli.py index 628c8d2..301c73a 100644 --- a/tro_utils/cli.py +++ b/tro_utils/cli.py @@ -1,4 +1,5 @@ """Console script for tro_utils.""" +import os import sys import click @@ -6,6 +7,37 @@ from . import TRPAttribute from .tro_utils import TRO +_TEMPLATES = { + "default": { + "description": "Default pretty template by Craig Willis", + "filename": "default.jinja2", + }, +} + + +class StringOrPath(click.ParamType): + """Custom parameter type to accept either a valid string or a file path.""" + + name = "string_or_path" + + def __init__(self, templates=None): + self.valid_strings = templates.keys() + + def convert(self, value, param, ctx): + # Check if the value is in the allowed string set + if value in self.valid_strings: + return value + # Check if the value is a valid path + elif os.path.exists(value) and os.path.isfile(value): + return value + else: + self.fail( + f"'{value}' is neither a valid option ({', '.join(self.valid_strings)}) " + f"nor a valid file path.", + param, + ctx, + ) + @click.group() @click.option( @@ -189,12 +221,20 @@ def sign(ctx): @cli.command(help="Generate a report of the TRO", name="report") @click.option( - "--template", "-t", type=click.Path(), required=True, help="Template file" + "--template", + "-t", + type=StringOrPath(_TEMPLATES), + required=True, + help=f"Template file or one of the following: {', '.join(_TEMPLATES.keys())}", ) @click.option("--output", "-o", type=click.Path(), required=True, help="Output file") @click.pass_context def generate_report(ctx, template, output): declaration = ctx.parent.params.get("declaration") + if template in _TEMPLATES: + template = os.path.join( + os.path.dirname(__file__), _TEMPLATES[template]["filename"] + ) tro = TRO( filepath=declaration, ) diff --git a/tro_utils/default.jinja2 b/tro_utils/default.jinja2 new file mode 100644 index 0000000..1ff41a4 --- /dev/null +++ b/tro_utils/default.jinja2 @@ -0,0 +1,69 @@ +# TRO Report + + +## TRO Information +| Property | Value | +| -------- | ----- | +| Name | {{ tro["schema:name"] }} | +| Description | {{ tro["schema:description"] }} | +| Created by | {{ tro["schema:creator"] }} | +| Created date | {{ tro["schema:dateCreated"] }} | + + +## TRACE System Information + +This TRO was generated by the following TRACE System: + +| Property | Value | +| -------- | ----- | +| Name | {{ tro["trov:wasAssembledBy"]["trov:name"] }} | +| Description | {{ tro["trov:wasAssembledBy"]["trov:description"] }} | +| Owner | {{ tro["trov:wasAssembledBy"]["trov:owner"] }} | +| Contact | {{ tro["trov:wasAssembledBy"]["trov:contact"] }} | +| URL | {{ tro["trov:wasAssembledBy"]["trov:url"] }} | + +
+Show Public Key +{{ tro["trov:wasAssembledBy"]["trov:publicKey"] }} +
+ +### Capabilities + +| Capability | Description | +| ----------- | ------------ | +{%- for capability in tro["trov:wasAssembledBy"].get("trov:hasCapability", []) %} +| {{ capability.get("@type", "") }} | {{ capability.get("trov:description", "") }} | +{%- endfor %} + +## Trusted Research Performances + +A Trusted Research Performance (TRP) captures the execution of a process in the context of a TRACE system. Typically, a TRP would take as input one or more sets of files (input arrangements) and produce another set of files (output arrangements). + + + +| Description | Accessed | Contributed | +| ----------- | ------------ | ------------ | +{%- for trp in tro["trps"] %} +| {{ trp["description"] }} | {{ trp["accessed"] }} | {{ trp["contributed"] }} | +{%- endfor %} + + +## Arrangements + +Arrangements define how artifacts, typically files, are organized before and after each TRP. Artifacts are defined by their location and a checksum of their contents. Artifacts may be local or remote, defined by an URI. They may be included or excluded from the associated archive. + +{%- for arrangement in tro["arrangements"].keys() %} + +### {{ tro["arrangements"][arrangement]["name"] }} + +| Artifact | SHA-256 | Status | +| -------- | -------- | ------ | +{%- for location in tro["arrangements"][arrangement]["artifacts"] %} + {%- if tro["arrangements"][arrangement]["artifacts"][location].get("excluded") %} +| ~~`{{ location }}`~~ | {{ tro["arrangements"][arrangement]["artifacts"][location]["sha256"] | truncate(32) }} | Excluded due to {{ tro["arrangements"][arrangement]["artifacts"][location]["excluded"] }} | + {%- else %} +| `{{ location }}` | {{ tro["arrangements"][arrangement]["artifacts"][location]["sha256"] | truncate(32) }} | {{ tro["arrangements"][arrangement]["artifacts"][location]["status"] }} | + + {%- endif %} +{%- endfor %} +{%- endfor %} diff --git a/tro_utils/tro_utils.py b/tro_utils/tro_utils.py index 2b87461..55eaba6 100644 --- a/tro_utils/tro_utils.py +++ b/tro_utils/tro_utils.py @@ -1,4 +1,5 @@ """Main module.""" +import base64 import hashlib import json import os @@ -393,7 +394,6 @@ def add_performance( def generate_report(self, template, report): graph = self.data["@graph"][0] - trs = graph["trov:wasAssembledBy"] composition = { obj["@id"]: obj for obj in graph["trov:hasComposition"]["trov:hasArtifact"] } @@ -429,6 +429,8 @@ def generate_report(self, template, report): dot.attr("node", shape="box3d", style="filled, rounded", fillcolor="#D6FDD0") + if isinstance(graph["trov:hasPerformance"], dict): + graph["trov:hasPerformance"] = [graph["trov:hasPerformance"]] for trp in graph["trov:hasPerformance"]: description = trp["rdfs:comment"] accessed = arrangements[trp["trov:accessedArrangement"]["@id"]]["name"] @@ -437,7 +439,8 @@ def generate_report(self, template, report): dot.edge(accessed, description) dot.edge(description, contributed) - dot.render("workflow", directory=".", cleanup=True, format="png") + png_bytes = dot.pipe(format="png") + png_base64 = base64.b64encode(png_bytes).decode("utf-8") # Detect changes between arrangements # Which files were added? Which files changed? @@ -458,26 +461,8 @@ def generate_report(self, template, report): arrangements[keys[n]]["artifacts"][location]["status"] = "Created" data = { - "name": graph.get("schema:name", "No name provided"), - "description": graph.get("schema:description", "No Description provided"), - "creator": graph.get("schema:creator", "No creator provided"), - "dateCreated": graph.get("schema:dateCreated", "No date provided"), - "trs": { - "publicKey": trs.get("trov:publicKey"), - "name": trs.get("schema:name", ""), - "comment": trs["rdfs:comment"], - "publisher": trs.get("schema:publisher", ""), - "description": trs.get("schema:description", ""), - "email": trs.get("schema:email", ""), - "url": trs.get("schema:url", ""), - "capabilities": [ - { - "name": _.get("trov:name", _["@type"]), - "description": _.get("trov:description", ""), - } - for _ in trs["trov:hasCapability"] - ], - }, + **graph, + "workflow_diagram": png_base64, "trps": [ { "id": trp["@id"],