Metadata-Version: 2.1
Name: grot
Version: 0.2.1
Summary: Graphviz syntax wrapper. Draw graphs with pure python.
Home-page: https://gitlab.com/kamichal/grot
Author: Michal Kaczmarczyk
Author-email: michal.s.kaczmarczyk@gmail.com
Maintainer: Michal Kaczmarczyk
Maintainer-email: michal.s.kaczmarczyk@gmail.com
License: MIT license
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Education
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python
Classifier: Topic :: Documentation
Classifier: Topic :: Multimedia :: Graphics :: Presentation
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Utilities
Description-Content-Type: text/markdown
License-File: LICENSE

# Grot
**Grot** is a noun and means **arrowhead** in polish language.

### Makes graphviz usage simpler
Much less headache. Gets you faster into the point.

## Hello Grot
To generate a graph you need to import Grot class, create its instance and
define nodes and edges. While `g.edge()` call, you can pass unlimited
number of nodes or plain strings (creates implicit node).

If you don't connect given node (as `unconnected`) it's going to float somewhere around.
```python
def example_01():
    import os
    from grot import Grot

    this_dir_path = os.path.dirname(__file__)  # if run in console - remove 'directory' parameter below
    out_dir_path = os.path.join(this_dir_path, "out")

    g = Grot(name="example_01", format="png", directory=out_dir_path, graph_attrs={"rankdir": "LR"})

    one = g.node("It is\neaiser")
    two = g.node("graphs", color="#8a9bac")
    ignored = g.node("Node floats when\nunconnected", color="#da3080")

    g.edge(one, "to define", two)
    g.render()


```
Source of this example is in [examples/example_01.py](examples/example_01.py).

It generates raw dot-syntax text file in: [examples/out/example_01.gv](examples/out/example_01.gv).
And the final graph file is in: [examples/out/example_01.gv.png](examples/out/example_01.gv.png):

[![Rendered example image to be shown in gitlab)](examples/out/example_01.gv.png?raw=true "example_01")](examples/out/example_01.gv.png)

# Branches

One call of `g.edge(node1, node2, node3, ...)` creates single chain of arrows.
To make a branch, you need to call `g.edge` once again.
```python
def example_02a():
    from grot import Grot

    this_dir_path = os.path.dirname(__file__)  # if run in console - remove 'directory' parameter below
    out_dir_path = os.path.join(this_dir_path, "out")
    g = Grot(name="example_02a", format="png", directory=out_dir_path, graph_attrs={"rankdir": "LR"})

    one = g.node("One")
    two = g.node("Two")
    three = g.node("Three")
    four = g.node("Four")
    five = g.node("Five")
    six = g.node("Six")
    seven = g.node("Seven")

    g.edge(one, two, three, four, "A")
    g.edge(two, five, six, "B")
    g.edge(two, seven, "C")

    g.render()


```
Source of this example is in [examples/example_02.py](examples/example_02.py).

It generates raw dot-syntax text file in: [examples/out/example_02a.gv](examples/out/example_02a.gv).
And the final graph file is in: [examples/out/example_02a.gv.png](examples/out/example_02a.gv.png):

[![Rendered example image to be shown in gitlab)](examples/out/example_02a.gv.png?raw=true "example_02a")](examples/out/example_02a.gv.png)

# Branches - syntax variant
You don't have to assign nodes to variables. However it's a good practice to do so.
You can define nodes while `g.edge(...)` call.
Here node `two` is assigned to a local variable, because we refer it several times.
```python
def example_02b():
    from grot import Grot

    this_dir_path = os.path.dirname(__file__)  # if run in console - remove 'directory' parameter below
    out_dir_path = os.path.join(this_dir_path, "out")

    g = Grot(name="example_02b", format="png", directory=out_dir_path, graph_attrs={"rankdir": "LR"})

    two = g.node("Two")
    g.edge(g.node("One"), two, g.node("Three"), g.node("Four"), "A")
    g.edge(two, g.node("Five"), g.node("Six"), "B")
    g.edge(two, g.node("Seven"), "C")

    g.render()


```
Source of this example is in [examples/example_02.py](examples/example_02.py).

It generates raw dot-syntax text file in: [examples/out/example_02b.gv](examples/out/example_02b.gv).
And the final graph file is in: [examples/out/example_02b.gv.png](examples/out/example_02b.gv.png):

[![Rendered example image to be shown in gitlab)](examples/out/example_02b.gv.png?raw=true "example_02b")](examples/out/example_02b.gv.png)

# Branches

One call of `g.edge(node1, node2, node3, ...)` creates single chain of arrows.
To make a branch, you need to call `g.edge` once again.
```python
def example_tree():
    import os
    from typing import List, Optional

    from airium import Airium
    from grot import Grot, NodeVisit, Node

    this_dir_path = os.path.dirname(__file__)  # if run in console - remove 'directory' parameter below
    out_dir_path = os.path.join(this_dir_path, "out")

    font_face = "Monospace"  # "Courier New", "Helvetica"
    g = Grot(
        name="example_tree",
        format="png",
        directory=out_dir_path,
        graph_attrs={
            "fontname": font_face,
            "layout": "neato",
            "overlap": "scalexy",
            "mode": "ipsep",
            "sep": "+5",
        },
        node_attrs={
            "fontname": font_face,
            "penwidth": "1.1",
            "shape": "box",
            "width": "0",
            "height": "0",
            "margin": "0",
        },
        edge_attrs={
            "fontname": font_face,
            "penwidth": "0.80",
        },
    )

    def html_table_label(
        own_name: str, own_type: str, own_path: str, children: list[NodeVisit], fg_color: str, bg_color=str
    ) -> str:
        a = Airium(base_indent="")

        with a.table(border="0", bgcolor=bg_color):
            shaded_color = "#757575"
            with a.tr():
                with a.td(align="left").font(color=fg_color, **{"point-size": "29"}):
                    a(f" <b><u>{own_name}</u></b>")
                with a.td(align="right").font(color=fg_color):
                    types_ = " | ".join(sorted(set(c.type for c in children)))
                    types_ = f" [{types_[30:]}...]" if len(types_) > 36 else f" [{types_}]"
                    a(f"{own_type}{types_}")
            with a.tr():
                with a.td(colspan=2, align="center").font(color=shaded_color):
                    a(f"<i>{own_path}</i>")

            for visit in children:
                type_annotation = f'<font color="{shaded_color}"><b>{visit.type}</b> '
                sub_types = " | ".join(sorted(NodeVisit.children_types(visit.value)))
                if len(sub_types) > 36:
                    type_annotation += f"[{sub_types[:30]}...]"
                elif sub_types:
                    type_annotation += f"[{sub_types}]"
                type_annotation += "</font>"
                right = repr(visit.value)
                if len(right) < 55 and "<" not in right:
                    left = f"{visit.key}: {type_annotation}"
                    right = f" = {right}"
                else:
                    left = f"{visit.key}:"
                    right = type_annotation

                with a.tr():
                    a.td(align="left").font(_t=left, color=fg_color)
                    a.td(align="right").font(_t=right, color=fg_color)

            if not children:
                a.tr().td(colspan=2).font(_t="No children", color=fg_color)
        return str(a)

    def draw_node(node_object, path=("root",)) -> Optional[Node]:
        children: List[NodeVisit] = []
        for visit in NodeVisit.iter_tree_nodes(node_object):
            visit: NodeVisit
            visit.graph_node_id = draw_node(visit.value, (*path, visit.key))
            children.append(visit)

        if not children:
            return  # draw only containers not leafs
        fg_col = "#111111" if len(path) < 2 else "#112233" if len(path) % 2 else "#334433"
        bg_col = "#eeeeee" if len(path) < 2 else "#ddeedd" if len(path) % 2 else "#eee6dd"
        table_body: str = html_table_label(
            own_name=path[-1],
            own_type=type(node_object).__name__,
            own_path=NodeVisit.join_path_str(*path),
            children=children,
            fg_color=fg_col,
            bg_color=bg_col,
        )

        own_node: Node = g.html_node(table_body)

        for visit in children:
            if visit.graph_node_id:
                g.edge(own_node, visit.graph_node_id)

        return own_node

    my_nested_structure = {
        "web-app": {
            "servlet": [
                {
                    "servlet-name": "cofaxCDS",
                    "servlet-class": "org.cofax.cds.CDSServlet",
                    "init-param": {
                        "configGlossary:installationAt": "Philadelphia, PA",
                        "configGlossary:poweredBy": "Cofax",
                        "dataStoreLogFile": "/usr/local/tomcat/logs/datastore.log",
                        "dataStoreConnUsageLimit": 100,
                        "dataStoreLogLevel": "debug",
                        "maxUrlLength": 500,
                    },
                },
                {
                    "servlet-name": "cofaxEmail",
                    "servlet-class": "org.cofax.cds.EmailServlet",
                    "init-param": {"mailHost": "mail1", "mailHostOverride": "mail2"},
                },
                {"servlet-name": "cofaxAdmin", "servlet-class": "org.cofax.cds.AdminServlet"},
                {"servlet-name": "fileServlet", "servlet-class": "org.cofax.cds.FileServlet"},
                {
                    "servlet-name": "cofaxTools",
                    "servlet-class": "org.cofax.cms.CofaxToolsServlet",
                    "init-param": {
                        "templatePath": "toolstemplates/",
                        "log": 1,
                        "logLocation": "/usr/local/tomcat/logs/CofaxTools.log",
                        "lookInContext": 1,
                        "adminGroupID": 4,
                        "betaServer": True,
                    },
                },
            ],
            "servlet-mapping": {
                "cofaxCDS": "/",
                "cofaxEmail": "/cofaxutil/aemail/*",
                "cofaxAdmin": "/admin/*",
                "fileServlet": "/static/*",
                "cofaxTools": "/tools/*",
            },
            "taglib": {"taglib-uri": "cofax.tld", "taglib-location": "/WEB-INF/tlds/cofax.tld"},
        }
    }
    draw_node(my_nested_structure)

    g.render()


```
Source of this example is in [examples/example_tree.py](examples/example_tree.py).

It generates raw dot-syntax text file in: [examples/out/example_tree.gv](examples/out/example_tree.gv).
And the final graph file is in: [examples/out/example_tree.gv.png](examples/out/example_tree.gv.png):

[![Rendered example image to be shown in gitlab)](examples/out/example_tree.gv.png?raw=true "example_tree")](examples/out/example_tree.gv.png)

