#!/usr/bin/env python3
import bottle
import datetime
import dateutil
import functools
import jinja2
import pyld
import re
import requests
from bottle import jinja2_template as template, request, response
from string import Template
# This only exists for exporting the bottle app object for a WSGI server such as Gunicorn
application = bottle.app()
# Directories to search for HTML templates
bottle.TEMPLATE_PATH = [ './templates' ]
def query(query_string, jsonld_frame=None):
"""
Send query to Fuseki via HTTP.
Return the query results.
"""
http_request = requests.post(
'http://localhost:7000/dokk',
data = { 'format': 'json', 'query': query_string})
results = http_request.json()
if jsonld_frame:
results = pyld.jsonld.frame(results, jsonld_frame, options={'omitGraph':False})
return results
def human_size(bytes, B=False):
"""
Convert a file size in bytes to a human friendly form.
This is only used in templates when showing file sizes.
"""
for unit in [ 'B' if B else '', 'K', 'M', 'G', 'T', 'P' ]:
if bytes < 1024: break
bytes = bytes / 1024
return '{}{}'.format(round(bytes), unit).rjust(5)
template = functools.partial(template, template_settings = {
'filters': {
'datetime': lambda date: dateutil.parser.parse(date).strftime('%b %-d, %Y - %H:%M%z%Z'),
'human_size': human_size
},
'globals': {
'now': lambda: datetime.datetime.now(datetime.timezone.utc),
'request': request,
'url': application.get_url,
},
'autoescape': True
})
@bottle.error(404)
def error404(error):
"""
Custom 404 page.
:param error: bottle.HTTPError given by Bottle when calling abort(404).
"""
return '[404] {}'.format(error.body)
@bottle.get('/static/<filename:path>', name='static')
def static(filename):
"""
Path for serving static files.
"""
return bottle.static_file(filename, root='./static/')
@bottle.get('/', name='homepage')
def homepage():
"""
"""
return template('index.html')
@bottle.get('/library', name='library')
def library():
"""
"""
data = query(
'''
PREFIX library: <dokk:library:>
PREFIX license: <dokk:license:>
CONSTRUCT {
?item library:title ?title;
library:author ?author ;
library:license ?license .
?license license:id ?license_id ;
license:name ?license_name .
}
WHERE {
?item library:title ?title ;
library:author ?author ;
library:license ?license .
OPTIONAL {
?license license:id ?license_id_optional ;
license:name ?license_name_optional .
}
BIND(COALESCE(?license_id_optional, SUBSTR(STR(?license), 14)) AS ?license_id)
BIND(COALESCE(?license_name_optional, SUBSTR(STR(?license), 14)) AS ?license_name)
}
ORDER BY UCASE(?title)
''',
{
'@context': {
'library': 'dokk:library:',
'license': 'dokk:license:',
'library:author': { '@container': '@set' },
'library:license': { '@container': '@set' }
},
'library:title': {}
})
return template('library.html', data=data)
@bottle.get('/library/<item_id>', name='library_item')
def library_item(item_id):
"""
"""
item_iri = '<dokk:library:' + item_id + '>'
try:
with open('../blob.dokk.org/pdf_to_text/' + item_id + '.txt', 'r') as file:
item_plaintext = file.read()
except:
item_plaintext = ''
data = query(f'''
PREFIX library: <dokk:library:>
PREFIX license: <dokk:license:>
CONSTRUCT {{
?item library:title ?title ;
library:author ?author ;
library:license ?license .
?license license:id ?license_id ;
license:name ?license_name .
}}
WHERE {{
?item
library:title ?title ;
library:author ?author ;
library:license ?license .
FILTER (?item = {item_iri})
OPTIONAL {{
?license license:id ?license_id_optional ;
license:name ?license_name_optional .
}}
BIND(COALESCE(?license_id_optional, SUBSTR(STR(?license), 14)) AS ?license_id)
BIND(COALESCE(?license_name_optional, SUBSTR(STR(?license), 14)) AS ?license_name)
}}
''',
{
'@context': {
'library': 'dokk:library:',
'license': 'dokk:license:',
'library:author': { '@container': '@set' },
'library:license': { '@container': '@set' }
},
'library:title': {}
})
return template('library_item.html', data=data['@graph'][0], item_id=item_id, plaintext=item_plaintext)
@bottle.get('/license', name='licenses')
def licenses():
"""
"""
data = query(
'''
PREFIX license: <dokk:license:>
SELECT *
WHERE {
?license license:id ?id ;
license:name ?name.
}
ORDER BY ASC(UCASE(?name))
''')
return template('licenses.html', data=data)
@bottle.get('/manpages', name='manpages')
def manpages():
"""
"""
data = query(
'''
PREFIX mp: <dokk:manpages:>
SELECT DISTINCT ?distribution ?version
WHERE {
[] mp:source [
mp:distribution_version ?version ;
mp:distribution_name ?distribution ;
] .
}
ORDER BY ?distribution ?version
''')
return template('manpages.html', data=data)
@bottle.get('/manpages/<distribution>/<version>', name='manpages_distribution')
def manpages_distribution(distribution, version):
"""
List packages by distribution.
"""
data = query(
f'''
PREFIX mp: <dokk:manpages:>
SELECT DISTINCT ?package
WHERE {{
?id mp:source [
mp:distribution_name "{distribution}" ;
mp:distribution_version "{version}" ;
mp:package ?package ;
] .
}}
ORDER BY ?package
''')
return template('manpages_distribution.html', data=data, distribution=distribution, version=version)
@bottle.get('/manpages/<distribution>/<version>/<package>', name='manpages_package')
def manpages_package(distribution, version, package):
"""
List manpages by package.
"""
data = query(
f'''
PREFIX mp: <dokk:manpages:>
SELECT ?id ?filename
WHERE {{
?id mp:source [
mp:package "{package}" ;
mp:distribution_version "{version}" ;
mp:distribution_name "{distribution}" ;
mp:filename ?filename ;
] .
}}
ORDER BY ?filename
''')
return template('manpages_package.html', data=data, distribution=distribution, version=version, package=package)
@bottle.get('/manpages/<distribution>/<version>/<package>/<name>.<section>.<lang>', name='manpages_page')
def manpages_page(distribution, version, package, name, section, lang):
"""
Display a single manpage.
"""
page_id = f'<dokk:manpages:{distribution}/{version}/{package}/{name}.{section}.{lang}>'
# These characters where url-encoded when creating the RDF data because they
# make invalid URIs. This means for example that in the graph the URIs will be
# encoded with %23 instead of #
page_id = page_id.replace(' ', '_') \
.replace('[', '%5B') \
.replace(']', '%5D') \
.replace('#', '%23')
data = query(
f'''
PREFIX mp: <dokk:manpages:>
DESCRIBE {page_id}
''',
{
'@context': {
'mp': 'dokk:manpages:',
},
'mp:source': {},
})
# Replace references to other manpages with links.
# Manpages have a section "See also" at the bottom where they suggest other pages.
# For example:
# SEE ALSO
# cdk(3), cdk_screen(3), cdk_display(3), cdk_binding(3), cdk_util(3)
# We convert these strings to links to other pages.
# Example: ls(1) -> <a href="...">ls(1)</a>
# regex explanation:
# - some manpages use a bold or italic name, so we match an optional <b>
# <i> tag
# - match valid characters for a manpage, assign name <page>
# - match optional closing </em> or </strong> tags
# - match '('
# - match number, assign name <section>
# - match ')'
html = data['@graph'][0]['mp:html']
html = re.sub (
'(?:<b>|<i>)?(?P<page>[\w_.:-]+)(?:</b>|</i>)?\((?P<section>[0-9]\w*)\)',
lambda match:
f'''<a href="{application.get_url('manpages_disambiguation',
name=match.group('page'), section=match.group('section')).lower()}"
>{match.group('page')}({match.group('section')})</a>''',
html)
data['@graph'][0]['mp:html'] = html
return template('manpage.html', data=data['@graph'][0], distribution=distribution,
version=version, package=package, name=name, section=section, lang=lang)
@bottle.get('/manpages/<name>.<section>', name='manpages_disambiguation')
def manpages_disambiguation(name, section):
"""
Show a list of manpages that match <name> and <section>
"""
data = query(
f'''
PREFIX mp: <dokk:manpages:>
DESCRIBE ?page
WHERE {{
?page mp:name_lowercase "{name}" ;
mp:section_lowercase "{section}" .
}}
''',
{
'@context': {
'mp': 'dokk:manpages:',
},
'mp:source': {},
})
return template('manpage_disambiguation.html', name=name, section=section, data=data)
@bottle.get('/license/<id>', name='license')
def license(id):
"""
"""
license_iri = 'license:' + id
data = query(Template(
'''
PREFIX license: <dokk:license:>
DESCRIBE $license
''').substitute(license=license_iri))
return template('license.html', data=data)
@bottle.get('/mimi_and_eunice', name='mimi_and_eunice')
def mimi_and_eunice():
"""
"""
data = query(
'''
PREFIX mimi_eunice: <dokk:mimi_and_eunice:>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
SELECT ?number ?title
WHERE {
?id mimi_eunice:title ?title .
BIND (SUBSTR(STR(?id), 22) as ?number)
}
ORDER BY ?number
''')
return template('mimi_and_eunice.html', data=data)
@bottle.get('/mimi_and_eunice/<number>', name='mimi_and_eunice_strip')
def mimi_and_eunice_strip(number):
"""
"""
iri = '<dokk:mimi_and_eunice:' + number + '>'
data = query(Template(
'''
PREFIX license: <dokk:license:>
PREFIX mimi_eunice: <dokk:mimi_and_eunice:>
DESCRIBE $iri ?license
WHERE {
$iri mimi_eunice:license ?license
}
''').substitute(iri=iri),
{
'@context': {
'license': 'dokk:license:',
'mimi_eunice': 'dokk:mimi_and_eunice:',
'mimi_eunice:tag': { '@container': '@set' },
'mimi_eunice:transcript': { '@container': '@set' }
},
'mimi_eunice:license': {},
'mimi_eunice:transcript': {
'mimi_eunice:order': {}
}
})
return template('mimi_and_eunice_strip.html', data=data['@graph'][0])
@bottle.get('/articles', name='articles')
def articles():
"""
"""
data = query(
'''
PREFIX : <dokk:articles:>
SELECT ?iri ?title
WHERE {
?iri :title ?title
}
ORDER BY ?title
''')
return template('articles.html', data=data)
@bottle.get('/<id>', name='article')
def article(id):
"""
"""
data = query(
f'''
PREFIX : <dokk:articles:>
DESCRIBE <dokk:articles:{id}>
''',
{
'@context': {
'a': 'dokk:articles:'
}
})
if '@graph' not in data or len(data['@graph']) < 1:
bottle.abort(404, "Article doesn't exist.")
return template('article.html', data=data['@graph'][0])