Skip to content

Commit

Permalink
Render system card
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristopherSpelt committed Sep 12, 2024
1 parent 9106c7b commit ea7763b
Show file tree
Hide file tree
Showing 13 changed files with 1,071 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ repos:
exclude: ^amt/static/vendor/.*
- id: trailing-whitespace
- id: check-yaml
exclude: example/
exclude: ^(example|example_system_card)/
- id: check-json
exclude: 'tsconfig\.json'
- id: check-added-large-files
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ RUN pyright
FROM development AS test

COPY ./example/ ./example/
COPY ./example_system_card/ ./example_system_card/
RUN npm run build

FROM project-base AS production
Expand Down
124 changes: 123 additions & 1 deletion amt/api/routes/project.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from typing import Annotated
from pathlib import Path
from typing import Annotated, Any

from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
Expand All @@ -8,6 +9,7 @@
from amt.core.exceptions import NotFound
from amt.enums.status import Status
from amt.services.projects import ProjectsService
from amt.services.storage import FileSystemStorageService
from amt.services.tasks import TasksService

router = APIRouter()
Expand Down Expand Up @@ -35,3 +37,123 @@ async def get_root(
raise NotFound()

return templates.TemplateResponse(request, "pages/index.html.j2", context)


# !!!
# Implementation of this endpoint is for now independent of the project ID, meaning
# that the same system card is rendered for all project ID's. This is due to the fact
# that the logical process flow of a system card is not complete.
# !!!
@router.get("/{project_id}/system_card")
async def get_system_card(
request: Request,
project_id: int,
projects_service: Annotated[ProjectsService, Depends(ProjectsService)],
file_system_storage_service: Annotated[FileSystemStorageService, Depends(FileSystemStorageService)],
) -> HTMLResponse:
logger.info(f"getting project with id {project_id}")
project = projects_service.get(project_id)
if not project:
logger.warning(f"project with id {project_id} not found")
raise NotFound()

file_system_storage_service.base_dir = Path("example_system_card/")
file_system_storage_service.filename = "system_card.yaml"
system_card_data = file_system_storage_service.read(autoload=False)

context = {"system_card": system_card_data, "project_id": project.id}

return templates.TemplateResponse(request, "pages/system_card.html.j2", context)


# !!!
# Implementation of this endpoint is for now independent of the project ID, meaning
# that the same system card is rendered for all project ID's. This is due to the fact
# that the logical process flow of a system card is not complete.
# !!!
@router.get("/{project_id}/system_card/assessments/{assessment_card}")
async def get_assessment_card(
request: Request,
project_id: int,
assessment_card: str,
projects_service: Annotated[ProjectsService, Depends(ProjectsService)],
) -> HTMLResponse:
logger.info(f"getting project with id {project_id}")
project = projects_service.get(project_id)
if not project:
logger.warning(f"project with id {project_id} not found")
raise NotFound()

assessment_card_data = get_include_contents(
Path("example_system_card/"), "system_card.yaml", "assessments", assessment_card
)

if not assessment_card_data:
logger.warning("assessment card not found")
raise NotFound()

context = {"assessment_card": assessment_card_data}

return templates.TemplateResponse(request, "pages/assessment_card.html.j2", context)


# !!!
# Implementation of this endpoint is for now independent of the project ID, meaning
# that the same system card is rendered for all project ID's. This is due to the fact
# that the logical process flow of a system card is not complete.
# !!!
@router.get("/{project_id}/system_card/models/{model_card}")
async def get_model_card(
request: Request,
project_id: int,
model_card: str,
projects_service: Annotated[ProjectsService, Depends(ProjectsService)],
) -> HTMLResponse:
logger.info(f"getting project with id {project_id}")
project = projects_service.get(project_id)
if not project:
logger.warning(f"project with id {project_id} not found")
raise NotFound()

model_card_data = get_include_contents(Path("example_system_card/"), "system_card.yaml", "models", model_card)

if not model_card_data:
logger.warning("model card not found")
raise NotFound()

context = {"model_card": model_card_data}

return templates.TemplateResponse(request, "pages/model_card.html.j2", context)


def get_include_contents(base_dir: Path, system_card: str, include_name: str, search_key: str) -> dict[str, Any] | None:
"""
Searches for `search_key` in the `!include`'s in `system_card` and returns its contents if found.
Example:
base_dir = /
system_card = "system_card.yaml"
include_name = "assessments"
search_key = "iama"
Suppose the system_card.yaml contains the following:
```
assessments:
- !include assessments/iama.yaml
- !include assessments/aiia.yaml
```
Then the function would return the contents of `assessments/iama.yaml`.
"""
file_system_storage_service = FileSystemStorageService(base_dir, system_card)
system_card_data = file_system_storage_service.read(autoload=False)

for include in system_card_data[include_name]:
include_path = Path(include.urlpath)
if search_key in str(include_path):
file_system_storage_service.base_dir = base_dir / include_path.parent
file_system_storage_service.filename = include_path.name
logger.info(f"fetching contents from {include_path}")
return file_system_storage_service.read()

logger.warning("could not fetch contents")
return None
43 changes: 36 additions & 7 deletions amt/services/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,54 @@ def init(storage_type: str = "file", **kwargs: Unpack[WriterFactoryArguments]) -
class FileSystemStorageService(Storage):
def __init__(self, location: Path = Path("./tests/data"), filename: str = "system_card.yaml") -> None:
self.base_dir = location
if not filename.endswith(".yaml"):
raise ValueError(f"Filename {filename} must end with .yaml instead of .{filename.split('.')[-1]}")
self.filename = filename
self.path = self.base_dir / self.filename

def write(self, data: dict[str, Any]) -> None:
if not Path(self.base_dir).exists():
Path(self.base_dir).mkdir()
with open(self.path, "w") as f:
dump(data, f, default_flow_style=False, sort_keys=False)

def read(self) -> Any: # noqa
# todo: this probably has to be moved to 'global scope'
yaml.add_constructor("!include", yaml_include.Constructor(base_dir=self.base_dir))
def read(self, autoload: bool = True) -> Any: # noqa
yaml.add_constructor("!include", yaml_include.Constructor(base_dir=self.base_dir, autoload=autoload))
with open(self.path) as f:
return yaml.full_load(f)
data = yaml.full_load(f)
del yaml.Loader.yaml_constructors["!include"]
del yaml.FullLoader.yaml_constructors["!include"]
return data

def close(self) -> None:
"""
This method is empty because with the `with` statement in the writer, Python will already close the writer
after usage.
"""

@property
def base_dir(self) -> Path:
return self._base_dir

@base_dir.setter
def base_dir(self, location: Path) -> None:
self._base_dir = location

@property
def autoload(self) -> bool:
return self._autoload

@autoload.setter
def autoload(self, autoload: bool) -> None:
self._autoload = autoload

@property
def filename(self) -> str:
return self._filename

@filename.setter
def filename(self, filename: str) -> None:
if not filename.endswith(".yaml"):
raise ValueError(f"Filename {filename} must end with .yaml instead of .{filename.split('.')[-1]}")
self._filename = filename

@property
def path(self) -> Path:
return self._base_dir / self.filename
38 changes: 38 additions & 0 deletions amt/site/templates/macros/cards.html.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{% macro render_include_path(path) -%}
{{ path.urlpath.split("/")[-1].split(".")[0]}}
{%- endmacro %}

{% macro render_attribute(attribute) -%}
{{attribute.capitalize().replace("_", " ")}}
{%- endmacro %}

{% macro render_value(key, value, depth) -%}
{% if value.__class__.__name__ == 'dict'%}
<div class="{% if depth == 1 %}margin-bottom-large{% endif%}">
{% for subkey, subvalue in value.items() %}
<strong> {{render_attribute(subkey)}} </strong>: {{render_value(subkey, subvalue, depth+1)}}
{% if not loop.last %}<br \>{% endif %}
{% endfor %}
</div>
{% elif value.__class__.__name__ == 'list'%}
<div style="margin-left: {{ depth * 10}}px">
{% for item in value %}
{{render_value("", item, depth+1)}}
{% endfor %}
</div>
{% elif value.__class__.__name__ == 'Data' %}
{{ render_include_path(value) }}
{% else %}
{% if key == "uri" or key == "link" or key == "upl"%}
<a class="rvo-link rvo-link--normal" href="{{value}}">{{value}}</a>
{% elif key == "oin" %}
<a class="rvo-link rvo-link--normal" href="{{value}}">https://oinregister.logius.nl/oin-register/{{value}}</a>
{% elif key == "email" %}
<a class="rvo-link rvo-link--normal" href="mailto:{{value}}">{{value}}</a>
{% elif key == "urn" %}
<a class="rvo-link rvo-link--normal" href="{{value}}">https://minbzk.github.io/instrument-registry/instruments/urn/{{value}}</a>
{% else %}
{{ value }}
{% endif %}
{% endif %}
{%- endmacro %}
58 changes: 58 additions & 0 deletions amt/site/templates/pages/assessment_card.html.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{% extends 'layouts/base.html.j2' %}
{% import 'macros/cards.html.j2' as render with context %}

{% block content %}
<div class="rvo-layout-column rvo-layout-gap--md">
<h2 class="utrecht-heading-2 rvo-heading--no-margins">Assessment card</h2>
<span class="rvo-text"> Last updated 2 hours ago </span>

<div class="rvo-table--responsive">
<table class="rvo-table">
<thead class="rvo-table-head">
<tr class="rvo-table-row">
<th scope="col" class="rvo-table-header">Attribute</th>
<th scope="col" class="rvo-table-header">Value</th>
</tr>
</thead>
{% for key, value in assessment_card.items() %}
{% if key != "contents" %}
<tbody class="rvo-table-body">
<tr class="rvo-table-row">
<td class="rvo-table-cell"><strong>{{key.capitalize().replace("_", " ")}}</strong></td>
<td class="rvo-table-cell"> {{render.render_value(key, value,0)}} </td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>

<h3 class="utrecht-heading-3">Content</h3>
<div class="rvo-table--responsive">
<table class="rvo-table">
<thead class="rvo-table-head">
<tr class="rvo-table-row">
<th scope="col" class="rvo-table-header">Question</th>
<th scope="col" class="rvo-table-header">Answer</th>
</tr>
</thead>
{% for content in assessment_card["contents"]%}
<tbody class="rvo-table-body">
<tr class="rvo-table-row">
<td class="rvo-table-cell"><strong>{{content["question"]}}</strong></td>
<td class="rvo-table-cell">
{{content["answer"]}}
{% set keys = ["urn", "remarks", "authors", "timestamp"] %}
{% set metadata = {} %}
{% for key in keys %}
{% set _x=metadata.__setitem__(key, content[key])%}
{% endfor %}
{{ render.render_value("",metadata,0) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
54 changes: 54 additions & 0 deletions amt/site/templates/pages/model_card.html.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{% extends 'layouts/base.html.j2' %}
{% import 'macros/cards.html.j2' as render with context %}

{% block content %}
<div class="rvo-layout-column rvo-layout-gap--md">
<h2 class="utrecht-heading-2 rvo-heading--no-margins">Model card</h2>
<span class="rvo-text"> Last updated 2 hours ago </span>

<div class="rvo-table--responsive">
<table class="rvo-table">
<thead class="rvo-table-head">
<tr class="rvo-table-row">
<th scope="col" class="rvo-table-header">Attribute</th>
<th scope="col" class="rvo-table-header">Value</th>
</tr>
</thead>
{% for key, value in model_card.items() %}
{% if key != "model-index" %}
<tbody class="rvo-table-body">
<tr class="rvo-table-row">
<td class="rvo-table-cell"><strong>{{key.capitalize().replace("_", " ")}}</strong></td>
<td class="rvo-table-cell"> {{render.render_value(key, value,0)}} </td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>

{% for model in model_card["model-index"] %}
<h3 class="utrecht-heading-3">{{model["name"]}}</h3>
<div class="rvo-table--responsive">
<table class="rvo-table">
<thead class="rvo-table-head">
<tr class="rvo-table-row">
<th scope="col" class="rvo-table-header">Attribute</th>
<th scope="col" class="rvo-table-header">Value</th>
</tr>
</thead>
{% for key, value in model.items() %}
{% if key != "name" %}
<tbody class="rvo-table-body">
<tr class="rvo-table-row">
<td class="rvo-table-cell"><strong>{{key.capitalize().replace("_", " ")}}</strong></td>
<td class="rvo-table-cell"> {{render.render_value(key, value,0)}} </td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
{% endblock %}
Loading

0 comments on commit ea7763b

Please sign in to comment.