glowtables/glowtables/views.py

159 lines
4.6 KiB
Python

# 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 <http://www.gnu.org/licenses/>.
"""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/<table_id>/")
@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())
),
),
),
)