feat: Basic support for C4 model primitives. (#508)

* Basic support for C4 model primitives.

* Use the "rect" shape for nodes

With the record shape we used before, graphviz would trip over
edges that set constraint=False.

* Adopt C4 terminology: Rename Dependency -> Relationship

* Adopt C4 terminology: Rename type -> technology

* Extract a shared C4Node

This makes the code more DRY, but also allows to add company-
specific extensions more easily. One need we have is to slightly
adapt the terminology. At Spotify, we happen to call `Container`
a `Component` for example. This is now easier to implement on top
of the shared `C4Node`.

* Add "C4" shield to the README

* Document how to produce a C4 diagram
pull/757/head
Marc Bruggmann 2 years ago committed by GitHub
parent e8eb3d83c3
commit 90dd23926b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -37,6 +37,7 @@ Diagrams lets you draw the cloud system architecture **in Python code**. It was
![generic provider](https://img.shields.io/badge/Generic-orange?color=5f87bf)
![programming provider](https://img.shields.io/badge/Programming-orange?color=5f87bf)
![saas provider](https://img.shields.io/badge/SaaS-orange?color=5f87bf)
![c4 provider](https://img.shields.io/badge/C4-orange?color=5f87bf)
## Getting Started

@ -0,0 +1,97 @@
"""
A set of nodes and edges to visualize software architecture using the C4 model.
"""
import html
import textwrap
from diagrams import Cluster, Node, Edge
def _format_node_label(name, key, description):
"""Create a graphviz label string for a C4 node"""
title = f'<font point-size="12"><b>{html.escape(name)}</b></font><br/>'
subtitle = f'<font point-size="9">[{html.escape(key)}]<br/></font>' if key else ""
text = f'<br/><font point-size="10">{_format_description(description)}</font>' if description else ""
return f"<{title}{subtitle}{text}>"
def _format_description(description):
"""
Formats the description string so it fits into the C4 nodes.
It line-breaks the description so it fits onto exactly three lines. If there are more
than three lines, all further lines are discarded and "..." inserted on the last line to
indicate that it was shortened. This will also html-escape the description so it can
safely be included in a HTML label.
"""
wrapper = textwrap.TextWrapper(width=40, max_lines=3)
lines = [html.escape(line) for line in wrapper.wrap(description)]
lines += [""] * (3 - len(lines)) # fill up with empty lines so it is always three
return "<br/>".join(lines)
def _format_edge_label(description):
"""Create a graphviz label string for a C4 edge"""
wrapper = textwrap.TextWrapper(width=24, max_lines=3)
lines = [html.escape(line) for line in wrapper.wrap(description)]
text = "<br/>".join(lines)
return f'<<font point-size="10">{text}</font>>'
def C4Node(name, technology="", description="", type="Container", **kwargs):
key = f"{type}: {technology}" if technology else type
node_attributes = {
"label": _format_node_label(name, key, description),
"labelloc": "c",
"shape": "rect",
"width": "2.6",
"height": "1.6",
"fixedsize": "true",
"style": "filled",
"fillcolor": "dodgerblue3",
"fontcolor": "white",
}
# collapse boxes to a smaller form if they don't have a description
if not description:
node_attributes.update({"width": "2", "height": "1"})
node_attributes.update(kwargs)
return Node(**node_attributes)
def Container(name, technology="", description="", **kwargs):
return C4Node(name, technology=technology, description=description, type="Container")
def Database(name, technology="", description="", **kwargs):
return C4Node(name, technology=technology, description=description, type="Database", shape="cylinder", labelloc="b")
def System(name, description="", external=False, **kwargs):
type = "External System" if external else "System"
fillcolor = "gray60" if external else "dodgerblue4"
return C4Node(name, description=description, type=type, fillcolor=fillcolor)
def Person(name, description="", external=False, **kwargs):
type = "External Person" if external else "Person"
fillcolor = "gray60" if external else "dodgerblue4"
style = "rounded,filled"
return C4Node(name, description=description, type=type, fillcolor=fillcolor, style=style)
def SystemBoundary(name, **kwargs):
graph_attributes = {
"label": html.escape(name),
"bgcolor": "white",
"margin": "16",
"style": "dashed",
}
graph_attributes.update(kwargs)
return Cluster(name, graph_attr=graph_attributes)
def Relationship(label="", **kwargs):
edge_attribtues = {"style": "dashed", "color": "gray60"}
if label:
edge_attribtues.update({"label": _format_edge_label(label)})
edge_attribtues.update(kwargs)
return Edge(**edge_attribtues)

@ -0,0 +1,77 @@
---
id: c4
title: C4
---
## C4 Diagrams
[C4](https://c4model.com/) is a standardized model to visualize software architecture.
You can generate C4 diagrams by using the node and edge classes from the `diagrams.c4` package:
```python
from diagrams import Diagram
from diagrams.c4 import Person, Container, Database, System, SystemBoundary, Relationship
graph_attr = {
"splines": "spline",
}
with Diagram("Container diagram for Internet Banking System", direction="TB", graph_attr=graph_attr):
customer = Person(
name="Personal Banking Customer", description="A customer of the bank, with personal bank accounts."
)
with SystemBoundary("Internet Banking System"):
webapp = Container(
name="Web Application",
technology="Java and Spring MVC",
description="Delivers the static content and the Internet banking single page application.",
)
spa = Container(
name="Single-Page Application",
technology="Javascript and Angular",
description="Provides all of the Internet banking functionality to customers via their web browser.",
)
mobileapp = Container(
name="Mobile App",
technology="Xamarin",
description="Provides a limited subset of the Internet banking functionality to customers via their mobile device.",
)
api = Container(
name="API Application",
technology="Java and Spring MVC",
description="Provides Internet banking functionality via a JSON/HTTPS API.",
)
database = Database(
name="Database",
technology="Oracle Database Schema",
description="Stores user registration information, hashed authentication credentials, access logs, etc.",
)
email = System(name="E-mail System", description="The internal Microsoft Exchange e-mail system.", external=True)
mainframe = System(
name="Mainframe Banking System",
description="Stores all of the core banking information about customers, accounts, transactions, etc.",
external=True,
)
customer >> Relationship("Visits bigbank.com/ib using [HTTPS]") >> webapp
customer >> Relationship("Views account balances, and makes payments using") >> [spa, mobileapp]
webapp >> Relationship("Delivers to the customer's web browser") >> spa
spa >> Relationship("Make API calls to [JSON/HTTPS]") >> api
mobileapp >> Relationship("Make API calls to [JSON/HTTPS]") >> api
api >> Relationship("reads from and writes to") >> database
api >> Relationship("Sends email using [SMTP]") >> email
api >> Relationship("Makes API calls to [XML/HTTPS]") >> mainframe
customer << Relationship("Sends e-mails to") << email
```
It will produce the following diagram:
![c4](/img/c4.png)

@ -0,0 +1,64 @@
import os
import random
import string
import unittest
from diagrams import Diagram
from diagrams import setcluster, setdiagram
from diagrams.c4 import Person, Container, Database, System, SystemBoundary, Relationship
class C4Test(unittest.TestCase):
def setUp(self):
self.name = "diagram-" + "".join([random.choice(string.hexdigits) for n in range(7)])
def tearDown(self):
setdiagram(None)
setcluster(None)
try:
os.remove(self.name + ".png")
except FileNotFoundError:
pass
def test_nodes(self):
with Diagram(name=self.name, show=False):
person = Person("person", "A person.")
container = Container("container", "Java application", "The application.")
database = Database("database", "Oracle database", "Stores information.")
def test_external_nodes(self):
with Diagram(name=self.name, show=False):
external_person = Person("person", external=True)
external_system = System("external", external=True)
def test_systems(self):
with Diagram(name=self.name, show=False):
system = System("system", "The internal system.")
system_without_description = System("unknown")
def test_edges(self):
with Diagram(name=self.name, show=False):
c1 = Container("container1")
c2 = Container("container2")
c1 >> c2
def test_edges_with_labels(self):
with Diagram(name=self.name, show=False):
c1 = Container("container1")
c2 = Container("container2")
c1 >> Relationship("depends on") >> c2
c1 << Relationship("is depended on by") << c2
def test_edge_without_constraint(self):
with Diagram(name=self.name, show=False):
s1 = System("system 1")
s2 = System("system 2")
s1 >> Relationship(constraint="False") >> s2
def test_cluster(self):
with Diagram(name=self.name, show=False):
with SystemBoundary("System"):
Container("container", "type", "description")

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Loading…
Cancel
Save