* 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 diagrampull/757/head
@@ -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") |