File: common.py

package info (click to toggle)
gavodachs 2.3%2Bdfsg-3
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 7,260 kB
  • sloc: python: 58,359; xml: 8,882; javascript: 3,453; ansic: 661; sh: 158; makefile: 22
file content (328 lines) | stat: -rw-r--r-- 10,096 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
"""
Common functions and classes for services and cores.
"""

#c Copyright 2008-2020, the GAVO project
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


import re
import os

import pkg_resources

from gavo import base
from gavo import utils
from gavo.formal import nevowc


class Error(base.ExecutiveAction):
	def __init__(self, msg, rd=None, hint=None):
		self.rd = rd
		self.msg = msg
		self.hint = hint
		base.ExecutiveAction.__init__(self, msg)


class BadMethod(Error):
	"""raised to generate an HTTP 405 response.
	"""
	def __str__(self):
		return "This resource cannot respond to the HTTP '%s' method"%self.msg


class UnknownURI(Error, base.NotFoundError):
	"""raised to generate an HTTP 404 response.
	"""
	def __str__(self):
		return Error.__str__(self)


class ForbiddenURI(Error):
	"""raised to generate an HTTP 403 response.
	"""


class Authenticate(Error):
	"""raised to initiate an authentication request.

	Authenticates are optionally constructed with the realm the user 
	shall authenticate in.  If you leave the realm out, the DC-wide default
	will be used.
	"""
	def __init__(self, realm=base.getConfig("web", "realm"), hint=None):
		self.realm = realm
		Error.__init__(self, "This is a request to authenticate against %s"%realm,
			hint=hint)


class RedirectBase(Error):
	def __init__(self, dest, hint=None):
		if isinstance(dest, bytes):
			dest = dest.decode("utf-8")

		self.rawDest = dest
		dest = str(dest)
		if not dest.startswith("http"):
			dest = base.getConfig("web", "serverURL")+base.makeSitePath(dest)
		self.dest = dest
		Error.__init__(self, "This is supposed to redirect to %s"%dest,
			hint=hint)


class WebRedirect(RedirectBase):
	"""raised to redirect a user agent to a different resource (HTTP 301).

	WebRedirectes are constructed with the destination URL that can be
	relative (to webRoot) or absolute (starting with http).
	"""


class SeeOther(RedirectBase):
	"""raised to redirect a user agent to a different resource (HTTP 303).

	SeeOthers are constructed with the destination URL that can be
	relative (to webRoot) or absolute (starting with http).

	They are essentially like WebRedirect, except they put out a 303 
	instead of a 301.
	"""


class Found(RedirectBase):
	"""raised to redirect a user agent to a different resource (HTTP 302).

	Found instances are constructed with the destination URL that can be
	relative (to webRoot) or absolute (starting with http).

	They are essentially like WebRedirect, except they put out a 302 
	instead of a 301.
	"""


def parseServicePath(serviceParts):
	"""returns a tuple of resourceDescriptor, serviceName.

	A serivce id consists of an inputsDir-relative path to a resource 
	descriptor, a slash, and the name of a service within this descriptor.

	This function returns a tuple of inputsDir-relative path and service name.
	It raises a gavo.Error if sid has an invalid format.  The existence of
	the resource or the service are not checked.
	"""
	return "/".join(serviceParts[:-1]), serviceParts[-1]


class QueryMeta(dict):
	"""A class keeping information on the query environment.

	It is constructed with a plain dictionary (there are alternative
	constructors for t.w requests are below) mapping 
	certain keys (you'll currently have to figure out which from the 
	source) to values, mostly strings, except for the keys listed in 
	listKeys, which should be sequences of strings.
	
	If you pass an empty dict, some sane defaults will be used.  You
	can get that "empty" query meta as common.emptyQueryMeta, but make
	sure you don't mutate it.

	QueryMetas constructed from request will have the user and password
	items filled out.

	If you're using formal, you should set the formal_data item
	to the dictionary created by formal.  This will let people use
	the parsed parameters in templates.

	Note: You cannot trust qm["user"] -- it is not validated against
	any credentials.
	"""
	
	# a set of keys handled by query meta to be ignored in parameter
	# lists because they are used internally.  This covers everything 
	# QueryMeta interprets, but also keys by introduced by certain gwidgets
	# and the formal infrastructure
	metaKeys = set(["_FILTER", "_OUTPUT", "_charset_", "_ADDITEM",
		"__nevow_form__", "_FORMAT", "_VERB", "_TDENC", "formal_data",
		"_SET", "_TIMEOUT", "_VOTABLE_VERSION", "FORMAT"])

	# a set of keys that have sequences as values (needed for construction
	# from t.w request.strargs)
	listKeys = set(["_ADDITEM", "_DBOPTIONS_ORDER", "_SET"])

	def __init__(self, initArgs=None, defaultLimit=None):
		if initArgs is None:
			initArgs = {}
		self.ctxArgs = utils.CaseSemisensitiveDict(initArgs)
		if defaultLimit is None:
			self.defaultLimit = base.getConfig("db", "defaultLimit")
		else:
			self.defaultLimit = defaultLimit
		self["formal_data"] = {}
		self["user"] = self["password"] = None
		self["accept"] = {}
		self["inputTable"] = None
		self._fillOutput(self.ctxArgs)
		self._fillDbOptions(self.ctxArgs)
		self._fillSet(self.ctxArgs)

	@classmethod
	def fromRequestArgs(cls, inArgs, **kwargs):
		"""constructs a QueryMeta from a gavo.web.common.Request.strargs
		"""
		args = {}
		for key, value in inArgs.items():
			# defense against broken legacy code: listify if necessary
			if not isinstance(value, list):
				value = [value]

			if key in cls.listKeys:
				args[key] = value
			else:
				if value:
					args[key] = value[0]
		return cls(args, **kwargs)

	@classmethod
	def fromRequest(cls, request, **kwargs):
		"""constructs a QueryMeta from a gavo.web.common.Request.

		In addition to getting information from the arguments, this
		also sets user and password.
		"""
		res = cls.fromRequestArgs(request.strargs, **kwargs)
		res["accept"] = utils.parseAccept(request.getHeader("accept"))
		res["user"] = request.getUser() or None
		res["password"] = utils.debytify(request.getPassword(), "utf-8") or None
		return res
	
	def _fillOutput(self, args):
		"""interprets values left by the OutputFormat widget.
		"""
		if "RESPONSEFORMAT" in args:
			self["format"] = args["RESPONSEFORMAT"]
		else:
			self["format"] = args.get("_FORMAT", "HTML")

		try:
# prefer fine-grained "verbosity" over _VERB or VERB
# Hack: malformed _VERBs result in None verbosity, which is taken to
# mean about "use fields of HTML".  Absent _VERB or VERB, on the other
# hand, means VERB=2, i.e., a sane default
			if "verbosity" in args:
				self["verbosity"] = int(args["verbosity"])
			elif "_VERB" in args:  # internal verb parameter
				self["verbosity"] = int(args["_VERB"])*10
			elif "VERB" in args:   # verb parameter for SCS and such
				self["verbosity"] = int(args["VERB"])*10
			else:
				self["verbosity"] = 20
		except ValueError:
			self["verbosity"] = "HTML"  # VERB given, but not an int.

		self["tdEnc"] = base.getConfig("ivoa", "votDefaultEncoding")=="td"
		if "_TDENC" in args:
			try:
				self["tdEnc"] = base.parseBooleanLiteral(args["_TDENC"])
			except ValueError:
				pass

		try:
			self["VOTableVersion"] = tuple(int(v) for v in
				args["_VOTABLE_VERSION"].split("."))
		except:  # simple ignore malformed version specs
			pass

		self["additionalFields"] = args.get("_ADDITEM", [])

	def _fillSet(self, args):
		"""interprets the output of a ColumnSet widget.
		"""
		self["columnSet"] = None
		if "_SET" in args:
			self["columnSet"] = set(args["_SET"])

	def _fillDbOptions(self, args):
		self["dbSortKeys"] = [s.strip() 
			for s in args.get("_DBOPTIONS_ORDER", []) if s.strip()]

		self["direction"] = {"ASC": "ASC", "DESC": "DESC"}[
			args.get("_DBOPTIONS_DIR", "ASC")]

		try:
			self["dbLimit"] = int(args["MAXREC"])
		except (ValueError, KeyError):
			self["dbLimit"] = self.defaultLimit

		try:
			self["timeout"] = max(float(args["_TIMEOUT"]), 0.001)
		except (ValueError, KeyError):
			self["timeout"] = base.getConfig("web", "sqlTimeout")

	def overrideDbOptions(self, sortKeys=None, limit=None, sortFallback=None,
			direction=None):
		if sortKeys is not None:
			self["dbSortKeys"] = sortKeys
		if not self["dbSortKeys"] and sortFallback is not None:
			self["dbSortKeys"] = sortFallback.split(",")
		if limit is not None:
			self["dbLimit"] = int(limit)
		if direction is not None:
			self["direction"] = direction

	def asSQL(self):
		"""returns the dbLimit and dbSortKey values as an SQL fragment.
		"""
		frag, pars = [], {}
		sortKeys = self["dbSortKeys"]
		dbLimit = self["dbLimit"]
# TODO: Sorting needs a thorough redesign, presumably giving column instance
# as column keys.  These could carry "default up" or "default down" in
# properties.  Meanwhile, there should be a UI to let users decide on
# sort direction.
# Meanwhile, let's do an emergency hack.
		if sortKeys:
			# Ok, we need to do some emergency securing here.  There should be
			# pre-validation that we're actually seeing a column key, but
			# just in case let's make sure we're seeing an SQL identifier.
			# (We can't rely on dbapi's escaping since we're not talking values here)
			frag.append("ORDER BY %s %s"%(",".join(
					re.sub('[^A-Za-z0-9"_]+', "", key) for key in sortKeys),
				self["direction"]))
		if dbLimit:
			frag.append("LIMIT %(_matchLimit)s")
			pars["_matchLimit"] = int(dbLimit)+1
		return " ".join(frag), pars


emptyQueryMeta = QueryMeta()


def getTemplatePath(key):
	"""see loadSystemTemplate.
	"""
	userPath = os.path.join(base.getConfig("rootDir"), "web/templates", key)
	if os.path.exists(userPath):
		return userPath
	else:
		resPath = "resources/templates/"+key
		if pkg_resources.resource_exists('gavo', resPath):
			return pkg_resources.resource_filename('gavo', resPath)
		else:
			raise base.NotFoundError(key, "template", "system templates")


def loadSystemTemplate(key):
	"""returns a nevow template for system pages from key.

	path is interpreted as relative to gavo_root/web/templates (first)
	and package internal (last).  If no template is found, None is
	returned (this harmonizes with the fallback in CustomTemplateMixin).
	"""
	try:
		tp = getTemplatePath(key)
		if tp is not None:
			return nevowc.XMLFile(tp)
	except IOError:
		pass