docs: improve architecture diagram assets
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 130 KiB |
@@ -1,206 +1,408 @@
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
from diagrams import Cluster, Diagram, Edge
|
||||
from diagrams.generic.blank import Blank
|
||||
from diagrams.onprem.client import Client, User
|
||||
from diagrams.onprem.compute import Server
|
||||
from diagrams.onprem.container import Docker
|
||||
from diagrams.onprem.database import Mongodb
|
||||
from diagrams.onprem.inmemory import Redis
|
||||
from diagrams.onprem.network import Internet
|
||||
from diagrams.onprem.network import Caddy, Internet
|
||||
from diagrams.onprem.queue import Rabbitmq
|
||||
from diagrams.onprem.security import Vault
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
OUT = ROOT / "docs" / "assets" / "diagrams"
|
||||
TARGET_RATIO = 16 / 9
|
||||
|
||||
|
||||
def graph_attr() -> dict[str, str]:
|
||||
"""Return shared Graphviz settings for clean 16:9 diagram output."""
|
||||
|
||||
return {
|
||||
"bgcolor": "transparent",
|
||||
"pad": "0.35",
|
||||
"ranksep": "0.8",
|
||||
"nodesep": "0.55",
|
||||
"fontname": "Inter",
|
||||
"bgcolor": "white",
|
||||
"pad": "0.42",
|
||||
"ranksep": "1.05",
|
||||
"nodesep": "0.72",
|
||||
"fontname": "Arial",
|
||||
"fontsize": "18",
|
||||
"dpi": "220",
|
||||
"size": "16,9!",
|
||||
"ratio": "fill",
|
||||
"splines": "spline",
|
||||
"outputorder": "edgesfirst",
|
||||
}
|
||||
|
||||
|
||||
def node_attr() -> dict[str, str]:
|
||||
"""Return readable node text styling while preserving provider icons."""
|
||||
|
||||
return {
|
||||
"fontname": "Inter",
|
||||
"fontsize": "13",
|
||||
"style": "rounded,filled",
|
||||
"fillcolor": "#111827",
|
||||
"fontcolor": "#E5E7EB",
|
||||
"color": "#334155",
|
||||
"fontname": "Arial",
|
||||
"fontsize": "14",
|
||||
"fontcolor": "#172033",
|
||||
"labelloc": "b",
|
||||
"margin": "0.12",
|
||||
}
|
||||
|
||||
|
||||
def edge_attr() -> dict[str, str]:
|
||||
return {"fontname": "Inter", "fontsize": "11", "color": "#64748B", "fontcolor": "#475569"}
|
||||
"""Return restrained edge styling for presentation diagrams."""
|
||||
|
||||
return {
|
||||
"fontname": "Arial",
|
||||
"fontsize": "11",
|
||||
"color": "#64748B",
|
||||
"fontcolor": "#334155",
|
||||
"arrowsize": "0.75",
|
||||
"penwidth": "1.6",
|
||||
}
|
||||
|
||||
|
||||
def cluster_attr() -> dict[str, str]:
|
||||
"""Return subtle cluster styling that keeps related services visually grouped."""
|
||||
|
||||
return {
|
||||
"bgcolor": "#F8FAFC",
|
||||
"fontname": "Arial",
|
||||
"fontsize": "16",
|
||||
"fontcolor": "#334155",
|
||||
"pencolor": "#CBD5E1",
|
||||
}
|
||||
|
||||
|
||||
def diagram(name: str, filename: str, *, direction: str = "LR") -> Diagram:
|
||||
"""Create a 16:9 icon-based diagram using the diagrams package."""
|
||||
|
||||
return Diagram(
|
||||
name,
|
||||
filename=str(OUT / filename),
|
||||
show=False,
|
||||
direction=direction,
|
||||
outformat="png",
|
||||
curvestyle="curved",
|
||||
graph_attr=graph_attr(),
|
||||
node_attr=node_attr(),
|
||||
edge_attr=edge_attr(),
|
||||
)
|
||||
|
||||
|
||||
def normalize_png(filename: str) -> None:
|
||||
"""Center a rendered PNG on an exact 16:9 white canvas."""
|
||||
|
||||
path = OUT / f"{filename}.png"
|
||||
png = _read_png(path)
|
||||
target_width, target_height = _target_size(png.width, png.height)
|
||||
if (png.width, png.height) == (target_width, target_height):
|
||||
return
|
||||
|
||||
channels = png.channels
|
||||
white_pixel = b"\xff" * channels
|
||||
rows = [bytearray(white_pixel * target_width) for _ in range(target_height)]
|
||||
offset_x = (target_width - png.width) // 2
|
||||
offset_y = (target_height - png.height) // 2
|
||||
for source_y, source_row in enumerate(png.rows):
|
||||
target_row = rows[offset_y + source_y]
|
||||
start = offset_x * channels
|
||||
target_row[start : start + len(source_row)] = source_row
|
||||
|
||||
_write_png(path, PngImage(target_width, target_height, png.color_type, rows))
|
||||
|
||||
|
||||
class PngImage:
|
||||
"""Small in-memory PNG representation for deterministic 16:9 padding."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: int,
|
||||
height: int,
|
||||
color_type: int,
|
||||
rows: list[bytearray],
|
||||
) -> None:
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.color_type = color_type
|
||||
self.rows = rows
|
||||
|
||||
@property
|
||||
def channels(self) -> int:
|
||||
"""Return channel count for supported truecolor PNG types."""
|
||||
|
||||
return 4 if self.color_type == 6 else 3
|
||||
|
||||
|
||||
def _target_size(width: int, height: int) -> tuple[int, int]:
|
||||
"""Calculate the smallest exact 16:9 canvas that contains the diagram."""
|
||||
|
||||
if width / height < TARGET_RATIO:
|
||||
return round(height * TARGET_RATIO), height
|
||||
return width, round(width / TARGET_RATIO)
|
||||
|
||||
|
||||
def _read_png(path: Path) -> PngImage:
|
||||
"""Read a non-interlaced 8-bit RGB/RGBA PNG produced by Graphviz."""
|
||||
|
||||
data = path.read_bytes()
|
||||
if not data.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||
raise ValueError(f"{path} is not a PNG file")
|
||||
|
||||
position = 8
|
||||
width = height = color_type = 0
|
||||
compressed = bytearray()
|
||||
while position < len(data):
|
||||
length = struct.unpack(">I", data[position : position + 4])[0]
|
||||
chunk_type = data[position + 4 : position + 8]
|
||||
chunk_data = data[position + 8 : position + 8 + length]
|
||||
position += 12 + length
|
||||
if chunk_type == b"IHDR":
|
||||
width, height, bit_depth, color_type, _, _, interlace = struct.unpack(
|
||||
">IIBBBBB", chunk_data
|
||||
)
|
||||
if bit_depth != 8 or color_type not in {2, 6} or interlace != 0:
|
||||
raise ValueError(f"Unsupported PNG encoding in {path}")
|
||||
elif chunk_type == b"IDAT":
|
||||
compressed.extend(chunk_data)
|
||||
elif chunk_type == b"IEND":
|
||||
break
|
||||
|
||||
channels = 4 if color_type == 6 else 3
|
||||
stride = width * channels
|
||||
raw = zlib.decompress(bytes(compressed))
|
||||
rows: list[bytearray] = []
|
||||
previous = bytearray(stride)
|
||||
cursor = 0
|
||||
for _ in range(height):
|
||||
filter_type = raw[cursor]
|
||||
cursor += 1
|
||||
row = bytearray(raw[cursor : cursor + stride])
|
||||
cursor += stride
|
||||
_unfilter(row, previous, filter_type, channels)
|
||||
rows.append(row)
|
||||
previous = row
|
||||
|
||||
return PngImage(width, height, color_type, rows)
|
||||
|
||||
|
||||
def _unfilter(row: bytearray, previous: bytearray, filter_type: int, channels: int) -> None:
|
||||
"""Reverse PNG scanline filters used by Graphviz output."""
|
||||
|
||||
for index in range(len(row)):
|
||||
left = row[index - channels] if index >= channels else 0
|
||||
up = previous[index]
|
||||
upper_left = previous[index - channels] if index >= channels else 0
|
||||
match filter_type:
|
||||
case 0:
|
||||
value = row[index]
|
||||
case 1:
|
||||
value = row[index] + left
|
||||
case 2:
|
||||
value = row[index] + up
|
||||
case 3:
|
||||
value = row[index] + ((left + up) // 2)
|
||||
case 4:
|
||||
value = row[index] + _paeth(left, up, upper_left)
|
||||
case _:
|
||||
raise ValueError(f"Unsupported PNG filter type: {filter_type}")
|
||||
row[index] = value & 0xFF
|
||||
|
||||
|
||||
def _paeth(left: int, up: int, upper_left: int) -> int:
|
||||
"""Return the PNG Paeth predictor for a single channel."""
|
||||
|
||||
prediction = left + up - upper_left
|
||||
left_distance = abs(prediction - left)
|
||||
up_distance = abs(prediction - up)
|
||||
upper_left_distance = abs(prediction - upper_left)
|
||||
if left_distance <= up_distance and left_distance <= upper_left_distance:
|
||||
return left
|
||||
if up_distance <= upper_left_distance:
|
||||
return up
|
||||
return upper_left
|
||||
|
||||
|
||||
def _write_png(path: Path, png: PngImage) -> None:
|
||||
"""Write a simple unfiltered PNG with the same color type as the source."""
|
||||
|
||||
raw = bytearray()
|
||||
for row in png.rows:
|
||||
raw.append(0)
|
||||
raw.extend(row)
|
||||
|
||||
ihdr = struct.pack(">IIBBBBB", png.width, png.height, 8, png.color_type, 0, 0, 0)
|
||||
chunks = [
|
||||
_chunk(b"IHDR", ihdr),
|
||||
_chunk(b"IDAT", zlib.compress(bytes(raw), level=6)),
|
||||
_chunk(b"IEND", b""),
|
||||
]
|
||||
path.write_bytes(b"\x89PNG\r\n\x1a\n" + b"".join(chunks))
|
||||
|
||||
|
||||
def _chunk(chunk_type: bytes, data: bytes) -> bytes:
|
||||
"""Create one PNG chunk with CRC."""
|
||||
|
||||
return (
|
||||
struct.pack(">I", len(data))
|
||||
+ chunk_type
|
||||
+ data
|
||||
+ struct.pack(">I", zlib.crc32(chunk_type + data) & 0xFFFFFFFF)
|
||||
)
|
||||
|
||||
|
||||
def link(label: str) -> Edge:
|
||||
"""Create a consistent labeled connector."""
|
||||
|
||||
return Edge(label=label)
|
||||
|
||||
|
||||
def render_service_architecture() -> None:
|
||||
with Diagram(
|
||||
"Service Architecture",
|
||||
filename=str(OUT / "service_architecture"),
|
||||
show=False,
|
||||
direction="LR",
|
||||
graph_attr=graph_attr(),
|
||||
node_attr=node_attr(),
|
||||
edge_attr=edge_attr(),
|
||||
):
|
||||
browser = Internet("Browser")
|
||||
demo = Blank("demo-app\nFastAPI UI")
|
||||
auth = Blank("auth-service\ngRPC")
|
||||
worker = Blank("sms-worker")
|
||||
mongo = Mongodb("MongoDB")
|
||||
redis = Redis("Redis")
|
||||
rabbit = Rabbitmq("RabbitMQ")
|
||||
providers = Blank("Kavenegar / SMS.ir")
|
||||
with diagram("Service Architecture", "service_architecture"):
|
||||
with Cluster("Client boundary", graph_attr=cluster_attr()):
|
||||
browser = Client("Browser")
|
||||
demo = Docker("demo-app\nFastAPI BFF")
|
||||
|
||||
browser >> Edge(label="HTTPS / local HTTP") >> demo
|
||||
demo >> Edge(label="gRPC") >> auth
|
||||
auth >> Edge(label="users + refresh sessions") >> mongo
|
||||
auth >> Edge(label="OTP hash + rate limits") >> redis
|
||||
auth >> Edge(label="SMS job") >> rabbit >> worker >> providers
|
||||
with Cluster("Application services", graph_attr=cluster_attr()):
|
||||
auth = Server("auth-service\ngRPC API")
|
||||
worker = Docker("sms-worker")
|
||||
|
||||
with Cluster("State and messaging", graph_attr=cluster_attr()):
|
||||
mongo = Mongodb("MongoDB\nusers + sessions")
|
||||
redis = Redis("Redis\nOTP TTL + limits")
|
||||
rabbit = Rabbitmq("RabbitMQ\nSMS jobs")
|
||||
|
||||
providers = Internet("Kavenegar / SMS.ir")
|
||||
|
||||
browser >> link("HTTPS / local HTTP") >> demo >> link("gRPC") >> auth
|
||||
auth >> link("documents") >> mongo
|
||||
auth >> link("ephemeral state") >> redis
|
||||
auth >> link("durable job") >> rabbit >> link("consume") >> worker
|
||||
worker >> link("provider API") >> providers
|
||||
normalize_png("service_architecture")
|
||||
|
||||
|
||||
def render_clean_architecture() -> None:
|
||||
with Diagram(
|
||||
"Clean Architecture Layers",
|
||||
filename=str(OUT / "clean_architecture"),
|
||||
show=False,
|
||||
direction="TB",
|
||||
graph_attr=graph_attr(),
|
||||
node_attr=node_attr(),
|
||||
edge_attr=edge_attr(),
|
||||
):
|
||||
with Cluster("Transport"):
|
||||
grpc = Blank("gRPC servicer")
|
||||
demo = Blank("Demo BFF")
|
||||
with Cluster("Application"):
|
||||
auth = Blank("AuthService\nuse cases")
|
||||
security = Blank("Token + OTP helpers")
|
||||
with Cluster("Domain"):
|
||||
entities = Blank("Entities")
|
||||
ports = Blank("Ports")
|
||||
with Cluster("Infrastructure"):
|
||||
mongo = Blank("Mongo repositories")
|
||||
redis = Blank("Redis OTP store")
|
||||
rabbit = Blank("RabbitMQ publisher")
|
||||
sms = Blank("SMS strategies")
|
||||
with diagram("Clean Architecture Layers", "clean_architecture", direction="TB"):
|
||||
with Cluster("Transport", graph_attr=cluster_attr()):
|
||||
demo = Client("Demo BFF")
|
||||
grpc = Server("gRPC servicer")
|
||||
|
||||
grpc >> auth
|
||||
demo >> grpc
|
||||
auth >> entities
|
||||
auth >> ports
|
||||
auth >> security
|
||||
ports >> mongo
|
||||
ports >> redis
|
||||
ports >> rabbit
|
||||
ports >> sms
|
||||
with Cluster("Application", graph_attr=cluster_attr()):
|
||||
auth = Server("AuthService\nuse cases")
|
||||
security = Vault("JWT + OTP\nsecurity helpers")
|
||||
|
||||
with Cluster("Domain", graph_attr=cluster_attr()):
|
||||
entities = Blank("Entities\nUser, Session, Role")
|
||||
ports = Blank("Ports\nrepositories + SMS")
|
||||
|
||||
with Cluster("Infrastructure", graph_attr=cluster_attr()):
|
||||
mongo = Mongodb("Mongo repositories")
|
||||
redis = Redis("Redis OTP store")
|
||||
rabbit = Rabbitmq("RabbitMQ publisher")
|
||||
sms = Internet("SMS strategies")
|
||||
|
||||
demo >> link("calls") >> grpc >> link("executes") >> auth
|
||||
auth >> link("uses") >> entities
|
||||
auth >> link("depends on") >> ports
|
||||
auth >> link("delegates crypto") >> security
|
||||
ports << link("implements") << [mongo, redis, rabbit, sms]
|
||||
normalize_png("clean_architecture")
|
||||
|
||||
|
||||
def render_otp_flow() -> None:
|
||||
with Diagram(
|
||||
"OTP Login Flow",
|
||||
filename=str(OUT / "otp_login_flow"),
|
||||
show=False,
|
||||
direction="LR",
|
||||
graph_attr=graph_attr(),
|
||||
node_attr=node_attr(),
|
||||
edge_attr=edge_attr(),
|
||||
):
|
||||
user = Blank("User")
|
||||
demo = Blank("demo-app")
|
||||
auth = Blank("auth-service")
|
||||
redis = Redis("Redis")
|
||||
rabbit = Rabbitmq("RabbitMQ")
|
||||
worker = Blank("sms-worker")
|
||||
sms = Blank("SMS provider")
|
||||
mongo = Mongodb("MongoDB")
|
||||
with diagram("OTP Login Flow", "otp_login_flow"):
|
||||
user = User("User")
|
||||
|
||||
user >> Edge(label="mobile") >> demo >> Edge(label="RequestOtp") >> auth
|
||||
auth >> Edge(label="store OTP hash") >> redis
|
||||
auth >> Edge(label="publish job") >> rabbit >> worker >> sms
|
||||
user >> Edge(label="code") >> demo >> Edge(label="VerifyOtp") >> auth
|
||||
auth >> Edge(label="compare hash") >> redis
|
||||
auth >> Edge(label="upsert user + session") >> mongo
|
||||
auth >> Edge(label="token pair") >> demo >> user
|
||||
with Cluster("Request OTP", graph_attr=cluster_attr()):
|
||||
demo_request = Client("demo-app")
|
||||
auth_request = Server("RequestOtp")
|
||||
redis_store = Redis("Redis\nstore HMAC + TTL")
|
||||
rabbit = Rabbitmq("RabbitMQ\npublish SMS job")
|
||||
|
||||
with Cluster("Delivery", graph_attr=cluster_attr()):
|
||||
worker = Docker("sms-worker")
|
||||
sms = Internet("SMS provider")
|
||||
|
||||
with Cluster("Verify OTP", graph_attr=cluster_attr()):
|
||||
demo_verify = Client("demo-app")
|
||||
auth_verify = Server("VerifyOtp")
|
||||
redis_verify = Redis("Redis\ncompare hash")
|
||||
mongo = Mongodb("MongoDB\nuser + session")
|
||||
|
||||
user >> link("1. mobile") >> demo_request >> link("2. gRPC") >> auth_request
|
||||
auth_request >> link("3. hash only") >> redis_store
|
||||
auth_request >> link("4. queued") >> rabbit >> link("5. consume") >> worker
|
||||
worker >> link("6. send OTP") >> sms
|
||||
user >> link("7. code") >> demo_verify >> link("8. gRPC") >> auth_verify
|
||||
auth_verify >> link("9. verify") >> redis_verify
|
||||
auth_verify >> link("10. issue session") >> mongo
|
||||
auth_verify >> link("11. token pair") >> demo_verify
|
||||
normalize_png("otp_login_flow")
|
||||
|
||||
|
||||
def render_refresh_flow() -> None:
|
||||
with Diagram(
|
||||
"Refresh Token Rotation",
|
||||
filename=str(OUT / "refresh_rotation_flow"),
|
||||
show=False,
|
||||
direction="LR",
|
||||
graph_attr=graph_attr(),
|
||||
node_attr=node_attr(),
|
||||
edge_attr=edge_attr(),
|
||||
):
|
||||
client = Blank("Client")
|
||||
auth = Blank("auth-service")
|
||||
mongo = Mongodb("MongoDB")
|
||||
old = Blank("Old session\nrevoked")
|
||||
new = Blank("New session\nactive")
|
||||
with diagram("Refresh Token Rotation", "refresh_rotation_flow"):
|
||||
client = Client("Client")
|
||||
auth = Server("auth-service\nRefreshToken")
|
||||
mongo = Mongodb("MongoDB\nrefresh_sessions")
|
||||
|
||||
client >> Edge(label="refresh token") >> auth
|
||||
auth >> Edge(label="hash lookup") >> mongo
|
||||
mongo >> Edge(label="active session") >> auth
|
||||
auth >> old
|
||||
auth >> new
|
||||
new >> Edge(label="new token pair") >> client
|
||||
with Cluster("Rotation result", graph_attr=cluster_attr()):
|
||||
old = Vault("Old hash\nrevoked")
|
||||
new = Vault("New hash\nactive")
|
||||
|
||||
client >> link("1. opaque refresh token") >> auth
|
||||
auth >> link("2. hash lookup") >> mongo >> link("3. active session") >> auth
|
||||
auth >> link("4. revoke") >> old
|
||||
auth >> link("5. create") >> new >> link("6. persist") >> mongo
|
||||
auth >> link("7. new token pair") >> client
|
||||
normalize_png("refresh_rotation_flow")
|
||||
|
||||
|
||||
def render_sms_strategy() -> None:
|
||||
with Diagram(
|
||||
"SMS Provider Strategy",
|
||||
filename=str(OUT / "sms_provider_strategy"),
|
||||
show=False,
|
||||
direction="LR",
|
||||
graph_attr=graph_attr(),
|
||||
node_attr=node_attr(),
|
||||
edge_attr=edge_attr(),
|
||||
):
|
||||
worker = Blank("sms-worker")
|
||||
factory = Blank("create_sms_client")
|
||||
port = Blank("SmsClient port")
|
||||
kav = Blank("Kavenegar")
|
||||
smsir = Blank("SMS.ir")
|
||||
debug = Blank("Debug local")
|
||||
with diagram("SMS Provider Strategy", "sms_provider_strategy", direction="TB"):
|
||||
worker = Docker("sms-worker")
|
||||
factory = Server("create_sms_client()\nprovider factory")
|
||||
port = Blank("SmsClient port\nsend_otp(...)")
|
||||
|
||||
worker >> factory >> port
|
||||
port >> kav
|
||||
port >> smsir
|
||||
port >> debug
|
||||
with Cluster("Concrete strategies", graph_attr=cluster_attr()):
|
||||
kavenegar = Internet("Kavenegar")
|
||||
sms_ir = Internet("SMS.ir")
|
||||
debug = Redis("Debug provider\nlocal Redis")
|
||||
|
||||
worker >> link("startup wiring") >> factory >> link("returns interface") >> port
|
||||
port >> link("SMS_PROVIDER=kavenegar") >> kavenegar
|
||||
port >> link("SMS_PROVIDER=sms_ir") >> sms_ir
|
||||
port >> link("SMS_PROVIDER=debug") >> debug
|
||||
normalize_png("sms_provider_strategy")
|
||||
|
||||
|
||||
def render_production_deployment() -> None:
|
||||
with Diagram(
|
||||
"Production Deployment",
|
||||
filename=str(OUT / "production_deployment"),
|
||||
show=False,
|
||||
direction="LR",
|
||||
graph_attr=graph_attr(),
|
||||
node_attr=node_attr(),
|
||||
edge_attr=edge_attr(),
|
||||
):
|
||||
internet = Internet("Internet")
|
||||
caddy = Blank("Caddy\nHTTPS")
|
||||
demo = Blank("demo-app")
|
||||
auth = Blank("auth-service")
|
||||
private = Blank("Private Docker network")
|
||||
mongo = Mongodb("MongoDB")
|
||||
redis = Redis("Redis")
|
||||
rabbit = Rabbitmq("RabbitMQ")
|
||||
worker = Blank("sms-worker")
|
||||
with diagram("Production Deployment", "production_deployment"):
|
||||
internet = Internet("Internet\ngapido.amiirkhl.ir")
|
||||
caddy = Caddy("Caddy\n80 / 443 only")
|
||||
|
||||
internet >> Edge(label="gapido.amiirkhl.ir") >> caddy >> demo >> auth
|
||||
auth >> private
|
||||
private >> mongo
|
||||
private >> redis
|
||||
private >> rabbit >> worker
|
||||
with Cluster("Private Docker network", graph_attr=cluster_attr()):
|
||||
demo = Docker("demo-app\nFastAPI UI")
|
||||
auth = Server("auth-service\nprivate gRPC")
|
||||
mongo = Mongodb("MongoDB\nprivate")
|
||||
redis = Redis("Redis\nprivate")
|
||||
rabbit = Rabbitmq("RabbitMQ\nprivate")
|
||||
worker = Docker("sms-worker")
|
||||
|
||||
sms = Internet("SMS provider")
|
||||
|
||||
internet >> link("HTTPS") >> caddy >> link("reverse proxy") >> demo
|
||||
demo >> link("gRPC") >> auth
|
||||
auth >> link("sessions") >> mongo
|
||||
auth >> link("OTP state") >> redis
|
||||
auth >> link("SMS job") >> rabbit >> link("consume") >> worker
|
||||
worker >> link("provider API") >> sms
|
||||
normalize_png("production_deployment")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Regenerate all 16:9 icon-based diagrams for docs and slides."""
|
||||
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
render_service_architecture()
|
||||
render_clean_architecture()
|
||||
@@ -212,4 +414,3 @@ def main() -> None:
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||