diff --git a/diagrams/aws/management.py b/diagrams/aws/management.py
index f6710686..3de22625 100644
--- a/diagrams/aws/management.py
+++ b/diagrams/aws/management.py
@@ -236,6 +236,10 @@ class TrustedAdvisor(_Management):
_icon = "trusted-advisor.png"
+class UserNotifications(_Management):
+ _icon = "user-notifications.png"
+
+
class WellArchitectedTool(_Management):
_icon = "well-architected-tool.png"
diff --git a/diagrams/azure/ml.py b/diagrams/azure/ml.py
index eb0c474e..e9fff87d 100644
--- a/diagrams/azure/ml.py
+++ b/diagrams/azure/ml.py
@@ -12,8 +12,8 @@ class AzureOpenAI(_Ml):
_icon = "azure-open-ai.png"
-class AzureSpeedToText(_Ml):
- _icon = "azure-speed-to-text.png"
+class AzureSpeechService(_Ml):
+ _icon = "azure-speech-service.png"
class BatchAI(_Ml):
diff --git a/diagrams/cli.py b/diagrams/cli.py
new file mode 100644
index 00000000..c75eb3d9
--- /dev/null
+++ b/diagrams/cli.py
@@ -0,0 +1,38 @@
+import argparse
+import sys
+
+
+def run() -> int:
+ """
+ Run diagrams code files in a diagrams environment.
+ Args:
+ paths: A list of paths to Python files containing diagrams code.
+
+ Returns:
+ The exit code.
+ """
+ parser = argparse.ArgumentParser(
+ description="Run diagrams code files in a diagrams environment.",
+ )
+ parser.add_argument(
+ "paths",
+ metavar="path",
+ type=str,
+ nargs="+",
+ help="a Python file containing diagrams code",
+ )
+ args = parser.parse_args()
+
+ for path in args.paths:
+ with open(path, encoding='utf-8') as f:
+ exec(f.read())
+
+ return 0
+
+
+def main():
+ sys.exit(run())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/diagrams/gis/data.py b/diagrams/gis/data.py
index 4da2ff43..99dbd696 100644
--- a/diagrams/gis/data.py
+++ b/diagrams/gis/data.py
@@ -24,4 +24,8 @@ class Openstreetmap(_Data):
_icon = "openstreetmap.png"
+class Overturemaps(_Data):
+ _icon = "overturemaps.png"
+
+
# Aliases
diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile
index 94be6b2b..ab653077 100644
--- a/docker/dev/Dockerfile
+++ b/docker/dev/Dockerfile
@@ -1,5 +1,5 @@
# use latest python alpine image.
-FROM python:3.13.2-alpine3.20
+FROM python:3.13.3-alpine3.20
# install system dependencies.
RUN apk update && apk add --no-cache \
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
index f38b5242..cdbc86f8 100644
--- a/docs/getting-started/installation.md
+++ b/docs/getting-started/installation.md
@@ -7,7 +7,7 @@ title: Installation
**diagrams** uses [Graphviz](https://www.graphviz.org/) to render the diagram, so you need to [install Graphviz](https://graphviz.gitlab.io/download/) to use it.
-> macOS users using [Homebrew](https://brew.sh) can install Graphviz via `brew install graphviz` . Similarly, Windows users with [Chocolatey](https://chocolatey.org) installed can run `choco install graphviz`.
+> macOS users using [Homebrew](https://brew.sh) can install Graphviz via `brew install graphviz` . Similarly, Windows users with [Chocolatey](https://chocolatey.org) installed can run `choco install graphviz` or use [Winget](https://learn.microsoft.com/windows/package-manager/) via `winget install Graphviz.Graphviz -i`.
After installing Graphviz (or if you already have it), install **diagrams**:
@@ -20,6 +20,9 @@ $ pipenv install diagrams
# using poetry
$ poetry add diagrams
+
+# using uv
+$ uv tool install diagrams
```
## Quick Start
@@ -47,6 +50,14 @@ This generates the diagram below:
It will be saved as `web_service.png` in your working directory.
+### CLI
+
+With the `diagrams` CLI you can process one or more diagram files at once.
+
+```shell
+$ diagrams diagram1.py diagram2.py
+```
+
## Next
See more [Examples](/docs/getting-started/examples) or see the [Guides](/docs/guides/diagram) page for more details.
diff --git a/docs/guides/edge.md b/docs/guides/edge.md
index befee710..c1bfced6 100644
--- a/docs/guides/edge.md
+++ b/docs/guides/edge.md
@@ -65,5 +65,131 @@ with Diagram(name="Advanced Web Service with On-Premises (colored)", show=False)
>> Edge(color="darkorange") \
>> aggregator
```
+
-
+## Less Edges
+
+As you can see on the previous graph the edges can quickly become noisy. Below are two examples to solve this problem.
+
+One approach is to get creative with the Node class to create blank placeholders, together with named nodes within Clusters, and then only pointing to single named elements within those Clusters.
+
+Compare the output below to the example output above .
+
+```python
+from diagrams import Cluster, Diagram, Node
+from diagrams.onprem.analytics import Spark
+from diagrams.onprem.compute import Server
+from diagrams.onprem.database import PostgreSQL
+from diagrams.onprem.inmemory import Redis
+from diagrams.onprem.aggregator import Fluentd
+from diagrams.onprem.monitoring import Grafana, Prometheus
+from diagrams.onprem.network import Nginx
+from diagrams.onprem.queue import Kafka
+
+with Diagram("\nAdvanced Web Service with On-Premise Less edges", show=False) as diag:
+ ingress = Nginx("ingress")
+
+ with Cluster("Service Cluster"):
+ serv1 = Server("grpc1")
+ serv2 = Server("grpc2")
+ serv3 = Server("grpc3")
+
+ with Cluster(""):
+ blankHA = Node("", shape="plaintext", width="0", height="0")
+
+ metrics = Prometheus("metric")
+ metrics << Grafana("monitoring")
+
+ aggregator = Fluentd("logging")
+ blankHA >> aggregator >> Kafka("stream") >> Spark("analytics")
+
+ with Cluster("Database HA"):
+ db = PostgreSQL("users")
+ db - PostgreSQL("replica") << metrics
+ blankHA >> db
+
+ with Cluster("Sessions HA"):
+ sess = Redis("session")
+ sess - Redis("replica") << metrics
+ blankHA >> sess
+
+ ingress >> serv2 >> blankHA
+
+diag
+```
+
+
+
+## Merged Edges
+
+Yet another option is to set the graph_attr dictionary key "concentrate" to "true".
+
+Note the following restrictions:
+
+1. the Edge must end at the same headport
+2. This only works when the "splines" graph_attr key is set to the value "spline". It has no effect when the value was set to "ortho", which is the default for the diagrams library.
+3. this will only work with the "dot" layout engine, which is the default for the diagrams library.
+
+For more information see:
+
+ https://graphviz.gitlab.io/doc/info/attrs.html#d:concentrate
+
+ https://www.graphviz.org/pdf/dotguide.pdf Section 3.3 Concentrators
+
+
+
+```python
+from diagrams import Cluster, Diagram, Edge, Node
+from diagrams.onprem.analytics import Spark
+from diagrams.onprem.compute import Server
+from diagrams.onprem.database import PostgreSQL
+from diagrams.onprem.inmemory import Redis
+from diagrams.onprem.aggregator import Fluentd
+from diagrams.onprem.monitoring import Grafana, Prometheus
+from diagrams.onprem.network import Nginx
+from diagrams.onprem.queue import Kafka
+
+graph_attr = {
+ "concentrate": "true",
+ "splines": "spline",
+}
+
+edge_attr = {
+ "minlen":"3",
+}
+
+with Diagram("\n\nAdvanced Web Service with On-Premise Merged edges", show=False,
+ graph_attr=graph_attr,
+ edge_attr=edge_attr) as diag:
+
+ ingress = Nginx("ingress")
+
+ metrics = Prometheus("metric")
+ metrics << Edge(minlen="0") << Grafana("monitoring")
+
+ with Cluster("Service Cluster"):
+ grpsrv = [
+ Server("grpc1"),
+ Server("grpc2"),
+ Server("grpc3")]
+
+ blank = Node("", shape="plaintext", height="0.0", width="0.0")
+
+ with Cluster("Sessions HA"):
+ sess = Redis("session")
+ sess - Redis("replica") << metrics
+
+ with Cluster("Database HA"):
+ db = PostgreSQL("users")
+ db - PostgreSQL("replica") << metrics
+
+ aggregator = Fluentd("logging")
+ aggregator >> Kafka("stream") >> Spark("analytics")
+
+ ingress >> [grpsrv[0], grpsrv[1], grpsrv[2],]
+ [grpsrv[0], grpsrv[1], grpsrv[2],] - Edge(headport="w", minlen="1") - blank
+ blank >> Edge(headport="w", minlen="2") >> [sess, db, aggregator]
+
+diag
+```
+
diff --git a/docs/nodes/aws.md b/docs/nodes/aws.md
index 05ca5b5e..66cb5ffa 100644
--- a/docs/nodes/aws.md
+++ b/docs/nodes/aws.md
@@ -1001,6 +1001,9 @@ Node classes list of the aws provider.
**diagrams.aws.management.TrustedAdvisor**
+
+**diagrams.aws.management.UserNotifications**
+
**diagrams.aws.management.WellArchitectedTool**
diff --git a/docs/nodes/azure.md b/docs/nodes/azure.md
index 875471e8..9a766500 100644
--- a/docs/nodes/azure.md
+++ b/docs/nodes/azure.md
@@ -491,8 +491,8 @@ Node classes list of the azure provider.
**diagrams.azure.ml.AzureOpenAI**
-
-**diagrams.azure.ml.AzureSpeedToText**
+
+**diagrams.azure.ml.AzureSpeechService**
**diagrams.azure.ml.BatchAI**
diff --git a/docs/nodes/gis.md b/docs/nodes/gis.md
index 0de533e7..4460c3b3 100644
--- a/docs/nodes/gis.md
+++ b/docs/nodes/gis.md
@@ -41,6 +41,9 @@ Node classes list of the gis provider.
**diagrams.gis.data.Openstreetmap**
+
+**diagrams.gis.data.Overturemaps**
+
## gis.database
diff --git a/pyproject.toml b/pyproject.toml
index 7e9b68b5..3e169152 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,6 +9,9 @@ homepage = "https://diagrams.mingrammer.com"
repository = "https://github.com/mingrammer/diagrams"
include = ["resources/**/*"]
+[tool.poetry.scripts]
+diagrams="diagrams.cli:main"
+
[tool.poetry.dependencies]
python = "^3.9"
graphviz = ">=0.13.2,<0.21.0"
diff --git a/resources/aws/management/user-notifications.png b/resources/aws/management/user-notifications.png
new file mode 100644
index 00000000..fb4511f3
Binary files /dev/null and b/resources/aws/management/user-notifications.png differ
diff --git a/resources/azure/ml/azure-speech-service.png b/resources/azure/ml/azure-speech-service.png
new file mode 100644
index 00000000..7716f11d
Binary files /dev/null and b/resources/azure/ml/azure-speech-service.png differ
diff --git a/resources/azure/ml/azure-speed-to-text.png b/resources/azure/ml/azure-speed-to-text.png
deleted file mode 100644
index ca331f12..00000000
Binary files a/resources/azure/ml/azure-speed-to-text.png and /dev/null differ
diff --git a/resources/gis/data/overturemaps.png b/resources/gis/data/overturemaps.png
new file mode 100644
index 00000000..44f52acc
Binary files /dev/null and b/resources/gis/data/overturemaps.png differ
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 00000000..77759f59
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,95 @@
+import os
+import unittest
+from io import StringIO
+from unittest.mock import mock_open, patch
+
+from diagrams.cli import run
+
+
+class CliTest(unittest.TestCase):
+ def setUp(self):
+ self.test_file = "test_diagram.py"
+ # dummy content for the test file
+ self.test_content_1 = """
+from diagrams import Diagram
+with Diagram(name="Test", show=False):
+ pass
+"""
+ # content from getting started examples with utf-8
+ # only support the installed fonts defined in Dockerfile
+ self.test_content_2 = """
+from diagrams import Diagram
+from diagrams.aws.compute import EC2
+from diagrams.aws.database import RDS
+from diagrams.aws.network import ELB
+
+with Diagram("test_2", show=False, direction="TB"):
+ ELB("lb") >> [EC2("ワーカー1"),
+ EC2("작업자 2를"),
+ EC2("робітник 3"),
+ EC2("worker4"),
+ EC2("työntekijä 4")] >> RDS("events")
+"""
+
+ def tearDown(self):
+ try:
+ os.remove("test.png")
+ except FileNotFoundError:
+ pass
+
+ def test_run_with_valid_file(self):
+ # write the test file
+ with open(self.test_file, "w") as f:
+ f.write(self.test_content_1)
+ with patch("sys.argv", ["diagrams", self.test_file]):
+ exit_code = run()
+ self.assertEqual(exit_code, 0)
+ try:
+ os.remove(self.test_file)
+ except FileNotFoundError:
+ pass
+
+ def test_run_with_multiple_files(self):
+
+ multiple_files = ["file1.py", "file2.py"]
+
+ # write the code files
+ with open("file1.py", "w") as f:
+ f.write(self.test_content_1)
+ with open("file2.py", "w") as f:
+ f.write(self.test_content_2)
+
+ with patch("sys.argv", ["diagrams"] + multiple_files):
+ exit_code = run()
+ self.assertEqual(exit_code, 0)
+
+ # cleanup code file
+ for one_file in multiple_files:
+ try:
+ os.remove(one_file)
+ except FileNotFoundError:
+ pass
+ # cleanup generated image
+ try:
+ os.remove("test_2.png")
+ except FileNotFoundError:
+ pass
+
+ def test_run_with_no_arguments(self):
+ with patch("sys.argv", ["diagrams"]):
+ with patch("sys.stderr", new=StringIO()) as fake_stderr:
+ with self.assertRaises(SystemExit):
+ run()
+ self.assertIn("the following arguments are required: path", fake_stderr.getvalue())
+
+ def test_run_with_nonexistent_file(self):
+ with patch("sys.argv", ["diagrams", "nonexistent.py"]):
+ with self.assertRaises(FileNotFoundError):
+ run()
+
+ def test_run_with_invalid_python_code(self):
+ invalid_content = "this is not valid python code"
+ with patch("builtins.open", mock_open(read_data=invalid_content)):
+ with patch("sys.argv", ["diagrams", self.test_file]):
+ with self.assertRaises(SyntaxError):
+ run()
diff --git a/website/static/img/advanced_web_service_with_on-premise_less_edges.png b/website/static/img/advanced_web_service_with_on-premise_less_edges.png
new file mode 100644
index 00000000..c3eca10a
Binary files /dev/null and b/website/static/img/advanced_web_service_with_on-premise_less_edges.png differ
diff --git a/website/static/img/advanced_web_service_with_on-premise_merged_edges.png b/website/static/img/advanced_web_service_with_on-premise_merged_edges.png
new file mode 100644
index 00000000..f04ab058
Binary files /dev/null and b/website/static/img/advanced_web_service_with_on-premise_merged_edges.png differ
diff --git a/website/static/img/resources/aws/management/user-notifications.png b/website/static/img/resources/aws/management/user-notifications.png
new file mode 100644
index 00000000..fb4511f3
Binary files /dev/null and b/website/static/img/resources/aws/management/user-notifications.png differ
diff --git a/website/static/img/resources/azure/ml/azure-speech-service.png b/website/static/img/resources/azure/ml/azure-speech-service.png
new file mode 100644
index 00000000..7716f11d
Binary files /dev/null and b/website/static/img/resources/azure/ml/azure-speech-service.png differ
diff --git a/website/static/img/resources/azure/ml/azure-speed-to-text.png b/website/static/img/resources/azure/ml/azure-speed-to-text.png
deleted file mode 100644
index ca331f12..00000000
Binary files a/website/static/img/resources/azure/ml/azure-speed-to-text.png and /dev/null differ
diff --git a/website/static/img/resources/gis/data/overturemaps.png b/website/static/img/resources/gis/data/overturemaps.png
new file mode 100644
index 00000000..44f52acc
Binary files /dev/null and b/website/static/img/resources/gis/data/overturemaps.png differ
diff --git a/website/static/img/resources/gis/georchestra/analytics.svg b/website/static/img/resources/gis/georchestra/analytics.svg
old mode 100755
new mode 100644
diff --git a/website/static/img/resources/gis/georchestra/data_api.svg b/website/static/img/resources/gis/georchestra/data_api.svg
old mode 100755
new mode 100644
diff --git a/website/static/img/resources/gis/georchestra/datafeeder.svg b/website/static/img/resources/gis/georchestra/datafeeder.svg
old mode 100755
new mode 100644