docs: improve architecture diagram assets

This commit is contained in:
2026-07-14 11:35:10 +03:30
parent 4db53f89d0
commit 70604b93b7
7 changed files with 355 additions and 154 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -1,206 +1,408 @@
import struct
import zlib
from pathlib import Path from pathlib import Path
from diagrams import Cluster, Diagram, Edge from diagrams import Cluster, Diagram, Edge
from diagrams.generic.blank import Blank 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.database import Mongodb
from diagrams.onprem.inmemory import Redis 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.queue import Rabbitmq
from diagrams.onprem.security import Vault
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "docs" / "assets" / "diagrams" OUT = ROOT / "docs" / "assets" / "diagrams"
TARGET_RATIO = 16 / 9
def graph_attr() -> dict[str, str]: def graph_attr() -> dict[str, str]:
"""Return shared Graphviz settings for clean 16:9 diagram output."""
return { return {
"bgcolor": "transparent", "bgcolor": "white",
"pad": "0.35", "pad": "0.42",
"ranksep": "0.8", "ranksep": "1.05",
"nodesep": "0.55", "nodesep": "0.72",
"fontname": "Inter", "fontname": "Arial",
"fontsize": "18",
"dpi": "220",
"size": "16,9!",
"ratio": "fill",
"splines": "spline",
"outputorder": "edgesfirst",
} }
def node_attr() -> dict[str, str]: def node_attr() -> dict[str, str]:
"""Return readable node text styling while preserving provider icons."""
return { return {
"fontname": "Inter", "fontname": "Arial",
"fontsize": "13", "fontsize": "14",
"style": "rounded,filled", "fontcolor": "#172033",
"fillcolor": "#111827", "labelloc": "b",
"fontcolor": "#E5E7EB", "margin": "0.12",
"color": "#334155",
} }
def edge_attr() -> dict[str, str]: 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: def render_service_architecture() -> None:
with Diagram( with diagram("Service Architecture", "service_architecture"):
"Service Architecture", with Cluster("Client boundary", graph_attr=cluster_attr()):
filename=str(OUT / "service_architecture"), browser = Client("Browser")
show=False, demo = Docker("demo-app\nFastAPI BFF")
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")
browser >> Edge(label="HTTPS / local HTTP") >> demo with Cluster("Application services", graph_attr=cluster_attr()):
demo >> Edge(label="gRPC") >> auth auth = Server("auth-service\ngRPC API")
auth >> Edge(label="users + refresh sessions") >> mongo worker = Docker("sms-worker")
auth >> Edge(label="OTP hash + rate limits") >> redis
auth >> Edge(label="SMS job") >> rabbit >> worker >> providers 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: def render_clean_architecture() -> None:
with Diagram( with diagram("Clean Architecture Layers", "clean_architecture", direction="TB"):
"Clean Architecture Layers", with Cluster("Transport", graph_attr=cluster_attr()):
filename=str(OUT / "clean_architecture"), demo = Client("Demo BFF")
show=False, grpc = Server("gRPC servicer")
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")
grpc >> auth with Cluster("Application", graph_attr=cluster_attr()):
demo >> grpc auth = Server("AuthService\nuse cases")
auth >> entities security = Vault("JWT + OTP\nsecurity helpers")
auth >> ports
auth >> security with Cluster("Domain", graph_attr=cluster_attr()):
ports >> mongo entities = Blank("Entities\nUser, Session, Role")
ports >> redis ports = Blank("Ports\nrepositories + SMS")
ports >> rabbit
ports >> 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: def render_otp_flow() -> None:
with Diagram( with diagram("OTP Login Flow", "otp_login_flow"):
"OTP Login Flow", user = User("User")
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")
user >> Edge(label="mobile") >> demo >> Edge(label="RequestOtp") >> auth with Cluster("Request OTP", graph_attr=cluster_attr()):
auth >> Edge(label="store OTP hash") >> redis demo_request = Client("demo-app")
auth >> Edge(label="publish job") >> rabbit >> worker >> sms auth_request = Server("RequestOtp")
user >> Edge(label="code") >> demo >> Edge(label="VerifyOtp") >> auth redis_store = Redis("Redis\nstore HMAC + TTL")
auth >> Edge(label="compare hash") >> redis rabbit = Rabbitmq("RabbitMQ\npublish SMS job")
auth >> Edge(label="upsert user + session") >> mongo
auth >> Edge(label="token pair") >> demo >> user 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: def render_refresh_flow() -> None:
with Diagram( with diagram("Refresh Token Rotation", "refresh_rotation_flow"):
"Refresh Token Rotation", client = Client("Client")
filename=str(OUT / "refresh_rotation_flow"), auth = Server("auth-service\nRefreshToken")
show=False, mongo = Mongodb("MongoDB\nrefresh_sessions")
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")
client >> Edge(label="refresh token") >> auth with Cluster("Rotation result", graph_attr=cluster_attr()):
auth >> Edge(label="hash lookup") >> mongo old = Vault("Old hash\nrevoked")
mongo >> Edge(label="active session") >> auth new = Vault("New hash\nactive")
auth >> old
auth >> new client >> link("1. opaque refresh token") >> auth
new >> Edge(label="new token pair") >> client 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: def render_sms_strategy() -> None:
with Diagram( with diagram("SMS Provider Strategy", "sms_provider_strategy", direction="TB"):
"SMS Provider Strategy", worker = Docker("sms-worker")
filename=str(OUT / "sms_provider_strategy"), factory = Server("create_sms_client()\nprovider factory")
show=False, port = Blank("SmsClient port\nsend_otp(...)")
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")
worker >> factory >> port with Cluster("Concrete strategies", graph_attr=cluster_attr()):
port >> kav kavenegar = Internet("Kavenegar")
port >> smsir sms_ir = Internet("SMS.ir")
port >> debug 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: def render_production_deployment() -> None:
with Diagram( with diagram("Production Deployment", "production_deployment"):
"Production Deployment", internet = Internet("Internet\ngapido.amiirkhl.ir")
filename=str(OUT / "production_deployment"), caddy = Caddy("Caddy\n80 / 443 only")
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")
internet >> Edge(label="gapido.amiirkhl.ir") >> caddy >> demo >> auth with Cluster("Private Docker network", graph_attr=cluster_attr()):
auth >> private demo = Docker("demo-app\nFastAPI UI")
private >> mongo auth = Server("auth-service\nprivate gRPC")
private >> redis mongo = Mongodb("MongoDB\nprivate")
private >> rabbit >> worker 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: def main() -> None:
"""Regenerate all 16:9 icon-based diagrams for docs and slides."""
OUT.mkdir(parents=True, exist_ok=True) OUT.mkdir(parents=True, exist_ok=True)
render_service_architecture() render_service_architecture()
render_clean_architecture() render_clean_architecture()
@@ -212,4 +414,3 @@ def main() -> None:
if __name__ == "__main__": if __name__ == "__main__":
main() main()