# -*- coding: utf-8 -*-
"""
Renderers that take services "as arguments".
"""
#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL. See the
#c COPYING file in the source distribution.
import re
import urllib.request, urllib.parse, urllib.error
from twisted.web import resource
from twisted.web import template
from twisted.web.template import tags as T
from gavo import base
from gavo import formal
from gavo import registry
from gavo import svcs
from gavo import utils
from gavo.web import common
from gavo.web import grend
def _protectForBibTeX(tx):
"""returns tx in a way that hopefully prevents larger disasters
when used with BibTeX.
Among others, this just looks for multiple uppercase characters within
one word and protects the respective word with curly braces; for now,
this is ASCII only.
This is also where we escape for TeX.
"""
return re.sub(r"([#$_&\\%])", r"\\\1",
re.sub(r"(\w*[A-Z]\w+[A-Z]\w*)", r"{\1}", tx))
[docs]class RendExplainer(object):
"""is a container for various functions having to do with explaining
renderers on services.
Use the explain(renderer, service) class method.
"""
@classmethod
def _explain_form(cls, service):
return T.transparent["an interface for web browsers through an ",
T.a(href=service.getURL("form"))["HTML form"]]
@classmethod
def _explain_fixed(cls, service):
return T.transparent["a ",
T.a(href=service.getURL("fixed"))["custom page"],
", possibly with dynamic content"]
@classmethod
def _explain_volatile(cls, service):
return T.transparent["a ",
T.a(href=service.getURL("volatile"))["custom page"],
", possibly with dynamic content"]
@classmethod
def _explain_soap(cls, service):
def generateArguments():
# Slightly obfuscated -- I need to get ", " in between the items.
fieldIter = iter(service.getInputKeysFor("soap"))
try:
nextItem = next(fieldIter)
while True:
desc = "%s/%s"%(nextItem.name, nextItem.type)
if nextItem.required:
desc = T.strong[desc]
yield desc
nextItem = next(fieldIter)
yield ', '
except StopIteration:
pass
return T.transparent["enables remote procedure calls through the"
" slightly aged SOAP mechanism; to use it,"
" feed the WSDL URL "+
service.getURL("soap")+"/go?wsdl"+
" to your SOAP library; the function signature is"
" useService(",
generateArguments(),
"). See also our ",
T.a(render="rootlink", href="/static/doc/soaplocal.shtml")[
"local soap hints"]]
@classmethod
def _explain_custom(cls, service):
res = T.transparent["a custom rendering of the service, typically"
" for interactive web applications."]
if svcs.getRenderer("custom").isBrowseable(service):
res[" See also the ",
T.a(href=service.getURL("custom"))["entry page"], "."]
return res
@classmethod
def _explain_static(cls, service):
return T.transparent["static (i.e. prepared) data or custom client-side"
" code; probably used to access ancillary files here"]
@classmethod
def _explain_text(cls, service):
return T.transparent["a text interface not intended for user"
" applications"]
@classmethod
def _explain_siap_xml(cls, service):
return T.transparent["a standard SIAP interface as defined by the"
" IVOA to access collections of celestial images; SIAP clients"
" use ", service.getURL("siap.xml"), " to access the service",
T.transparent(render="ifadmin")[" – ",
T.a(href="http://voparis-validator.obspm.fr/validator.php?"
"spec=Simple+Image+Access+1.0"
"&format=XHTML&batch=yes"
+"&serviceURL=%s"%urllib.parse.quote(service.getURL("siap.xml"))
+"&POS=%s%%2C%s&SIZE=%s%%2C%s&FORMAT=ALL"%(
base.getMetaText(service, "testQuery.pos.ra", default="180"),
base.getMetaText(service, "testQuery.pos.dec", default="60"),
base.getMetaText(service, "testQuery.size.ra", default="3"),
base.getMetaText(service, "testQuery.size.dec", default="3")))[
"Validate"]]]
@classmethod
def _explain_siap2_xml(cls, service):
return T.transparent["a standard SIAP version 2 interface as defined by the"
" IVOA to access collections of celestial images; SIAP clients"
" use ", service.getURL("siap2.xml"), " to access the service",
T.transparent(render="ifadmin")[" – ",
T.a(href="http://voparis-validator.obspm.fr/validator.php?"
"spec=Simple+Image+Access+2.0"
"&serviceURL=%s"%urllib.parse.quote(service.getURL("siap2.xml"))
+"&FOV=&BAND=&TIME=&POL=&SPATRES=&EXPTIME=&ID=&COLLECTION=&FACILITY=&INSTRUMENT=&DPTYPE=image&CALIB=&TARGET=&TIMERES=&SPECRP=&FORMAT=&MAXREC=&format=XHTML&REQUEST=query"
+"&POS=CIRCLE+{}+{}+{}".format(
base.getMetaText(service, "testQuery.pos.ra", default="180"),
base.getMetaText(service, "testQuery.pos.dec", default="60"),
base.getMetaText(service, "testQuery.size.ra", default="3")))[
"Validate"]]]
@classmethod
def _explain_scs_xml(cls, service):
return T.transparent["a standard SCS interface as defined by the"
" IVOA to access catalog-type data; SCS clients"
" use ", service.getURL("scs.xml"), " to access the service",
T.transparent(render="ifadmin")[" – ",
T.a(href="http://voparis-validator.obspm.fr/validator.php?"
"spec=Simple+Cone+Search+1.03"
"&format=XHTML&batch=yes"
+"&serviceURL=%s"%urllib.parse.quote(service.getURL("scs.xml"))
+"&RA=%s&DEC=%s&SR=%s&VERB=3"%(
base.getMetaText(service, "testQuery.ra", default="180"),
base.getMetaText(service, "testQuery.dec", default="60"),
base.getMetaText(service, "testQuery.sr", default="1")))[
"Validate"]]]
@classmethod
def _explain_ssap_xml(cls, service):
return T.transparent["a standard SSAP interface as defined by the"
" IVOA to access spectral or time-series data; SSAP clients"
" use ", service.getURL("ssap.xml"), " to access the service",
T.transparent(render="ifadmin")[" – ",
T.a(href="http://voparis-validator.obspm.fr/validator.php?"
"REQUEST=queryData&POS=&SIZE=&TIME=&BAND=&FORMAT=ALL"
"&VERSION=1.1&APERTURE=&SPECRP=&SPATRES=&TIMERES=&SNR="
"&REDSHIFT=&VARAMPL=&TARGETNAME=&TARGETCLASS=&FLUXCALIB=any"
"&WAVECALIB=any&PUBDID=&CREATORDID=&COLLECTION=&TOP=&MAXREC=1"
"&MTIME=&COMPRESS=true&RUNID=&spec=Simple+Spectral+Access+1.1"
"&format=XHTML"
+"&serviceURL=%s"%urllib.parse.quote(service.getURL("ssap.xml")))[
"Validate"]]]
@classmethod
def _explain_slap_xml(cls, service):
return T.transparent["a standard SLAP interface as defined by the"
" IVOA to access spectral line data; SLAP clients (usually"
" spectral analysis programs)"
" use ", service.getURL("slap.xml"), " to access the service",
T.transparent(render="ifadmin")[" – ",
T.a(href="http://voparis-validator.obspm.fr/validator.php?"
"REQUEST=queryData&spec=Simple+Line+Access+1.0"
"&format=XHTML"
"&addparams=MAXREC%3D1%26"
+"&serviceURL=%s"%urllib.parse.quote(service.getURL("slap.xml")))[
"Validate"]]]
@classmethod
def _explain_dali(cls, service):
return T.transparent["a complex interface consisting of multiple"
" endpoints (sync, async, perhaps tables or examples, and possibly"
" more). This is used, in particular, for TAP. You will usually need"
" a client programme to use this."
" In such clients, you can find this service by its"
" IVOID, ",
T.code(render="meta")["identifier"],
", or access it by entering its base URL ",
T.code[service.getURL("dali")],
" directly. Using an XSL-enabled web browser you can, in a pinch,"
" also operate ",
T.a(href=service.getURL("async"))["the service"],
" asynchronously without a specialized client."]
@classmethod
def _explain_uws_xml(cls, service):
return T.transparent["a user-defined UWS service."
" This service is best accessed using specialized clients"
" or libraries. Give those its base URL ",
T.a(href=service.getURL("uws.xml"))[service.getURL("uws.xml")],
". Using an XSL-enabled web browser you can"
" also click on the link above and operate the service 'manually'."
" For parameters and the output schema, see below."]
@classmethod
def _explain_pubreg_xml(cls, service):
return T.transparent["an interface for the OAI-PMH protocol, typically"
" this site's publishing registry (but possibly some other"
" registry-like thing). This endpoint is usually accessed"
" by harvesters, but with an XML-enabled browser you can"
" also try the access URL at ",
T.a(href=service.getURL("pubreg.xml"))[service.getURL("pubreg.xml")],
"."]
@classmethod
def _explain_qp(cls, service):
return T.transparent["an interface that uses the last path element"
" to query the column %s in the underlying table."%
service.getProperty("queryField", "defunct")]
@classmethod
def _explain_upload(cls, service):
return T.transparent["a ",
T.a(href=service.getURL("upload"))["form-based interface"],
" for uploading data"]
@classmethod
def _explain_dlget(cls, service):
return T.transparent["a datalink interface letting specialized clients"
" retrieve parts of datasets or discover related data. You"
" use this kind of service exclusively in combination with"
" a pubdid, usually via a direct link."]
@classmethod
def _explain_dlmeta(cls, service):
return T.transparent["a datalink interface for discovering access"
" options (processed data, related datasets...) for a dataset."
" You usually need a publisherDID to use this kind of service."
" For special applications, the base URL of this service might"
" still come handy: %s"%service.getURL("dlmeta")]
@classmethod
def _explain_dlasync(cls, service):
return T.transparent["an asynchronous interface to retrieving"
" processed data. This needs a special client that presumably"
" would first look at the dlmeta endpoint to discover what"
" processing options are available."]
@classmethod
def _explain_api(cls, service):
return T.transparent["an interface for operation with curl and"
" similar low-level tools. The endpoint is at ",
T.a(href=service.getURL("api"))[service.getURL("api")],
"; as usual for DALI-conforming services, parameters"
" an response structure is available by ",
T.a(href=service.getURL("api")+"MAXREC=0")["querying with"
" MAXREC=0"],
"."]
@classmethod
def _explain_coverage(cls, service):
return T.transparent["an interface to retrieve the spatial coverage"
" of this service. By default, this will return a FITS MOC,"
" but browsers and similar clients declaring they accept PNGs"
" will get a sky plot showing the coverage."]
@classmethod
def _explain_examples(cls, service):
return T.transparent["a list of examples for"
" how to use this resource. You can read ",
T.a(href=service.getURL("examples"))["the examples"],
" with your browser. RDFa-aware clients can figure out the"
" examples at ",
service.getURL("api")]
@classmethod
def _explain_hips(cls, service):
return T.transparent["a HiPS endpoint for fast zooming and panning"
" through in particular images. Paste the access URL ",
T.a(href=service.getURL("hips"))[service.getURL("hips")],
" into a HiPS client or click it when you have a javascript-"
"enabled browser."]
@classmethod
def _explainEverything(cls, service):
return T.transparent["a renderer with some custom access method that"
" should be mentioned in the service description"]
[docs] @classmethod
def explain(cls, renderer, service):
return getattr(cls, "_explain_"+renderer.replace(".", "_"),
cls._explainEverything)(service)
[docs]class ServiceInfoRenderer(MetaRenderer, utils.IdManagerMixin):
"""A renderer showing all kinds of metadata on a service.
This renderer produces the default referenceURL page. To change its
appearance, override the serviceinfo.html template.
"""
name = "info"
customTemplate = svcs.loadSystemTemplate("serviceinfo.html")
def __init__(self, *args, **kwargs):
grend.ServiceBasedPage.__init__(self, *args, **kwargs)
self.describingRD = self.service.rd
self.rawFootnotes = set()
[docs] @template.renderer
def title(self, request, tag):
return tag["Information on Service '%s'"%
base.getMetaText(self.service, "title")]
[docs] @template.renderer
def notebubble(self, request, tag):
if not tag.slotData["note"]:
return ""
id = tag.slotData["note"].tag
self.rawFootnotes.add(tag.slotData["note"])
return tag(href="#note-%s"%id)["Note %s"%id]
[docs] def data_internalpath(self, request, tag):
return "%s/%s"%(self.service.rd.sourceId, self.service.id)
[docs] def data_htmlOutputFields(self, request, tag):
res = [f.asInfoDict() for f in self.service.getCurOutputFields()]
res.sort(key=lambda val: val["name"].lower())
return res
[docs] def data_votableOutputFields(self, request, tag):
queryMeta = svcs.QueryMeta({"_FORMAT": "VOTable", "_VERB": 3})
res = [f.asInfoDict() for f in self.service.getCurOutputFields(queryMeta)]
res.sort(key=lambda val: val["verbLevel"])
return res
[docs] def data_rendAvail(self, request, tag):
return [{"rendName": rend,
"rendExpl": RendExplainer.explain(rend, self.service)}
for rend in self.service.allowed]
[docs] def data_publications(self, request, tag):
res = [{"sets": ",".join(p.sets), "render": p.render}
for p in self.service.publications
if p.sets and p.render not in registry.HIDDEN_RENDERERS]
return sorted(res, key=lambda v: v["render"])
[docs] def data_browserURL(self, request, tag):
return self.service.getBrowserURL()
[docs] def data_service(self, request, tag):
return self.service
defaultLoader = common.doctypedStan(
T.html[
T.head[
T.title["Missing Template"]],
T.body[
T.p["Infos are only available with a serviceinfo.html template"]]
])
[docs]class TableInfoRenderer(MetaRenderer):
"""A renderer for displaying table information.
Since tables don't necessarily have associated services, this
renderer cannot use a service to sit on. Instead, the table is
being passed in as as an argument. There's a built-in vanity tableinfo
that sits on //dc_tables#show using this renderer (it could really
sit anywhere else).
"""
name = "tableinfo"
customTemplate = svcs.loadSystemTemplate("tableinfo.html")
[docs] def render(self, request):
if not hasattr(self, "table"):
# _retrieveTableDef did not run, i.e., no tableName was given
raise svcs.UnknownURI(
"You must provide a table name to this renderer.")
self.macroPackage = self.table
self.metaCarrier = self.table
return super(TableInfoRenderer, self).render(request)
def _retrieveTableDef(self, tableName):
try:
self.tableName = tableName
self.table = registry.getTableDef(tableName)
self.describingRD = self.table.rd
except base.NotFoundError as msg:
raise base.ui.logOldExc(svcs.UnknownURI(str(msg)))
[docs] def data_forADQL(self, request, tag):
return self.table.adql
[docs] def data_fields(self, request, tag):
res = [f.asInfoDict() for f in self.table]
for d in res:
if d["note"]:
d["noteKey"] = d["note"].tag
if "alphaOrder" in request.strargs:
res.sort(key=lambda item: item["name"].lower())
return res
[docs] def data_internalpath(self, request, tag):
return "%s/%s"%(self.table.rd.sourceId, self.table.id)
[docs] @template.renderer
def title(self, request, tag):
return tag["Table information for '%s'"%self.tableName]
[docs] @template.renderer
def iftapinfo(self, request, tag):
"""renders the content if there was a tapinfo key somewhere in
the query string.
"""
if "tapinfo" in request.strargs:
return tag
else:
return ""
[docs] def data_tableDef(self, request, tag):
return self.table
[docs] def getChild(self, name, request):
self._retrieveTableDef(name.decode("utf-8"))
return self
defaultLoader = common.doctypedStan(
T.html[
T.head[
T.title["Missing Template"]],
T.body[
T.p["Infos are only available with a tableinfo.html template"]]
])
[docs]class TableNoteRenderer(MetaRenderer):
"""A renderer for displaying table notes.
It takes a schema-qualified table name and a note tag in the segments.
This does not use the underlying service, so it could and will run on
any service. However, you really should run it on __system__/dc_tables/show,
and there's a built-in vanity name tablenote for this.
"""
name = "tablenote"
[docs] def render(self, request):
if not hasattr(self, "noteTag"):
# _retrieveTableDef did not run, i.e., no tableName was given
raise svcs.UnknownURI(
"You must provide table name and note tag to this renderer.")
return super(TableNoteRenderer, self).render(request)
def _retrieveNote(self, tableName, noteTag):
try:
table = registry.getTableDef(tableName)
self.metaCarrier = table
self.renderedNote = table.getNote(noteTag
).getContent(targetFormat="html", macroPackage=table)
except base.NotFoundError as msg:
raise base.ui.logOldExc(svcs.UnknownURI(msg))
self.noteTag = noteTag
self.tableName = tableName
[docs] def getChild(self, name, request):
segments = request.popSegments(name)
if len(segments)==2:
self._retrieveNote(segments[0], segments[1])
elif len(segments)==3: # segments[0] may be anything,
# but conventionally "inner"
self._retrieveNote(segments[1], segments[2])
self.loader = self.innerLoader
else:
raise svcs.UnknownURI("No such table note")
return self
[docs] def data_tableName(self, request, tag):
return self.tableName
[docs] def data_noteTag(self, request, tag):
return self.noteTag
[docs] @template.renderer
def noteHTML(self, request, tag):
return T.xml(self.renderedNote)
loader = formal.XMLString("""
<html xmlns="xmlns=http://www.w3.org/1999/xhtml"
xmlns:n="http://nevow.com/ns/nevow/0.1">
<head>
<title>\getConfig{web}{sitename} – Note for table
<n:invisible n:render="unicode" n:data="tableName"/></title>
<n:invisible n:render="commonhead"/>
<style>
span.target {font-size: 180%;font-weight:bold}
</style>
</head>
<body>
<n:invisible n:render="noteHTML"/>
</body>
</html>""")
innerLoader = template.TagLoader(
T.transparent(render="noteHTML"))
[docs]class HowToCiteRenderer(MetaRenderer):
"""A renderer that lets you format citation instructions.
"""
name = "howtocite"
customTemplate = svcs.loadSystemTemplate("howtocite.html")
[docs]class CoverageRenderer(MetaRenderer):
"""A renderer returning various forms of a service's spatial coverage.
This will return a 404 if the service doesn't have a coverage.spatial
meta (and will bomb out if that isn't a SMoc).
Based on the accept header, it will return a PNG if the client
indicates it's interested in that or if it accepts text/html, in which
case we assume it's a browser; otherwise, it will produce a
MOC in FITS format.
"""
name = "coverage"
[docs] def render(self, request):
from gavo.utils import pgsphere
try:
moc = pgsphere.SMoc.fromASCII(
self.service.getMeta(
"coverage.spatial", raiseOnFail=True).getContent())
except base.NoMetaKey:
raise svcs.UnknownURI(
"No spatial coverage available for this service.")
if self.returnAPNG(request):
request.setHeader("content-type", "image/png")
return moc.getPlot(xsize=400)
else:
request.setHeader("content-type", "application/fits")
return moc.asFITS()
[docs] @classmethod
def returnAPNG(self, request):
"""returns true if request indicates we're being retrieved by
a web browser.
"""
acceptDict = utils.parseAccept(request.getHeader("accept"))
return ("image/png" in acceptDict
or "image/webp" in acceptDict # firefox ~78 hack
or "image/*" in acceptDict
or "text/html" in acceptDict)
[docs] @classmethod
def isCacheable(cls, segments, request):
"""only cache this if we return a PNG.
Our caching system doesn't support content negotiation, so we
can't keep the FITS (and it's fast to generate, so that doesn't
matter so much.
"""
return cls.returnAPNG(request)
[docs]class EditionRenderer(MetaRenderer):
"""A renderer representing a (tutorial-like) text document.
Not sure yet what I'll do when people actually retrieve these.
This must have a meta accessURL with the document URI. It may
have a sourceURL meta giving the VCS URI.
"""
name = "edition"
[docs]class ExternalRenderer(grend.ServiceBasedPage):
"""A renderer redirecting to an external resource.
These try to access an external publication on the parent service
and ask it for an accessURL. If it doesn't define one, this will
lead to a redirect loop.
In the DC, external renderers are mainly used for registration of
third-party browser-based services.
"""
name = "external"
[docs] @classmethod
def isBrowseable(self, service):
return True # we probably need some way to say when that's wrong...
[docs] def render(self, request):
# look for a matching publication in the parent service...
for pub in self.service.publications:
if pub.render==self.name:
break
else: # no publication, 404
raise svcs.UnknownURI("No publication for an external service here.")
raise svcs.WebRedirect(base.getMetaText(pub, "accessURL",
macroPackage=self.service))
[docs]class RDInfoPage(grend.CustomTemplateMixin, grend.ResourceBasedPage):
"""A page giving infos about an RD.
This is not a renderer but a helper for RDInfoRenderer.
"""
customTemplate = svcs.loadSystemTemplate("rdinfo.html")
[docs] def data_services(self, request, tag):
return sorted(self.rd.services,
key=lambda s: base.getMetaText(s, "title", default=s.id))
[docs] def data_tables(self, request, tag):
return sorted((t for t in self.rd.tables
if t.onDisk
and not t.temporary
and not t.hasProperty("internal")),
key=lambda t: t.id)
[docs] def data_clientRdId(self, request, tag):
return self.rd.sourceId
def _getDescriptionHTML(self, descItem):
"""returns stan for the "description" of a service or a table.
The RD's description is not picked up.
"""
iDesc = descItem.getMeta("description", propagate=False)
if iDesc is None:
return ""
else:
return T.div(class_="lidescription")[
T.xml(iDesc.getContent("blockhtml", macroPackage=descItem))]
[docs] @template.renderer
def rdsvc(self, request, tag):
service = tag.slotData
return tag[
T.a(href=service.getURL("info"))[
base.getMetaText(service, "title", default=service.id)],
self._getDescriptionHTML(service)]
[docs] @template.renderer
def rdtable(self, request, tag):
tableDef = tag.slotData
qName = tableDef.getQName()
adqlNote = ""
if tableDef.adql:
adqlNote = T.span(class_="adqlnote")[" ",
"–", " queryable through ",
T.a(href="/tap")["TAP"], " and ",
T.a(href="/adql")["ADQL"],
" "]
return tag[
T.a(href="/tableinfo/%s"%qName)[qName],
adqlNote,
self._getDescriptionHTML(tableDef)]
[docs] @classmethod
def makePageTitle(cls, rd):
"""returns a suitable title for the rd info page.
This is a class method to allow other renderers to generate
titles for link anchors.
"""
return "Information on resource '%s'"%base.getMetaText(
rd, "title", default="%s"%rd.sourceId)
[docs] @template.renderer
def title(self, request, tag):
return tag[self.makePageTitle(self.rd)]
defaultLoader = common.doctypedStan(
T.html[
T.head[
T.title["Missing Template"]],
T.body[
T.p["RD infos are only available with an rdinfo.html template"]]
])
[docs]class RDInfoRenderer(grend.CustomTemplateMixin, grend.ServiceBasedPage):
"""A renderer for displaying various properties about a resource descriptor.
This renderer could really be attached to any service since
it does not call it, but it usually lives on //services/overview.
By virtue of builtin vanity, you can reach the rdinfo renderer
at /browse, and thus you can access /browse/foo/q to view the RD infos.
This is the form used by table registrations.
In addition to all services, this renderer also links tableinfos
for all non-temporary, on-disk tables defined in the RD. When
you actually want to hide some internal on-disk tables, you can
set a property ``internal`` on the table (the value is ignored).
"""
name = "rdinfo"
customTemplate = svcs.loadSystemTemplate("rdlist.html")
[docs] def data_publishedRDs(self, request, tag):
with base.getTableConn() as conn:
return [row[0] for row in
conn.query(
"""SELECT DISTINCT sourceRD
FROM (
SELECT sourceRD FROM dc.resources
WHERE NOT deleted) as q
ORDER BY sourceRD""")]
[docs] def getChild(self, name, request):
rdId = "/".join(request.popSegments(name))
if not rdId:
raise svcs.WebRedirect("browse")
clientRD = base.caches.getRD(rdId)
return RDInfoPage(request, clientRD)
defaultLoader = common.doctypedStan(
T.html[
T.head[
T.title["Missing Template"]],
T.body[
T.p["The RD list is only available with an rdlist.html template"]]
])
[docs]class ResourceRecordMaker(resource.Resource):
"""A page that returns resource records for internal services.
This is basically like OAI-PMH getRecord, except we're using rd/id/svcid
from our path.
Also (and that's fairly important for purx), this will use the
_metadataUpdated meta for a modified header.
"""
[docs] def render(self, request):
raise svcs.UnknownURI("What resource record do you want?")
[docs] def getChild(self, name, request):
from gavo.registry import builders
segments = request.popSegments(name)
rdParts, svcId = segments[:-1], segments[-1]
rdId = "/".join(rdParts)
try:
rd = base.caches.getRD(rdId)
resob = rd.getById(svcId)
except base.NotFoundError:
raise svcs.UnknownURI("The resource %s#%s is unknown at this site."%(
rdId, svcId))
timestampUpdated = utils.parseISODT(
resob.getMeta("_metadataUpdated").getContent("text")).timestamp()
return common.TypedData(
utils.xmlrender(
builders.getVORMetadataElement(resob),
prolog="<?xml version='1.0'?>"
"<?xml-stylesheet href='/static/xsl/oai.xsl' type='text/xsl'?>",
),
"application/xml",
timestampUpdated)