* 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) | ![generic provider](https://img.shields.io/badge/Generic-orange?color=5f87bf) | ||||
![programming provider](https://img.shields.io/badge/Programming-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) | ![saas provider](https://img.shields.io/badge/SaaS-orange?color=5f87bf) | ||||
![c4 provider](https://img.shields.io/badge/C4-orange?color=5f87bf) | |||||
## Getting Started | ## 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") |