# This file is part of the Glowtables software # Copyright (C) 2023 Valentin Lorentz # # This program is free software: you can redistribute it and/or modify it under the # terms of the GNU Affero General Public License version 3, as published by the # Free Software Foundation. # # This program is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A # PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License along with # this program. If not, see . """Minimal webapp to display Glowtables""" import functools import importlib.metadata import importlib.resources import logging import operator import xml.etree.ElementTree as ET from typing import Callable, List, TypeVar import flask from .cache import Cache from .shortxml import Namespace from .sparql import RemoteSparqlBackend from .table import Language, Table HTML = Namespace("http://www.w3.org/1999/xhtml") LANG = Language("en") # TODO: configurable logger = logging.getLogger(__name__) app = flask.Flask(__name__) TView = TypeVar("TView", bound=Callable) def _sparql_backend() -> RemoteSparqlBackend: return RemoteSparqlBackend( "https://query.wikidata.org/sparql", agent="Unconfigured Glowtable instance", cache=Cache("file:sparql_cache.sqlite3"), ) def xhtml_view(f: TView) -> TView: """Decorator for Flask views which may return XHTML as :mod:`xml.etree.ElementTree` objects.""" @functools.wraps(f) def newf(*args, **kwargs): res = f(*args, **kwargs) if isinstance(res, (ET.Element, ET.ElementTree)): xml = ET.tostring(res, default_namespace=HTML.uri) return flask.Response(xml, mimetype="application/xhtml+xml") else: return res return newf # type: ignore[return-value] def list_tables() -> List[Table]: """Returns all :class:`Table` instances registered as ``glowtables.tables`` entrypoints.""" table_entrypoints: List[ importlib.metadata.EntryPoint ] = importlib.metadata.entry_points( # type: ignore[call-arg,assignment] group="glowtables.tables" ) tables = [] for table_entrypoint in sorted(table_entrypoints, key=operator.attrgetter("name")): table = table_entrypoint.load() if not isinstance(table, Table): logger.error( "%s is %r, which is not an instance of glowtables.table.Table", table_entrypoint.name, table, ) continue tables.append(table) return tables @app.route("/") @xhtml_view def index() -> ET.Element: """Displays the list of tables.""" tables = list_tables() return HTML.html( HTML.head( HTML.title("Glowtables"), HTML.link(rel="stylesheet", type="text/css", href="/style.css"), ), HTML.body( HTML.h1("Glowtables"), HTML.ul( [ HTML.li( HTML.a(table.display_names[LANG], href=f"/tables/{table.id}/") ) for table in tables ] ) if tables else HTML.p( """ There are no tables defined, check the Glowtables documentation to find how to configure them. """ ), ), ) @app.route("/style.css") def style() -> flask.Response: """Serves the CSS.""" css = importlib.resources.files(__package__).joinpath("style.css").read_bytes() return flask.Response(css, mimetype="text/css") @app.route("/tables//") @xhtml_view def table_(table_id: str) -> ET.Element: """Displays a table.""" tables = list_tables() for table in tables: if table.id == table_id: break else: flask.abort(404) return HTML.html( HTML.head( HTML.title("Glowtables"), HTML.link(rel="stylesheet", type="text/css", href="/style.css"), ), HTML.body( HTML.h1(table.display_names[LANG]), HTML.table( HTML.thead( HTML.tr( HTML.th(field.display_names[LANG]) for field in table.fields ) ), HTML.tbody( HTML.tr(HTML.td(cell) for cell in row) for row in table.query(_sparql_backend()) ), ), ), )