-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
e8eb3d8
commit 90dd239
Showing
5 changed files
with
239 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.