Skip to content

Commit

Permalink
Merge pull request #19 from cognitedata/doc-as-mermaid
Browse files Browse the repository at this point in the history
"Doc as mermaid" which is one step towards a "documentation" feature (see issue #15 )

option 1: copy mermaid output to (Windows) `clip.exe` and copy it into `https://mermaid.live` for review
```
➟  poetry run bootstrap-cli diagram .local/config-deploy-bootstrap.yml | clip.exe
```
option 2: add a markdown wrapper and pipe it into a `graph.md` document, where you can use VSCode with mermaid extension as output or for review
```
➟  poetry run bootstrap-cli diagram --markdown-yes .local/config-deploy-bootstrap.yml > graph.md
```
  • Loading branch information
spex66 authored Apr 6, 2022
2 parents 7faea58 + 978f6fd commit d86bcc4
Show file tree
Hide file tree
Showing 7 changed files with 480 additions and 55 deletions.
2 changes: 1 addition & 1 deletion incubator/bootstrap_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@

# keep manually in sync with pyproject.toml until the above approach is working in Docker too
# TODO: 220327 pa: switching to gh-action supported semantic-versioning
__version__ = "1.6.0"
__version__ = "1.7.0"
347 changes: 309 additions & 38 deletions incubator/bootstrap_cli/__main__.py

Large diffs are not rendered by default.

Empty file.
155 changes: 155 additions & 0 deletions incubator/bootstrap_cli/mermaid_generator/mermaid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# std-lib
from datetime import datetime
from dataclasses import dataclass

# type-hints
from typing import Dict, List, TypeVar, Type, Union


# helper function
def timestamp():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


#
# Mermaid Dataclasses
#
# Subgraph can contain Subgraph, Nodes and Edges
# Edge only reference source/dest Nodes by their (full) name
#
#
# because within f'' strings no backslash-character is allowed
NEWLINE = "\n"


@dataclass
class MermaidFlowchartElement:
name: str
# TODO: ading default_factory value, will break the code
# see https://stackoverflow.com/q/51575931/1104502
comments: List[str]

# dump comments
def comments_to_mermaid(self):
return f"""{NEWLINE.join([f'%% {comment}' for comment in self.comments]) + NEWLINE}""" if self.comments else ""


# https://mermaid-js.github.io/mermaid/#/flowchart?id=node-shapestyle-and-arrowstyle
@dataclass
class Node(MermaidFlowchartElement):
short: str

def __repr__(self):
# TODO: how to add comments from super class the right way?
return self.comments_to_mermaid() + f"""{self.name}""" + (rf"""["{self.short}"]""" if self.short else "")


@dataclass
class RoundedNode(Node):
def __repr__(self):
return self.comments_to_mermaid() + f"""{self.name}""" + (rf"""("{self.short}")""" if self.short else "")


@dataclass
class TrapezNode(Node):
def __repr__(self):
return self.comments_to_mermaid() + f"""{self.name}""" + (rf"""[\"{self.short}"/]""" if self.short else "")


@dataclass
class AssymetricNode(Node):
def __repr__(self):
return self.comments_to_mermaid() + f"""{self.name}""" + (rf""">"{self.short}"]""" if self.short else "")


@dataclass
class SubroutineNode(Node):
def __repr__(self):
return self.comments_to_mermaid() + f"""{self.name}""" + (rf"""[["{self.short}"]]""" if self.short else "")


@dataclass
class Edge(MermaidFlowchartElement):
# from / to cannot be used as "from" is a reserved keyword
dest: str
annotation: str

def __repr__(self):
return self.comments_to_mermaid() + f"""{self.name}-->{self.dest}"""
# cannot render a 🕑 on an edge annotation
# return (
# rf'''{self.name}-->|{self.annotation}|{self.dest}'''
# if self.annotation
# else f'''{self.name}-->{self.dest}'''
# )


@dataclass
class DottedEdge(Edge):
def __repr__(self):
return self.comments_to_mermaid() + f"""{self.name}-.->{self.dest}"""


# type-hint for ExtpipesCore instance response
T_Subgraph = TypeVar("T_Subgraph", bound="Subgraph")


@dataclass
class Subgraph(MermaidFlowchartElement):
elements: List[Union[T_Subgraph, Node]]

def __contains__(self, name):
return name in [elem.name for elem in self.elements]

def __getitem__(self, name):
if name in self.elements:
return [elem.name for elem in self.elements if elem.name == name][0] # exactly one expected

def __repr__(self):
return (
self.comments_to_mermaid()
+ f"""
subgraph "{self.name}"
{NEWLINE.join([f' {elem}' for elem in self.elements])}
end
"""
)


class GraphRegistry:
"""
A graph reqistry is
* a list of elements and edges to render (representing the "graph")
* provides a registry for lookup of already created subgraphs by name for reuse ("get_or_create")
* supports printing the graph in a mermaid-compatible format
"""

def __init__(self, elements=[]):
self.subgraph_registry: Dict[str, Type[Subgraph]] = {}
# nested
self.elements: List[Union[T_Subgraph, Node, Edge]] = elements
# final block of edges
self.edges: List[Edge] = []

def get_or_create(self, subgraph_name):
return self.subgraph_registry.setdefault(
# get if exists
subgraph_name,
# create if new
Subgraph(name=subgraph_name, elements=[], comments=[]),
)

# def __str__(self):
# for elem in self.elements:
# print(elem.name)

def to_mermaid(self) -> str:

mermaid_flowchart = "\n".join(
(
["graph LR", f"%% {timestamp()} - Script generated Mermaid diagram"]
+ list(map(str, self.elements + self.edges))
)
)

return mermaid_flowchart
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]
name = "inso-bootstrap-cli"
# keep manually in sync with __init__.py > __version__
version = "1.6.0"
version = "1.7.0"
description = "A CLI to deploy a CDF Project to bootstrap CDF Groups scoped with Data Sets and RAW DBs"
authors = ["Peter Arwanitis <peter.arwanitis@cognite.com>", "Tugce Ozgur Oztetik <tugce.oztetik@cognite.com>", "Sverre Dørheim <sverre.dorheim@cognite.com>"]
license = "Apache-2.0"
Expand Down
1 change: 0 additions & 1 deletion tests/example/config-deploy-bootstrap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,3 @@ bootstrap:

# Changelog:
# 220404 pa: created for pytests

28 changes: 14 additions & 14 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from incubator.bootstrap_cli.__main__ import BootstrapCore
from dotenv import load_dotenv
import os, json
import json


def main():
config_file = "tests/example/config-deploy-bootstrap.yml"
Expand All @@ -9,35 +10,35 @@ def main():

print(bootstrap.deployed["datasets"])

root_account='root'
root_account = "root"
group_name, group_capabilities = bootstrap.generate_group_name_and_capabilities(
root_account=root_account,
)
root_account=root_account,
)
# sort capabilities by key
print(group_name, json.dumps(sorted(group_capabilities, key=lambda x: list(x.keys())[0]), indent=2))

print('='*80)
print("=" * 80)

action='read'
group_ns = 'src'
group_core = 'src:001:sap'
action = "read"
group_ns = "src"
group_core = "src:001:sap"
group_name, group_capabilities = bootstrap.generate_group_name_and_capabilities(
action=action,
group_ns=group_ns,
group_core=group_core,
)
)

# sort capabilities by key
print(group_name, json.dumps(sorted(group_capabilities, key=lambda x: list(x.keys())[0]), indent=2))

print('='*80)
print("=" * 80)

action='owner'
group_ns = 'src'
action = "owner"
group_ns = "src"
group_name, group_capabilities = bootstrap.generate_group_name_and_capabilities(
action=action,
group_ns=group_ns,
)
)

# sort capabilities by key
print(group_name, json.dumps(sorted(group_capabilities, key=lambda x: list(x.keys())[0]), indent=2))
Expand All @@ -53,4 +54,3 @@ def main():
# print('\n'.join([f'{k}: {v}' for k,v in os.environ.items()]))

main()

0 comments on commit d86bcc4

Please sign in to comment.