Package gavo :: Package svcs :: Module common
[frames] | no frames]

Source Code for Module gavo.svcs.common

  1  """ 
  2  Common functions and classes for services and cores. 
  3  """ 
  4   
  5  #c Copyright 2008-2019, the GAVO project 
  6  #c 
  7  #c This program is free software, covered by the GNU GPL.  See the 
  8  #c COPYING file in the source distribution. 
  9   
 10   
 11  import re 
 12  import os 
 13   
 14  from nevow import inevow 
 15  from nevow import loaders 
 16   
 17  import pkg_resources 
 18   
 19  from gavo import base 
 20  from gavo import utils 
21 22 23 -class Error(base.Error):
24 - def __init__(self, msg, rd=None, hint=None):
25 self.rd = rd 26 base.Error.__init__(self, msg, hint=hint)
27
28 29 -class BadMethod(Error):
30 """raised to generate an HTTP 405 response. 31 """
32 - def __str__(self):
33 return "This resource cannot respond to the HTTP '%s' method"%self.msg
34
35 36 -class UnknownURI(Error, base.NotFoundError):
37 """raised to generate an HTTP 404 response. 38 """
39 - def __str__(self):
40 return Error.__str__(self)
41
42 43 -class ForbiddenURI(Error):
44 """raised to generate an HTTP 403 response. 45 """
46
47 48 -class Authenticate(Error):
49 """raised to initiate an authentication request. 50 51 Authenticates are optionally constructed with the realm the user 52 shall authenticate in. If you leave the realm out, the DC-wide default 53 will be used. 54 """
55 - def __init__(self, realm=base.getConfig("web", "realm"), hint=None):
56 self.realm = realm 57 Error.__init__(self, "This is a request to authenticate against %s"%realm, 58 hint=hint)
59
60 61 -class RedirectBase(Error):
62 - def __init__(self, dest, hint=None):
63 self.rawDest = dest 64 dest = str(dest) 65 if not dest.startswith("http"): 66 dest = base.getConfig("web", "serverURL")+base.makeSitePath(dest) 67 self.dest = dest 68 Error.__init__(self, "This is supposed to redirect to %s"%dest, 69 hint=hint)
70
71 72 -class WebRedirect(RedirectBase):
73 """raised to redirect a user agent to a different resource (HTTP 301). 74 75 WebRedirectes are constructed with the destination URL that can be 76 relative (to webRoot) or absolute (starting with http). 77 """
78
79 80 -class SeeOther(RedirectBase):
81 """raised to redirect a user agent to a different resource (HTTP 303). 82 83 SeeOthers are constructed with the destination URL that can be 84 relative (to webRoot) or absolute (starting with http). 85 86 They are essentially like WebRedirect, except they put out a 303 87 instead of a 301. 88 """
89
90 -class Found(RedirectBase):
91 """raised to redirect a user agent to a different resource (HTTP 302). 92 93 Found instances are constructed with the destination URL that can be 94 relative (to webRoot) or absolute (starting with http). 95 96 They are essentially like WebRedirect, except they put out a 302 97 instead of a 301. 98 """
99
100 101 -def parseServicePath(serviceParts):
102 """returns a tuple of resourceDescriptor, serviceName. 103 104 A serivce id consists of an inputsDir-relative path to a resource 105 descriptor, a slash, and the name of a service within this descriptor. 106 107 This function returns a tuple of inputsDir-relative path and service name. 108 It raises a gavo.Error if sid has an invalid format. The existence of 109 the resource or the service are not checked. 110 """ 111 return "/".join(serviceParts[:-1]), serviceParts[-1]
112
113 114 -class QueryMeta(dict):
115 """A class keeping information on the query environment. 116 117 It is constructed with a plain dictionary (there are alternative 118 constructors for nevow contexts and requests are below) mapping 119 certain keys (you'll currently have to figure out which from the 120 source) to values, mostly strings, except for the keys listed in 121 listKeys, which should be sequences of strings. 122 123 If you pass an empty dict, some sane defaults will be used. You 124 can get that "empty" query meta as common.emptyQueryMeta, but make 125 sure you don't mutate it. 126 127 QueryMetas constructed from request will have the user and password 128 items filled out. 129 130 If you're using nevow formal, you should set the formal_data item 131 to the dictionary created by formal. This will let people use 132 the parsed parameters in templates. 133 """ 134 135 # a set of keys handled by query meta to be ignored in parameter 136 # lists because they are used internally. This covers everything 137 # QueryMeta interprets, but also keys by introduced by certain gwidgets 138 # and the nevow infrastructure 139 metaKeys = set(["_FILTER", "_OUTPUT", "_charset_", "_ADDITEM", 140 "__nevow_form__", "_FORMAT", "_VERB", "_TDENC", "formal_data", 141 "_SET", "_TIMEOUT", "_VOTABLE_VERSION", "FORMAT"]) 142 143 # a set of keys that have sequences as values (needed for construction 144 # from nevow request.args) 145 listKeys = set(["_ADDITEM", "_DBOPTIONS_ORDER", "_SET"]) 146
147 - def __init__(self, initArgs=None, defaultLimit=None):
148 if initArgs is None: 149 initArgs = {} 150 self.ctxArgs = utils.CaseSemisensitiveDict(initArgs) 151 if defaultLimit is None: 152 self.defaultLimit = base.getConfig("db", "defaultLimit") 153 else: 154 self.defaultLimit = defaultLimit 155 self["formal_data"] = {} 156 self["user"] = self["password"] = None 157 self["accept"] = {} 158 self._fillOutput(self.ctxArgs) 159 self._fillDbOptions(self.ctxArgs) 160 self._fillSet(self.ctxArgs)
161 162 @classmethod
163 - def fromNevowArgs(cls, nevowArgs, **kwargs):
164 """constructs a QueryMeta from a nevow web arguments dictionary. 165 """ 166 args = {} 167 for key, value in nevowArgs.iteritems(): 168 # defense against broken legacy code: listify if necessay 169 if not isinstance(value, list): 170 value = [value] 171 172 if key in cls.listKeys: 173 args[key] = value 174 else: 175 if value: 176 args[key] = value[0] 177 return cls(args, **kwargs)
178 179 @classmethod
180 - def fromRequest(cls, request, **kwargs):
181 """constructs a QueryMeta from a nevow request. 182 183 In addition to getting information from the arguments, this 184 also sets user and password. 185 """ 186 res = cls.fromNevowArgs(request.args, **kwargs) 187 res["accept"] = utils.parseAccept(request.getHeader("accept")) 188 res["user"], res["password"] = request.getUser(), request.getPassword() 189 return res
190 191 @classmethod
192 - def fromContext(cls, ctx, **kwargs):
193 """constructs a QueryMeta from a nevow context. 194 """ 195 return cls.fromRequest(inevow.IRequest(ctx), **kwargs)
196
197 - def _fillOutput(self, args):
198 """interprets values left by the OutputFormat widget. 199 """ 200 if "RESPONSEFORMAT" in args: 201 self["format"] = args["RESPONSEFORMAT"] 202 else: 203 self["format"] = args.get("_FORMAT", "HTML") 204 205 try: 206 # prefer fine-grained "verbosity" over _VERB or VERB 207 # Hack: malformed _VERBs result in None verbosity, which is taken to 208 # mean about "use fields of HTML". Absent _VERB or VERB, on the other 209 # hand, means VERB=2, i.e., a sane default 210 if "verbosity" in args: 211 self["verbosity"] = int(args["verbosity"]) 212 elif "_VERB" in args: # internal verb parameter 213 self["verbosity"] = int(args["_VERB"])*10 214 elif "VERB" in args: # verb parameter for SCS and such 215 self["verbosity"] = int(args["VERB"])*10 216 else: 217 self["verbosity"] = 20 218 except ValueError: 219 self["verbosity"] = "HTML" # VERB given, but not an int. 220 221 self["tdEnc"] = base.getConfig("ivoa", "votDefaultEncoding")=="td" 222 if "_TDENC" in args: 223 try: 224 self["tdEnc"] = base.parseBooleanLiteral(args["_TDENC"]) 225 except ValueError: 226 pass 227 228 try: 229 self["VOTableVersion"] = tuple(int(v) for v in 230 args["_VOTABLE_VERSION"].split(".")) 231 except: # simple ignore malformed version specs 232 pass 233 234 self["additionalFields"] = args.get("_ADDITEM", [])
235
236 - def _fillSet(self, args):
237 """interprets the output of a ColumnSet widget. 238 """ 239 self["columnSet"] = None 240 if "_SET" in args: 241 self["columnSet"] = set(args["_SET"])
242
243 - def _fillDbOptions(self, args):
244 self["dbSortKeys"] = [s.strip() 245 for s in args.get("_DBOPTIONS_ORDER", []) if s.strip()] 246 247 self["direction"] = {"ASC": "ASC", "DESC": "DESC"}[ 248 args.get("_DBOPTIONS_DIR", "ASC")] 249 250 try: 251 self["dbLimit"] = int(args["MAXREC"]) 252 except (ValueError, KeyError): 253 self["dbLimit"] = self.defaultLimit 254 255 try: 256 self["timeout"] = int(args["_TIMEOUT"]) 257 except (ValueError, KeyError): 258 self["timeout"] = base.getConfig("web", "sqlTimeout")
259
260 - def overrideDbOptions(self, sortKeys=None, limit=None, sortFallback=None, 261 direction=None):
262 if sortKeys is not None: 263 self["dbSortKeys"] = sortKeys 264 if not self["dbSortKeys"] and sortFallback is not None: 265 self["dbSortKeys"] = sortFallback.split(",") 266 if limit is not None: 267 self["dbLimit"] = int(limit) 268 if direction is not None: 269 self["direction"] = direction
270
271 - def asSQL(self):
272 """returns the dbLimit and dbSortKey values as an SQL fragment. 273 """ 274 frag, pars = [], {} 275 sortKeys = self["dbSortKeys"] 276 dbLimit = self["dbLimit"] 277 # TODO: Sorting needs a thorough redesign, presumably giving column instance 278 # as column keys. These could carry "default up" or "default down" in 279 # properties. Meanwhile, there should be a UI to let users decide on 280 # sort direction. 281 # Meanwhile, let's to an emergency hack. 282 if sortKeys: 283 # Ok, we need to do some emergency securing here. There should be 284 # pre-validation that we're actually seeing a column key, but 285 # just in case let's make sure we're seeing an SQL identifier. 286 # (We can't rely on dbapi's escaping since we're not talking values here) 287 frag.append("ORDER BY %s %s"%(",".join( 288 re.sub('[^A-Za-z0-9"_]+', "", key) for key in sortKeys), 289 self["direction"])) 290 if dbLimit: 291 frag.append("LIMIT %(_matchLimit)s") 292 pars["_matchLimit"] = int(dbLimit)+1 293 return " ".join(frag), pars
294 295 296 emptyQueryMeta = QueryMeta()
297 298 299 -def getTemplatePath(key):
300 """see loadSystemTemplate. 301 """ 302 userPath = os.path.join(base.getConfig("rootDir"), "web/templates", key) 303 if os.path.exists(userPath): 304 return userPath 305 else: 306 resPath = "resources/templates/"+key 307 if pkg_resources.resource_exists('gavo', resPath): 308 return pkg_resources.resource_filename('gavo', resPath)
309
310 311 -def loadSystemTemplate(key):
312 """returns a nevow template for system pages from key. 313 314 path is interpreted as relative to gavo_root/web/templates (first) 315 and package internal (last). If no template is found, None is 316 returned (this harmonizes with the fallback in CustomTemplateMixin). 317 """ 318 try: 319 tp = getTemplatePath(key) 320 if tp is not None: 321 return loaders.xmlfile(tp) 322 except IOError: 323 pass
324