1 """
2 Functions available to rowmaker procs.
3
4 Rowmaker procs are compiled in the namespace defined by this module.
5
6 Maybe we should allow additional modules to be specified in gavorc?
7 """
8
9
10
11
12
13
14
15 import base64
16 import datetime
17 import math
18 import os
19 import pprint
20 import re
21 import sys
22 import time
23 import traceback
24 import urllib
25
26 from gavo import base
27 from gavo import stc
28 from gavo import utils
29 from gavo.base import (
30 coords, parseBooleanLiteral, parseInt, sqlmunge,
31 makeSitePath, makeAbsoluteURL, SkipThis)
32 from gavo.base.literals import (identity,
33 parseInt, parseFloat, parseBooleanLiteral, parseUnicode,
34 parseDefaultDate, parseDefaultDatetime, parseDefaultTime, parseCooPair,
35 parseSPoint, getDefaultValueParsers)
36
37 from gavo.base.unitconv import (
38 PLANCK_H, LIGHT_C)
39 from gavo.rscdef.common import (
40 getStandardPubDID, getAccrefFromStandardPubDID,
41 getDatalinkMetaLink,
42 getInputsRelativePath)
43 from gavo.stc import parseSimpleSTCS
44 from gavo.stc.times import (
45 dateTimeToJdn, dateTimeToMJD, dateTimeToJYear,
46 bYearToDateTime, jdnToDateTime, mjdToDateTime, TTtoTAI, TAItoTT)
47 from gavo.utils import codetricks
48 from gavo.utils import (
49 dmsToDeg, hmsToDeg, DEG, parseISODT, iterSimpleText, getFileStem,
50 getWCSAxis, getRelativePath, loadPythonModule)
51 from gavo.utils import pgsphere
52
53
54
55 DEG_MAS = 1/3600000.
56
57 DEG_ARCSEC = 1/3600.
58
59
60 MS = base.makeStruct
64 """can be raised by user code to indicate that a row should be
65 skipped when building a table.
66
67 Note: To skip an entire source, raise SkipThis (usually in a rowfilter
68 or so).
69
70 Also note that the non-code way to skip things, `Triggers`_, is
71 preferred when you don't already use code.
72 """
73
76 """inserts c_x, c_y, and c_z for the equatorial position alpha, delta
77 into result.
78
79 c_x, c_y, and c_z are the cartesian coordinates of the intersection
80 point of the radius vector of alpha, delta with the unit sphere.
81
82 alpha and delta already have to be floats, so you probably want
83 to use variables here.
84
85 >>> r = {}; addCartesian(r, 25, 30); str(r["c_x"]), str(r["c_y"])
86 ('0.784885567221', '0.365998150771')
87 """
88 result["c_x"], result["c_y"], result["c_z"] = coords.computeUnitSphereCoords(
89 alpha, delta)
90
93 """inserts pm_total (in degs/yr) and pm_posang (in degs, east over north)
94 into result.
95
96 pma and pmd have to be in degs/yr, with cos(delta) applied to pma.
97 """
98 if pma is None or pmd is None:
99 tpm = pmpa = None
100 else:
101 tpm = math.sqrt(pma**2+pmd**2)
102 pmpa = math.atan2(pma, pmd)*360/2/math.pi
103 result["pm_total"] = tpm
104 result["pm_posang"] = pmpa
105
115
118 """returns a ``datetime.timedelta`` object for literal parsed according
119 to format.
120
121 For format, you can the magic values ``!!secondsSinceMidnight``,
122 ``!!decimalHours`` or a strptime-like spec using the H, M, and S codes.
123
124 >>> parseTime("89930", "!!secondsSinceMidnight")
125 datetime.timedelta(1, 3530)
126 >>> parseTime("23.4", "!!decimalHours")
127 datetime.timedelta(0, 84240)
128 >>> parseTime("3.4:5", "%H.%M:%S")
129 datetime.timedelta(0, 11045)
130 >>> parseTime("20:04", "%H:%M")
131 datetime.timedelta(0, 72240)
132 """
133 if format=="!!secondsSinceMidnight":
134 return datetime.timedelta(seconds=float(literal))
135 elif format=="!!decimalHours":
136 return datetime.timedelta(hours=float(literal))
137 else:
138
139
140 partDict = utils.parsePercentExpression(literal, format)
141 return datetime.timedelta(0, hours=float(partDict.get("H", 0)),
142 minutes=float(partDict.get("M", 0)), seconds=float(partDict.get("S", 0)))
143
147 """returns a ``datetime.date`` object of literal parsed according to the
148 strptime-similar format.
149
150 The function understands the special ``dateFormat`` ``!!jYear``
151 (stuff like 1980.89).
152 """
153 if format in ("!!julianEp", "!!jYear"):
154 return stc.jYearToDateTime(float(literal))
155 return datetime.datetime(*time.strptime(literal, format)[:3])
156
160 """returns a ``datetime.datetime`` object from a literal parsed according
161 to the strptime-similar format.
162
163 A ``ValueError`` is raised if literal doesn't match format (actually,
164 a parse with essentially DALI-standard ISO representation is always tried)
165 """
166 try:
167 return datetime.datetime(*time.strptime(literal, format)[:6])
168 except ValueError:
169
170 return parseISODT(literal)
171
175 """returns a modified julian date made from some datetime representation.
176
177 Valid representations include:
178
179 * MJD (a float smaller than 1e6)
180 * JD (a float larger than 1e6)
181 * datetime.datetime instances
182 * ISO time strings.
183 """
184 if literal is None:
185 return None
186 elif isinstance(literal, str):
187 return stc.dateTimeToMJD(parseTimestamp(literal))
188 elif isinstance(literal, datetime.datetime):
189 return stc.dateTimeToMJD(literal)
190 elif literal>1e6:
191 return literal-stc.JD_MJD
192 else:
193 return literal
194
198 """makes a datetime instance from a date and a time.
199 """
200 return datetime.datetime(date.year, date.month, date.day)+time
201
205 """converts the various forms angles might be encountered to degrees.
206
207 format is one of hms, dms, fracHour. For sexagesimal/time angles,
208 you can pass a sepChar (default: split at blanks) that lets you
209 specify what separates hours/degrees, minutes, and seconds.
210
211 >>> str(parseAngle("23 59 59.95", "hms"))
212 '359.999791667'
213 >>> "%10.5f"%parseAngle("-20:31:05.12", "dms", sepChar=":")
214 ' -20.51809'
215 >>> "%010.6f"%parseAngle("21.0209556", "fracHour")
216 '315.314334'
217 """
218 if format=="dms":
219 return utils.dmsToDeg(literal, sepChar=sepChar)
220 elif format=="hms":
221 return utils.hmsToDeg(literal, sepChar=sepChar)
222 elif format=="fracHour":
223 return utils.fracHoursToDeg(literal)
224 else:
225 raise base.Error("Invalid format: %s"%format)
226
230 """returns the mean value between two values.
231
232 Beware: Integer division done here for the benefit of datetime calculations.
233
234 >>> computeMean(1.,3)
235 2.0
236 >>> computeMean(datetime.datetime(2000, 10, 13),
237 ... datetime.datetime(2000, 10, 12))
238 datetime.datetime(2000, 10, 12, 12, 0)
239 """
240 return val1+(val2-val1)/2
241
245 """returns the string literal with all blanks removed.
246
247 This is useful when numbers are formatted with blanks thrown in.
248
249 Nones are passed through.
250 """
251 if literal is None:
252 return None
253 else:
254 return literal.replace(" ", "")
255
259 """returns a path made up from the last ``numElements`` items in ``path``.
260 """
261 newPath = []
262 for i in range(int(numElements)):
263 path, part = os.path.split(path)
264 newPath.append(part)
265 newPath.reverse()
266 return os.path.join(*newPath)
267
271 """returns val*factor+offset if val is not None, None otherwise.
272
273 This is when you want to manipulate a numeric value that may be NULL.
274 It is a somewhat safer alternative to using nullExcs with scaled values.
275 """
276 if val is None:
277 return None
278 return factor*val+offset
279
284 """returns default if literal is ``nullLiteral``, else
285 ``baseParser(literal)``.
286
287 If ``checker`` is non-None, it must be a callable returning ``True`` if its
288 argument is a null value.
289
290 ``nullLiteral`` is compared against the unprocessed literal (usually, a
291 string). The intended use is like this (but note that often, a
292 ``nullExcs`` attribute on a rowmaker ``map`` element is the more
293 elegant way:
294
295 >>> parseWithNull("8888.0", float, "8888")
296 8888.0
297 >>> print(parseWithNull("8888", float, "8888"))
298 None
299 >>> print(parseWithNull("N/A", int, "N/A"))
300 None
301 """
302 if (nullLiteral is not base.Undefined and literal==nullLiteral
303 ) or literal is None:
304 return default
305 if checker is not None:
306 if checker(literal):
307 return default
308 res = baseParser(literal)
309 if res is None:
310 return default
311 return res
312
313
314 -def getHTTPPar(inputData, parser, single=False, forceUnique=False,
315 parName="?"):
316 """returns a parsed value from inputData.
317
318 Deprecated. Should no longer be necessary with 1.0-style input
319 processing where you have guaranteed multiplicities after the
320 contextGrammar, and contextGrammars are reasonably cheap to use.
321
322 inputData may be
323
324 * None -- the function will return None
325 * an empty list -- the function will return None
326 * a value other than a list -- as if it were a list of length 1
327 * a list -- the function will return a list of parsed items
328
329 This is of conveniently and robustly pulling out data from stuff coming
330 out of inputKeys without multiplicity.
331
332 If you pass single=True, you'll get exactly one value (or None). The
333 value will be the first one from a sequence.
334
335 If you pass forceUnique=True, a MultiplicityError will be raised if
336 inputData is longer than one.
337 """
338 if inputData is None:
339 return None
340 if not isinstance(inputData, list):
341 inputData = [inputData]
342 if len(inputData)==0:
343 return None
344
345 if forceUnique and len(inputData)>1:
346 raise base.MultiplicityError(
347 "Inputs for this parameter must not have more than"
348 " one value; hovever, %s was passed in."%str(inputData),
349 colName=parName)
350
351 if single:
352 return parser(inputData[0])
353 else:
354 return [parser(v) for v in inputData]
355
359 """returns ``val`` unless it is ``None``, in which case a ``ValidationError``
360 for ``fieldName`` will be raised.
361 """
362 if val is None:
363 raise base.ValidationError("Value is required but was not provided",
364 fieldName)
365 return val
366
369 """yields _MAX and _MIN inputKeys from a single input key.
370
371 This also tries to sensibly fix descriptions and ucds.
372 This is mainly for datalink metaMakers; condDescs may use a
373 similar thing, but that's not exposed to RDs.
374
375 Don't use this function any more. It will go away soon.
376 """
377 name = inputKey.name
378 ucd = inputKey.ucd or None
379 description = inputKey.description
380 base.ui.notifyWarning("Deprecated genLimitKeys function used."
381 " Fix your RD to use proper DALI-intervals (ask dachs-support"
382 " if you don't know what to do.)")
383
384 yield inputKey.change(name=name+"_MIN",
385 ucd=ucd and "par.min;"+ucd,
386 description=description.rstrip(".")+", lower limit")
387 yield inputKey.change(name=name+"_MAX",
388 ucd=ucd and "par.max;"+ucd,
389 description=description.rstrip(".")+", upper limit")
390
394 """returns a unix-compatible file name for an access reference.
395
396 The file name will not contain terrible characters, let alone
397 slashes. This is used to, e.g., keep all previews in one directory.
398 """
399
400 if accref.startswith("/"):
401 accref = utils.getRelativePath(accref,
402 base.getConfig("inputsDir"), liberalChars=True)
403
404 return base64.b64encode(accref, "$!")
405
408 globals()[name] = func
409
410
411 -def makeProc(funcName, code, setupCode, parent, **moreNames):
412 """compiles a function in the rmkfunc's namespace.
413
414 code is a complete function source. setupCode is executed right away
415 in this namespace to add globals.
416 """
417 funcNs = globals().copy()
418 funcNs["parent"] = parent
419 funcNs.update(moreNames)
420 if setupCode.strip():
421 try:
422 exec setupCode.rstrip() in funcNs
423 except (SyntaxError, TypeError) as ex:
424 raise base.ui.logOldExc(
425 base.BadCode(setupCode, "setup code", ex))
426 except NameError as ex:
427 raise base.ui.logOldExc(
428 base.BadCode(setupCode, "setup code", ex,
429 hint="This typically happens when you forget to put"
430 " quotes around string values."))
431 return utils.compileFunction(code.rstrip(), funcName, funcNs,
432 debug=base.DEBUG)
433
438
439
440 if __name__=="__main__":
441 _test()
442