Source code for dae.genomic_resources.resource_implementation

from __future__ import annotations

import logging
import textwrap
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, cast

from cerberus import Validator
from jinja2 import Template
from markdown2 import markdown

from dae.task_graph.graph import Task, TaskGraph
from dae.utils.helpers import convert_size

from .repository import GenomicResource

logger = logging.getLogger(__name__)


[docs] def get_base_resource_schema() -> dict[str, Any]: return { "type": {"type": "string"}, "meta": { "type": "dict", "allow_unknown": True, "schema": { "description": {"type": "string"}, "labels": {"type": "dict", "nullable": True}, }, }, }
[docs] class ResourceStatistics: """ Base class for statistics. Subclasses should be created using mixins defined for each statistic type that the resource contains. """ def __init__(self, resource_id: str): self.resource_id = resource_id
[docs] @staticmethod def get_statistics_folder() -> str: return "statistics"
[docs] class GenomicResourceImplementation(ABC): """ Base class used by resource implementations. Resources are just a folder on a repository. Resource implementations are classes that know how to use the contents of the resource. """ def __init__(self, genomic_resource: GenomicResource): self.resource = genomic_resource self.config: dict = self.resource.get_config() self._statistics: ResourceStatistics | None = None @property def resource_id(self) -> str: return self.resource.resource_id
[docs] def get_config(self) -> dict: return self.config
@property def files(self) -> set[str]: """Return a list of resource files the implementation utilises.""" return set()
[docs] @abstractmethod def calc_statistics_hash(self) -> bytes: """ Compute the statistics hash. This hash is used to decide whether the resource statistics should be recomputed. """ raise NotImplementedError
[docs] @abstractmethod def add_statistics_build_tasks(self, task_graph: TaskGraph, **kwargs: Any) -> list[Task]: """Add tasks for calculating resource statistics to a task graph.""" raise NotImplementedError
[docs] @abstractmethod def calc_info_hash(self) -> bytes: """Compute and return the info hash.""" raise NotImplementedError
[docs] @abstractmethod def get_info(self, **kwargs: Any) -> str: """Construct the contents of the implementation's HTML info page.""" raise NotImplementedError
[docs] def get_statistics(self) -> ResourceStatistics | None: """Try and load resource statistics.""" return None
[docs] def reload_statistics(self) -> ResourceStatistics | None: self._statistics = None return self.get_statistics()
[docs] class InfoImplementationMixin: """Mixin that provides generic template info page generation interface.""" resource: GenomicResource
[docs] def get_template(self) -> Template: return Template(textwrap.dedent(""" {% extends base %} {% block content %} {% endblock %} """))
def _get_template_data(self) -> dict: return {}
[docs] def get_template_data(self) -> dict: """ Return a data dictionary to be used by the template. Will transform the description in the meta section using markdown. """ template_data = self._get_template_data() @dataclass class FileEntry: """Provides an entry into manifest object.""" name: str size: str md5: str | None template_data["resource_files"] = [ FileEntry(entry.name, convert_size(entry.size), entry.md5) for entry in self.resource.get_manifest().entries.values() if not entry.name.startswith("statistics") and entry.name != "index.html"] template_data["resource_files"].append( FileEntry("statistics/", "", "")) return template_data
[docs] def get_info(self) -> str: """Construct the contents of the implementation's HTML info page.""" template_data = self.get_template_data() return self.get_template().render( resource=self.resource, markdown=markdown, data=template_data, base=RESOURCE_TEMPLATE, )
[docs] class ResourceConfigValidationMixin: """Mixin that provides validation of resource configuration."""
[docs] @staticmethod @abstractmethod def get_schema() -> dict: """Return schema to be used for config validation.""" raise NotImplementedError
[docs] @classmethod def validate_and_normalize_schema( cls, config: dict, resource: GenomicResource) -> dict: """Validate the resource schema and return the normalized version.""" # pylint: disable=not-callable validator = Validator(cls.get_schema()) if not validator.validate(config): logger.error( "Resource %s of type %s has an invalid configuration. %s", resource.resource_id, resource.get_type(), validator.errors) raise ValueError(f"Invalid configuration: {resource.resource_id}") return cast(dict, validator.document)
RESOURCE_TEMPLATE = Template(""" <html> <head> <style> h3,h4 { margin-top:0.5em; margin-bottom:0.5em; } table { border-collapse: collapse; } th, td { padding: 10px; } th { font-size: 20px; } td { font-size: 18px; } {% block extra_styles %}{% endblock %} </style> </head> <body> <h1>Resource</h1> <div> <table border="1"> <tr> <td> <b>Id:</b> </td> <td>{{ resource.resource_id }}</td> </tr> <tr> <td> <b>Type:</b> </td> <td>{{ resource.get_type() }}</td> </tr> <tr> <td> <b>Version:</b> </td> <td>{{ resource.get_version_str() }}</td> </tr> <tr> <td><b>Summary:</b></td> <td> <div> <template shadowrootmode="open"> {%- set summary = resource.get_summary() -%} {{ markdown(summary, extras=["tables"]) if summary else "N/A" }} </template> </div> </td> </tr> <tr> <td> <b>Description:</b> </td> <td> <div> <template shadowrootmode="open"> {%- set description = resource.get_description() -%} {{ markdown(description, extras=["tables"]) if description else "N/A" }} </template> </div> </td> </tr> <tr> <td> <b>Labels:</b> </td> <td> {% if resource.get_labels() %} <ul> {% for label, value in resource.get_labels().items() %} <li>{{ label }}: {{ value }}</li> {% endfor %} </ul> {% endif %} </td> </tr> </table> </div> {% block content %} N/A {% endblock %} <h1>Files</h1> <table> <thead> <tr> <th>Filename</th> <th>Size</th> <th>md5</th> </tr> </thead> <tbody> {%- for entry in data["resource_files"] %} <tr> <td class="nowrap"> <a href='{{entry.name}}'>{{entry.name}}</a> </td> <td class="nowrap">{{entry.size}}</td> <td class="nowrap">{{entry.md5}}</td> </tr> {%- endfor %} </tbody> </table> </body> </html> """) # noqa: E501