Skip to content

Commit

Permalink
refactor: Move state to a separate module
Browse files Browse the repository at this point in the history
Maintaining the application state in a separate module makes it possible
to make use (`mpypy`) of type hints for state variables.
  • Loading branch information
jamilraichouni committed Jan 15, 2025
1 parent 14717a5 commit f0d8e03
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 69 deletions.
121 changes: 52 additions & 69 deletions capella_model_explorer/backend/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

# Copyright InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import logging
import os
import pathlib
Expand All @@ -19,11 +22,10 @@
import jinja2
import markupsafe
import prometheus_client
import prometheus_client.registry
import pydantic

import capella_model_explorer
from capella_model_explorer.backend import model_diff
from capella_model_explorer.backend import model_diff, state
from capella_model_explorer.backend import templates as tl

FRONTEND_DIR = pathlib.Path("./frontend/dist")
Expand Down Expand Up @@ -63,18 +65,16 @@ def _create_diff_lookup(data, lookup=None):

def _finalize(markup: t.Any) -> object:
markup = markupsafe.escape(markup)
return capellambse.helpers.replace_hlinks(
markup, app.state.model, _make_href
)
return capellambse.helpers.replace_hlinks(markup, state.model, _make_href)


def _make_href(
obj: capellambse.model.ModelElement | capellambse.model.AbstractDiagram,
) -> str | None:
if app.state.templates_index is None:
if state.templates_index is None:
return None

for idx, template in app.state.templates_index.flat.items():
for idx, template in state.templates_index.flat.items():
if "type" in dir(template.scope):
clsname = template.scope.type
if obj.xtype.rsplit(":", 1)[-1] == clsname:
Expand All @@ -94,7 +94,7 @@ def _make_href_filter(obj: object) -> str | None:
raise TypeError(f"Expected a model object, got {obj!r}")

try:
app.state.model.by_uuid(obj.uuid)
state.model.by_uuid(obj.uuid)
except KeyError:
return "#"

Expand All @@ -104,12 +104,12 @@ def _make_href_filter(obj: object) -> str | None:
def _render_instance_page(template_text, base, object=None):
try:
# render the template with the object
template = app.state.env.from_string(template_text)
template = state.jinja_env.from_string(template_text)
rendered = template.render(
object=object,
model=app.state.model,
diff_data=app.state.diff,
object_diff=app.state.object_diff,
model=state.model,
diff_data=state.diff,
object_diff=state.object_diff,
)
return fastapi.responses.HTMLResponse(
content=rendered, status_code=200
Expand All @@ -128,7 +128,7 @@ def _render_instance_page(template_text, base, object=None):
'<p style="color:red">'
f"Unexpected error: {type(e).__name__}: {e}"
'</p><pre style="font-size:80%;overflow:scroll">'
f"object={object!r}\nmodel={app.state.model!r}"
f"object={object!r}\nmodel={state.model!r}"
f"\n\n{trace}"
"</pre>"
)
Expand Down Expand Up @@ -159,49 +159,36 @@ def _render_instance_page(template_text, base, object=None):
except KeyError as err:
raise SystemExit("MODEL environment variable is not set") from err

_MODEL: capellambse.MelodyModel
try:
_ = _MODEL
except NameError:
_MODEL = capellambse.loadcli(MODEL_INFO)
_ = state.model
except AttributeError:
state.model = capellambse.loadcli(MODEL_INFO)

app.state.model = _MODEL
app.state.templates = fastapi.templating.Jinja2Templates(
directory=FRONTEND_DIR
)
state.templates = fastapi.templating.Jinja2Templates(directory=FRONTEND_DIR)

app.state.templates_path = pathlib.Path(
state.templates_path = pathlib.Path(
os.getenv("TEMPLATES_DIR", str(TEMPLATES_DIR))
)
app.state.last_interaction = time.time()
app.state.diff = {}
app.state.object_diff = {}
app.state.templates_loader = tl.TemplateLoader(app.state.model)
app.state.env = jinja2.Environment(
loader=jinja2.FileSystemLoader(app.state.templates_path)
)
app.state.env.finalize = _finalize
app.state.env.filters["make_href"] = _make_href_filter
app.state.templates_index = app.state.templates_loader.index_path(
app.state.templates_path
)
app.state.idle_time_gauge = prometheus_client.Gauge(
"idletime_minutes",
"Time in minutes since the last user interaction",
state.templates_loader = tl.TemplateLoader(state.model)
state.jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(state.templates_path)
)
state.jinja_env.finalize = _finalize
state.jinja_env.filters["make_href"] = _make_href_filter
state.templates_index = state.templates_loader.index_path(state.templates_path)


@app.middleware("http")
async def update_last_interaction_time(request: fastapi.Request, call_next):
if request.url.path not in ("/metrics", "/favicon.ico"):
app.state.last_interaction = time.time()
state.last_interaction = time.time()
return await call_next(request)


@app.get("/metrics")
def metrics():
idle_time_minutes = (time.time() - app.state.last_interaction) / 60
app.state.idle_time_gauge.set(idle_time_minutes)
idle_time_minutes = (time.time() - state.last_interaction) / 60
state.idle_time_gauge.set(idle_time_minutes)
return fastapi.Response(
content=prometheus_client.generate_latest(),
media_type="text/plain",
Expand All @@ -211,21 +198,19 @@ def metrics():
@app.router.get("/api/commits")
async def get_commits():
try:
return model_diff.populate_commits(app.state.model)
return model_diff.populate_commits(state.model)
except Exception as e:
return {"error": str(e)}


@app.router.post("/api/compare")
async def post_compare(commit_range: CommitRange):
try:
app.state.diff = model_diff.get_diff_data(
app.state.model, commit_range.head, commit_range.prev
state.diff = model_diff.get_diff_data(
state.model, commit_range.head, commit_range.prev
)
app.state.diff["lookup"] = _create_diff_lookup(
app.state.diff["objects"]
)
if app.state.diff["lookup"]:
state.diff["lookup"] = _create_diff_lookup(state.diff["objects"])
if state.diff["lookup"]:
return {"success": True}
return {"success": False, "error": "No model changes to show"}
except Exception as e:
Expand All @@ -235,8 +220,8 @@ async def post_compare(commit_range: CommitRange):

@app.router.get("/api/diff")
async def get_diff():
if app.state.diff:
return app.state.diff
if state.diff:
return state.diff
return {"error": "No data available. Please compare two commits first."}


Expand All @@ -247,30 +232,30 @@ async def version():

@app.router.get("/api/model-info")
def model_info():
info = app.state.model.info
info = state.model.info
resinfo = info.resources["\x00"]
return {
"title": info.title,
"revision": resinfo.revision,
"hash": resinfo.rev_hash,
"capella_version": info.capella_version,
"branch": resinfo.branch,
"badge": app.state.model.description_badge,
"badge": state.model.description_badge,
}


@app.router.post("/api/object-diff")
async def post_object_diff(object_id: ObjectDiffID):
if object_id.uuid not in app.state.diff["lookup"]:
if object_id.uuid not in state.diff["lookup"]:
raise fastapi.HTTPException(status_code=404, detail="Object not found")

app.state.object_diff = app.state.diff["lookup"][object_id.uuid]
state.object_diff = state.diff["lookup"][object_id.uuid]
return {"success": True}


@app.router.get("/api/objects/{uuid}")
def read_object(uuid: str):
obj = app.state.model.by_uuid(uuid)
obj = state.model.by_uuid(uuid)
details = tl.simple_object(obj)
return {
"idx": details["idx"],
Expand All @@ -281,27 +266,27 @@ def read_object(uuid: str):

@app.router.get("/api/views")
def read_templates():
app.state.templates_index = app.state.templates_loader.index_path(
app.state.templates_path
state.templates_index = state.templates_loader.index_path(
state.templates_path
)
return app.state.templates_index.as_dict
return state.templates_index.as_dict


@app.router.get("/api/views/{template_name}")
def read_template(template_name: str):
template_name = urllib.parse.unquote(template_name)
if (
app.state.templates_index is None
or template_name not in app.state.templates_index.flat
state.templates_index is None
or template_name not in state.templates_index.flat
):
return {
"error": (
f"Template {template_name} not found"
" or templates index not initialized"
)
}
base = app.state.templates_index.flat[template_name]
base.compute_instance_list(app.state.model)
base = state.templates_index.flat[template_name]
base.compute_instance_list(state.model)
return base


Expand All @@ -312,14 +297,14 @@ def render_template(template_name: str, object_id: str):
try:
template_name = urllib.parse.unquote(template_name)
if (
app.state.templates_index is None
or template_name not in app.state.templates_index.flat
state.templates_index is None
or template_name not in state.templates_index.flat
):
return {"error": f"Template {template_name} not found"}
base = app.state.templates_index.flat[template_name]
base = state.templates_index.flat[template_name]
template_filename = base.template
# load the template file from the templates folder
content = (app.state.templates_path / template_filename).read_text(
content = (state.templates_path / template_filename).read_text(
encoding="utf8"
)
except Exception as e:
Expand All @@ -331,7 +316,7 @@ def render_template(template_name: str, object_id: str):
object = None
else:
try:
object = app.state.model.by_uuid(object_id)
object = state.model.by_uuid(object_id)
except Exception as e:
error_message = markupsafe.Markup(
"<p style='color:red'>Requested object not found: {}</p>"
Expand All @@ -344,6 +329,4 @@ def render_template(template_name: str, object_id: str):
@app.router.get("/{rest_of_path:path}")
async def catch_all(request: fastapi.Request, rest_of_path: str):
del rest_of_path
return app.state.templates.TemplateResponse(
"index.html", {"request": request}
)
return state.templates.TemplateResponse("index.html", {"request": request})
34 changes: 34 additions & 0 deletions capella_model_explorer/backend/state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

import time
import typing as t

import prometheus_client

if t.TYPE_CHECKING:
import pathlib

import capellambse
import fastapi.templating
import jinja2

import capella_model_explorer.backend.templates

diff: dict = {}
idle_time_gauge = prometheus_client.Gauge(
"idletime_minutes",
"Time in minutes since the last user interaction",
)
jinja_env: jinja2.Environment
last_interaction = time.time()
model: capellambse.MelodyModel
object_diff: dict = {}
templates: fastapi.templating.Jinja2Templates
templates_loader: capella_model_explorer.backend.templates.TemplateLoader
templates_index: (
capella_model_explorer.backend.templates.TemplateCategories | None
)
templates_path: pathlib.Path

0 comments on commit f0d8e03

Please sign in to comment.