ligolw.py 30.5 KB
Newer Older
Kipp Cannon's avatar
Kipp Cannon committed
1
# Copyright (C) 2006--2018  Kipp Cannon
kipp's avatar
kipp committed
2 3 4
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
duncan's avatar
duncan committed
5
# Free Software Foundation; either version 3 of the License, or (at your
kipp's avatar
kipp committed
6 7 8 9 10 11 12 13 14 15 16
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

kipp's avatar
kipp committed
17

kipp's avatar
kipp committed
18 19 20 21 22 23 24 25
#
# =============================================================================
#
#                                   Preamble
#
# =============================================================================
#

kipp's avatar
kipp committed
26

27 28 29
"""
This module provides class definitions corresponding to the elements that
can be found in a LIGO Light Weight XML file.  It also provides a class
kipp's avatar
kipp committed
30 31 32
representing an entire LIGO Light Weight XML document, a ContentHandler
class for use with SAX2 parsers, and a convenience function for
constructing a parser.
33 34 35
"""


36
import datetime
Kipp Cannon's avatar
Kipp Cannon committed
37
import dateutil.parser
38 39
import sys
from xml import sax
40
from xml.sax.xmlreader import AttributesImpl
kipp's avatar
kipp committed
41
from xml.sax.saxutils import escape as xmlescape
42
from xml.sax.saxutils import unescape as xmlunescape
43

kipp's avatar
kipp committed
44

45
from . import __author__, __date__, __version__
46
from . import types as ligolwtypes
47 48
import six
from functools import reduce
49 50


51 52 53 54 55 56 57 58
#
# =============================================================================
#
#                         Document Header, and Indent
#
# =============================================================================
#

kipp's avatar
kipp committed
59

60 61 62
NameSpace = u"http://ldas-sw.ligo.caltech.edu/doc/ligolwAPI/html/ligolw_dtd.txt"


63
Header = u"""<?xml version='1.0' encoding='utf-8'?>
64
<!DOCTYPE LIGO_LW SYSTEM "%s">""" % NameSpace
65

kipp's avatar
kipp committed
66

kipp's avatar
kipp committed
67
Indent = u"\t"
68 69 70 71 72 73 74 75 76 77


#
# =============================================================================
#
#                                Element Class
#
# =============================================================================
#

kipp's avatar
kipp committed
78

79 80 81 82 83 84 85
class ElementError(Exception):
	"""
	Base class for exceptions generated by elements.
	"""
	pass


86
class attributeproxy(property):
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
	"""
	Expose an XML attribute of an Element subclass as Python instance
	attribute with support for an optional default value.

	The .getAttribute() and .setAttribute() methods of the instance to
	which this is attached are used to retrieve and set the unicode
	attribute value, respectively.

	When retrieving a value, the function given via the dec keyword
	argument will be used to convert the unicode into a native Python
	object (the default is to leave the unicode value as unicode).
	When setting a value, the function given via the enc keyword
	argument will be used to convert a native Python object to a
	unicode string.

	When retrieving a value, if .getAttribute() raises KeyError then
	AttributeError is raised unless a default value is provided in
	which case it is returned instead.

	If doc is provided it will be used as the documentation string,
	otherwise a default documentation string will be constructed
	identifying the attribute's name and explaining the default value
	if one is set.

	NOTE:  If an XML document is parsed and an element is encountered
	that does not have a value set for an attribute whose corresponding
	attributeproxy has a default value defined, then Python codes will
	be told the default value.  Therefore, the default value given here
	must match what the XML DTD says the default value is for that
	attribute.  Likewise, attributes for which the DTD does not define
	a default must not have a default defined here.  These conditions
	must both be met to not create a discrepancy between the behaviour
	of Python codes relying on this I/O library and other interfaces to
	the same document.

	Example:

	>>> class Test(Element):
	...	Scale = attributeproxy(u"Scale", enc = u"%.17g".__mod__, dec = float, default = 1.0, doc = "This is the scale (default = 1).")
	...
	>>> x = Test()
	>>> # have not set value, default will be returned
	>>> x.Scale
	1.0
	>>> x.Scale = 16
	>>> x.Scale
	16.0
	>>> # default can be retrieved via the .default attribute of the
	>>> # class attribute
	>>> Test.Scale.default
	1.0
	>>> # default is read-only
	>>> Test.Scale.default = 2.
	Traceback (most recent call last):
	  File "<stdin>", line 1, in <module>
	AttributeError: can't set attribute
	>>> # internally, value is stored as unicode (for XML)
144
	>>> assert x.getAttribute("Scale") == "16"
145 146 147 148 149
	>>> # deleting an attribute restores the default value if defined
	>>> del x.Scale
	>>> x.Scale
	1.0
	"""
150
	def __init__(self, name, enc = six.text_type, dec = six.text_type, default = None, doc = None):
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
		# define get/set/del implementations, relying on Python's
		# closure mechanism to remember values for name, default,
		# etc.
		def getter(self):
			try:
				val = self.getAttribute(name)
			except KeyError:
				if default is not None:
					return default
				raise AttributeError("attribute '%s' is not set" % name)
			return dec(val)
		def setter(self, value):
			self.setAttribute(name, enc(value))
		def deleter(self):
			self.removeAttribute(name)
		# construct a default documentation string if needed
		if doc is None:
			doc = "The \"%s\" attribute." % name
			if default is not None:
				doc += "  Default is \"%s\" if not set." % str(default)
		# initialize the property object
172
		super(attributeproxy, self).__init__(getter, (setter if enc is not None else None), (deleter if enc is not None else None), doc)
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
		# documentation is not inherited, need to set it explicitly
		self.__doc__ = doc
		# record default attribute.  if no value is supplied,
		# AttributeError will be raised on attempts to retrieve it
		if default is not None:
			self._default = default

	@property
	def default(self):
		"""
		Default value.  AttributeError is raised if no default
		value is set.
		"""
		return self._default


189 190
class Element(object):
	"""
kipp's avatar
kipp committed
191 192 193 194 195 196
	Base class for all element types.  This class is inspired by the
	class of the same name in the Python standard library's xml.dom
	package.  One important distinction is that the standard DOM
	element is used to represent the structure of a document at a much
	finer level of detail than here.  For example, in the case of the
	standard DOM element, each XML attribute is its own element being a
197 198
	child node of its tag, while here they are simply stored as
	attributes of the tag element itself.
kipp's avatar
kipp committed
199 200 201 202 203

	Despite the differences, the documentation for the xml.dom package,
	particularly that of the Element class and it's parent, the Node
	class, is useful as supplementary material in understanding how to
	use this class.
204
	"""
kipp's avatar
kipp committed
205
	# XML tag names are case sensitive:  compare with ==, !=, etc.
206
	tagName = None
207
	validchildren = frozenset()
208

209 210 211 212
	@classmethod
	def validattributes(cls):
		return frozenset(name for name in dir(cls) if isinstance(getattr(cls, name), attributeproxy))

213
	def __init__(self, attrs = None):
214 215 216 217 218 219
		"""
		Construct an element.  The argument is a
		sax.xmlreader.AttributesImpl object (see the xml.sax
		documentation, but it's basically a dictionary-like thing)
		used to set the element attributes.
		"""
kipp's avatar
kipp committed
220
		self.parentNode = None
221 222
		if attrs is None:
			self.attributes = AttributesImpl({})
223
		elif set(attrs.keys()) <= self.validattributes():
224 225
			self.attributes = attrs
		else:
226
			raise ElementError("%s element: invalid attribute(s) %s" % (self.tagName, ", ".join("'%s'" % key for key in set(attrs.keys()) - self.validattributes())))
227
		self.childNodes = []
228 229
		self.pcdata = None

230
	def start_tag(self, indent):
231 232 233
		"""
		Generate the string for the element's start tag.
		"""
234
		return u"%s<%s%s>" % (indent, self.tagName, u"".join(u" %s=\"%s\"" % keyvalue for keyvalue in self.attributes.items()))
235

236
	def end_tag(self, indent):
237
		"""
238
		Generate the string for the element's end tag.
239
		"""
240
		return u"%s</%s>" % (indent, self.tagName)
241 242 243

	def appendChild(self, child):
		"""
kipp's avatar
kipp committed
244 245
		Add a child to this element.  The child's parentNode
		attribute is updated, too.
246
		"""
247
		self.childNodes.append(child)
kipp's avatar
kipp committed
248
		child.parentNode = self
249 250 251 252 253 254 255 256 257
		self._verifyChildren(len(self.childNodes) - 1)
		return child

	def insertBefore(self, newchild, refchild):
		"""
		Insert a new child node before an existing child. It must
		be the case that refchild is a child of this node; if not,
		ValueError is raised. newchild is returned.
		"""
258 259
		for i, childNode in enumerate(self.childNodes):
			if childNode is refchild:
260 261 262 263 264
				self.childNodes.insert(i, newchild)
				newchild.parentNode = self
				self._verifyChildren(i)
				return newchild
		raise ValueError(refchild)
265

266 267 268
	def removeChild(self, child):
		"""
		Remove a child from this element.  The child element is
kipp's avatar
kipp committed
269 270 271
		returned, and it's parentNode element is reset.  If the
		child will not be used any more, you should call its
		unlink() method to promote garbage collection.
272
		"""
273 274
		for i, childNode in enumerate(self.childNodes):
			if childNode is child:
275 276 277 278
				del self.childNodes[i]
				child.parentNode = None
				return child
		raise ValueError(child)
279

kipp's avatar
kipp committed
280 281 282
	def unlink(self):
		"""
		Break internal references within the document tree rooted
283
		on this element to promote garbage collection.
kipp's avatar
kipp committed
284 285 286 287
		"""
		self.parentNode = None
		for child in self.childNodes:
			child.unlink()
288
		del self.childNodes[:]
kipp's avatar
kipp committed
289

290 291 292 293 294 295
	def replaceChild(self, newchild, oldchild):
		"""
		Replace an existing node with a new node. It must be the
		case that oldchild is a child of this node; if not,
		ValueError is raised. newchild is returned.
		"""
296 297 298
		# .index() would use compare-by-value, we want
		# compare-by-id because we want to find the exact object,
		# not something equivalent to it.
299 300
		for i, childNode in enumerate(self.childNodes):
			if childNode is oldchild:
301
				childNode.parentNode = None
302 303 304 305 306
				self.childNodes[i] = newchild
				newchild.parentNode = self
				self._verifyChildren(i)
				return newchild
		raise ValueError(oldchild)
307

kipp's avatar
kipp committed
308 309
	def getElements(self, filter):
		"""
310 311
		Return a list of elements below and including this element
		for which filter(element) returns True.
kipp's avatar
kipp committed
312 313 314
		"""
		l = reduce(lambda l, e: l + e.getElements(filter), self.childNodes, [])
		if filter(self):
315
			l.append(self)
kipp's avatar
kipp committed
316 317
		return l

318
	def getElementsByTagName(self, tagName):
kipp's avatar
kipp committed
319
		return self.getElements(lambda e: e.tagName == tagName)
320

321
	def getChildrenByAttributes(self, attrs):
322
		l = []
Kipp Cannon's avatar
Kipp Cannon committed
323
		attrs = tuple(attrs.items())
324 325
		for c in self.childNodes:
			try:
Kipp Cannon's avatar
Kipp Cannon committed
326
				if all(c.getAttribute(name) == value for name, value in attrs):
327 328 329 330 331
					l.append(c)
			except KeyError:
				pass
		return l

332
	def hasAttribute(self, attrname):
333
		return attrname in self.attributes
334

335 336 337 338
	def getAttribute(self, attrname):
		return self.attributes[attrname]

	def setAttribute(self, attrname, value):
kipp's avatar
kipp committed
339 340 341 342
		# cafeful:  this digs inside an AttributesImpl object and
		# modifies its internal data.  probably not a good idea,
		# but I don't know how else to edit an attribute because
		# the stupid things don't export a method to do it.
343
		self.attributes._attrs[attrname] = six.text_type(value)
344

Kipp Cannon's avatar
Kipp Cannon committed
345 346 347 348 349 350 351 352 353 354
	def removeAttribute(self, attrname):
		# cafeful:  this digs inside an AttributesImpl object and
		# modifies its internal data.  probably not a good idea,
		# but I don't know how else to edit an attribute because
		# the stupid things don't export a method to do it.
		try:
			del self.attributes._attrs[attrname]
		except KeyError:
			pass

355 356 357 358
	def appendData(self, content):
		"""
		Add characters to the element's pcdata.
		"""
359
		if self.pcdata is not None:
360 361 362 363
			self.pcdata += content
		else:
			self.pcdata = content

364 365 366 367 368 369 370 371 372
	def _verifyChildren(self, i):
		"""
		Method used internally by some elements to verify that
		their children are from the allowed set and in the correct
		order following modifications to their child list.  i is
		the index of the child that has just changed.
		"""
		pass

373 374 375 376 377 378 379
	def endElement(self):
		"""
		Method invoked by document parser when it encounters the
		end-of-element event.
		"""
		pass

380
	def write(self, fileobj = sys.stdout, indent = u""):
381 382 383
		"""
		Recursively write an element and it's children to a file.
		"""
384 385
		fileobj.write(self.start_tag(indent))
		fileobj.write(u"\n")
386 387
		for c in self.childNodes:
			if c.tagName not in self.validchildren:
388
				raise ElementError("invalid child %s for %s" % (c.tagName, self.tagName))
389
			c.write(fileobj, indent + Indent)
390
		if self.pcdata is not None:
391 392
			fileobj.write(xmlescape(self.pcdata))
			fileobj.write(u"\n")
393 394
		fileobj.write(self.end_tag(indent))
		fileobj.write(u"\n")
395 396


397 398 399 400 401 402 403 404 405
class EmptyElement(Element):
	"""
	Parent class for Elements that cannot contain text.
	"""
	def appendData(self, content):
		if not content.isspace():
			raise TypeError("%s does not hold text" % type(self))


kipp's avatar
kipp committed
406 407 408 409 410
def WalkChildren(elem):
	"""
	Walk the XML tree of children below elem, returning each in order.
	"""
	for child in elem.childNodes:
411
		yield child
kipp's avatar
kipp committed
412
		for elem in WalkChildren(child):
kipp's avatar
kipp committed
413 414 415
			yield elem


416 417 418 419 420 421 422 423 424
#
# =============================================================================
#
#                         Name Attribute Manipulation
#
# =============================================================================
#


425
class LLWNameAttr(six.text_type):
426 427 428 429 430 431
	"""
	Baseclass to hide pattern-matching of various element names.
	Subclasses must provide a .dec_pattern compiled regular expression
	defining a group "Name" that identifies the meaningful portion of
	the string, and a .enc_pattern that gives a format string to be
	used with "%" to reconstrct the full string.
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448

	This is intended to be used to provide the enc and dec functions
	for an attributeproxy instance.

	Example:

	>>> import re
	>>> class Test(Element):
	...	class TestName(LLWNameAttr):
	...		dec_pattern = re.compile(r"(?P<Name>[a-z0-9_]+):test\Z")
	...		enc_pattern = u"%s:test"
	...
	...	Name = attributeproxy(u"Name", enc = TestName.enc, dec = TestName)
	...
	>>> x = Test()
	>>> x.Name = u"blah"
	>>> # internally, suffix has been appended
449 450
	>>> print(x.getAttribute("Name"))
	blah:test
451
	>>> # but attributeproxy reports original value
452 453
	>>> print(x.Name)
	blah
Kipp Cannon's avatar
Kipp Cannon committed
454 455 456 457 458
	>>> # only lower-case Latin letters, numerals, and '_' are allowed
	>>> x.Name = u"Hello-world"
	Traceback (most recent call last):
	  File "<stdin>", line 1, in <module>
	ValueError: invalid Name 'Hello-world'
459 460 461 462 463 464
	"""
	def __new__(cls, name):
		try:
			name = cls.dec_pattern.search(name).group(u"Name")
		except AttributeError:
			pass
465
		return name
466 467 468

	@classmethod
	def enc(cls, name):
Kipp Cannon's avatar
Kipp Cannon committed
469 470 471 472 473
		s = cls.enc_pattern % name
		# confirm invertiblity
		if cls(s) != name:
			raise ValueError("invalid Name '%s'" % name)
		return s
474 475


476 477 478 479 480 481 482 483
#
# =============================================================================
#
#                        LIGO Light Weight XML Elements
#
# =============================================================================
#

kipp's avatar
kipp committed
484

485
class LIGO_LW(EmptyElement):
486 487 488
	"""
	LIGO_LW element.
	"""
489
	tagName = u"LIGO_LW"
490
	validchildren = frozenset([u"LIGO_LW", u"Comment", u"Param", u"Table", u"Array", u"Stream", u"IGWDFrame", u"AdcData", u"AdcInterval", u"Time", u"Detector"])
491

492 493 494
	Name = attributeproxy(u"Name")
	Type = attributeproxy(u"Type")

495 496 497 498 499

class Comment(Element):
	"""
	Comment element.
	"""
500
	tagName = u"Comment"
501

502
	def write(self, fileobj = sys.stdout, indent = u""):
503
		fileobj.write(self.start_tag(indent))
504
		if self.pcdata is not None:
505
			fileobj.write(xmlescape(self.pcdata))
506 507
		fileobj.write(self.end_tag(u""))
		fileobj.write(u"\n")
508 509 510 511 512 513


class Param(Element):
	"""
	Param element.
	"""
514
	tagName = u"Param"
515
	validchildren = frozenset([u"Comment"])
516

517 518 519 520 521 522
	DataUnit = attributeproxy(u"DataUnit")
	Name = attributeproxy(u"Name")
	Scale = attributeproxy(u"Scale")
	Start = attributeproxy(u"Start")
	Type = attributeproxy(u"Type")
	Unit = attributeproxy(u"Unit")
523

524

525
class Table(EmptyElement):
526 527 528
	"""
	Table element.
	"""
529
	tagName = u"Table"
530
	validchildren = frozenset([u"Comment", u"Column", u"Stream"])
531

Kipp Cannon's avatar
Kipp Cannon committed
532 533 534
	Name = attributeproxy(u"Name")
	Type = attributeproxy(u"Type")

535
	def _verifyChildren(self, i):
536 537 538
		ncomment = 0
		ncolumn = 0
		nstream = 0
539
		for child in self.childNodes:
540
			if child.tagName == Comment.tagName:
541
				if ncomment:
542
					raise ElementError("only one Comment allowed in Table")
543
				if ncolumn or nstream:
544
					raise ElementError("Comment must come before Column(s) and Stream in Table")
545
				ncomment += 1
546
			elif child.tagName == Column.tagName:
547
				if nstream:
548
					raise ElementError("Column(s) must come before Stream in Table")
549 550 551
				ncolumn += 1
			else:
				if nstream:
552
					raise ElementError("only one Stream allowed in Table")
553 554 555
				nstream += 1


556
class Column(EmptyElement):
557 558 559
	"""
	Column element.
	"""
560
	tagName = u"Column"
561

Kipp Cannon's avatar
Kipp Cannon committed
562 563 564 565
	Name = attributeproxy(u"Name")
	Type = attributeproxy(u"Type")
	Unit = attributeproxy(u"Unit")

566 567 568 569
	def start_tag(self, indent):
		"""
		Generate the string for the element's start tag.
		"""
570
		return u"%s<%s%s/>" % (indent, self.tagName, u"".join(u" %s=\"%s\"" % keyvalue for keyvalue in self.attributes.items()))
571 572 573 574 575

	def end_tag(self, indent):
		"""
		Generate the string for the element's end tag.
		"""
kipp's avatar
kipp committed
576
		return u""
577

578
	def write(self, fileobj = sys.stdout, indent = u""):
579 580 581
		"""
		Recursively write an element and it's children to a file.
		"""
582 583
		fileobj.write(self.start_tag(indent))
		fileobj.write(u"\n")
584

585

586
class Array(EmptyElement):
587 588 589
	"""
	Array element.
	"""
590
	tagName = u"Array"
591
	validchildren = frozenset([u"Dim", u"Stream"])
592

Kipp Cannon's avatar
Kipp Cannon committed
593 594 595 596
	Name = attributeproxy(u"Name")
	Type = attributeproxy(u"Type")
	Unit = attributeproxy(u"Unit")

kipp's avatar
kipp committed
597
	def _verifyChildren(self, i):
598
		nstream = 0
599
		for child in self.childNodes:
600
			if child.tagName == Dim.tagName:
601
				if nstream:
602
					raise ElementError("Dim(s) must come before Stream in Array")
603 604
			else:
				if nstream:
605
					raise ElementError("only one Stream allowed in Array")
606 607 608 609 610 611 612
				nstream += 1


class Dim(Element):
	"""
	Dim element.
	"""
613
	tagName = u"Dim"
614

Kipp Cannon's avatar
Kipp Cannon committed
615 616 617 618 619
	Name = attributeproxy(u"Name")
	Scale = attributeproxy(u"Scale", enc = ligolwtypes.FormatFunc[u"real_8"], dec = ligolwtypes.ToPyType[u"real_8"])
	Start = attributeproxy(u"Start", enc = ligolwtypes.FormatFunc[u"real_8"], dec = ligolwtypes.ToPyType[u"real_8"])
	Unit = attributeproxy(u"Unit")

Kipp Cannon's avatar
Kipp Cannon committed
620 621 622 623 624 625 626 627 628 629 630 631
	@property
	def n(self):
		return ligolwtypes.ToPyType[u"int_8s"](self.pcdata) if self.pcdata is not None else None

	@n.setter
	def n(self, val):
		self.pcdata = ligolwtypes.FormatFunc[u"int_8s"](val) if val is not None else None

	@n.deleter
	def n(self):
		self.pcdata = None

632
	def write(self, fileobj = sys.stdout, indent = u""):
633
		fileobj.write(self.start_tag(indent))
634
		if self.pcdata is not None:
635
			fileobj.write(xmlescape(self.pcdata))
636 637
		fileobj.write(self.end_tag(u""))
		fileobj.write(u"\n")
638

639 640 641 642 643

class Stream(Element):
	"""
	Stream element.
	"""
644
	tagName = u"Stream"
645

646 647 648 649 650
	Content = attributeproxy(u"Content")
	Delimiter = attributeproxy(u"Delimiter", default = u",")
	Encoding = attributeproxy(u"Encoding")
	Name = attributeproxy(u"Name")
	Type = attributeproxy(u"Type", default = u"Local")
651

Kipp Cannon's avatar
Kipp Cannon committed
652 653 654 655 656
	def __init__(self, *args):
		super(Stream, self).__init__(*args)
		if self.Type not in (u"Remote", u"Local"):
			raise ElementError("invalid Type for Stream: '%s'" % self.Type)

657

658
class IGWDFrame(EmptyElement):
659 660 661
	"""
	IGWDFrame element.
	"""
662
	tagName = u"IGWDFrame"
663
	validchildren = frozenset([u"Comment", u"Param", u"Time", u"Detector", u"AdcData", u"LIGO_LW", u"Stream", u"Array", u"IGWDFrame"])
664

665 666
	Name = attributeproxy(u"Name")

667

668
class Detector(EmptyElement):
669 670 671
	"""
	Detector element.
	"""
672
	tagName = u"Detector"
673
	validchildren = frozenset([u"Comment", u"Param", u"LIGO_LW"])
674

675 676
	Name = attributeproxy(u"Name")

677

678
class AdcData(EmptyElement):
679 680 681
	"""
	AdcData element.
	"""
682
	tagName = u"AdcData"
683
	validchildren = frozenset([u"AdcData", u"Comment", u"Param", u"Time", u"LIGO_LW", u"Array"])
684

685 686
	Name = attributeproxy(u"Name")

687

688
class AdcInterval(EmptyElement):
689 690 691
	"""
	AdcInterval element.
	"""
692
	tagName = u"AdcInterval"
693
	validchildren = frozenset([u"AdcData", u"Comment", u"Time"])
694 695 696 697

	DeltaT = attributeproxy(u"DeltaT", enc = ligolwtypes.FormatFunc[u"real_8"], dec = ligolwtypes.ToPyType[u"real_8"])
	Name = attributeproxy(u"Name")
	StartTime = attributeproxy(u"StartTime")
698 699 700 701 702 703


class Time(Element):
	"""
	Time element.
	"""
704
	tagName = u"Time"
705

Kipp Cannon's avatar
Kipp Cannon committed
706 707 708
	Name = attributeproxy(u"Name")
	Type = attributeproxy(u"Type", default = u"ISO-8601")

709 710 711 712
	def __init__(self, *args):
		super(Time, self).__init__(*args)
		if self.Type not in ligolwtypes.TimeTypes:
			raise ElementError("invalid Type for Time: '%s'" % self.Type)
713

Kipp Cannon's avatar
Kipp Cannon committed
714 715 716 717
	def endElement(self):
		if self.Type == u"ISO-8601":
			self.pcdata = dateutil.parser.parse(self.pcdata)
		elif self.Type == u"GPS":
718
			from lal import LIGOTimeGPS
719 720 721
			# FIXME:  remove cast to string when lal swig
			# can cast from unicode
			self.pcdata = LIGOTimeGPS(str(self.pcdata))
Kipp Cannon's avatar
Kipp Cannon committed
722 723
		elif self.Type == u"Unix":
			self.pcdata = float(self.pcdata)
724
		else:
Kipp Cannon's avatar
Kipp Cannon committed
725 726
			# unsupported time type.  not impossible that
			# calling code has overridden TimeTypes set in
727
			# ligo.lw.types;  just accept it as a string
Kipp Cannon's avatar
Kipp Cannon committed
728 729 730 731 732 733
			pass

	def write(self, fileobj = sys.stdout, indent = u""):
		fileobj.write(self.start_tag(indent))
		if self.pcdata is not None:
			if self.Type == u"ISO-8601":
734
				fileobj.write(xmlescape(six.text_type(self.pcdata.isoformat())))
Kipp Cannon's avatar
Kipp Cannon committed
735
			elif self.Type == u"GPS":
736
				fileobj.write(xmlescape(six.text_type(self.pcdata)))
Kipp Cannon's avatar
Kipp Cannon committed
737 738 739 740 741 742 743 744
			elif self.Type == u"Unix":
				fileobj.write(xmlescape(u"%.16g" % self.pcdata))
			else:
				# unsupported time type.  not impossible.
				# assume correct thing to do is cast to
				# unicode and let calling code figure out
				# how to ensure that does the correct
				# thing.
745
				fileobj.write(xmlescape(six.text_type(self.pcdata)))
746 747
		fileobj.write(self.end_tag(u""))
		fileobj.write(u"\n")
Kipp Cannon's avatar
Kipp Cannon committed
748 749 750

	@classmethod
	def now(cls, Name = None):
751 752 753 754 755
		"""
		Instantiate a Time element initialized to the current UTC
		time in the default format (ISO-8601).  The Name attribute
		will be set to the value of the Name parameter if given.
		"""
Kipp Cannon's avatar
Kipp Cannon committed
756 757 758 759 760 761 762 763
		self = cls()
		if Name is not None:
			self.Name = Name
		self.pcdata = datetime.datetime.utcnow()
		return self

	@classmethod
	def from_gps(cls, gps, Name = None):
764 765 766 767
		"""
		Instantiate a Time element initialized to the value of the
		given GPS time.  The Name attribute will be set to the
		value of the Name parameter if given.
768 769 770 771

		Note:  the new Time element holds a reference to the GPS
		time, not a copy of it.  Subsequent modification of the GPS
		time object will be reflected in what gets written to disk.
772
		"""
Kipp Cannon's avatar
Kipp Cannon committed
773 774 775 776 777
		self = cls(AttributesImpl({u"Type": u"GPS"}))
		if Name is not None:
			self.Name = Name
		self.pcdata = gps
		return self
778

779

780
class Document(EmptyElement):
781 782 783
	"""
	Description of a LIGO LW file.
	"""
784
	tagName = u"Document"
785
	validchildren = frozenset([u"LIGO_LW"])
786

787
	def write(self, fileobj = sys.stdout, xsl_file = None):
788 789 790
		"""
		Write the document.
		"""
791 792
		fileobj.write(Header)
		fileobj.write(u"\n")
793
		if xsl_file is not None:
794
			fileobj.write(u'<?xml-stylesheet type="text/xsl" href="%s" ?>\n' % xsl_file)
795
		for c in self.childNodes:
796
			if c.tagName not in self.validchildren:
797
				raise ElementError("invalid child %s for %s" % (c.tagName, self.tagName))
798
			c.write(fileobj)
799 800


801 802 803 804 805 806 807 808
#
# =============================================================================
#
#                             SAX Content Handler
#
# =============================================================================
#

kipp's avatar
kipp committed
809

810
class LIGOLWContentHandler(sax.handler.ContentHandler, object):
811 812 813 814 815 816
	"""
	ContentHandler class for parsing LIGO Light Weight documents with a
	SAX2-compliant parser.

	Example:

817 818
	>>> # initialize empty Document tree into which parsed XML tree
	>>> # will be inserted
819
	>>> xmldoc = Document()
820
	>>> # create handler instance attached to Document object
821
	>>> handler = LIGOLWContentHandler(xmldoc)
822 823 824
	>>> # open file and parse
	>>> make_parser(handler).parse(open("demo.xml"))
	>>> # write XML (default to stdout)
825 826 827
	>>> xmldoc.write()

	NOTE:  this example is for illustration only.  Most users will wish
828 829
	to use the .load_*() functions in the ligo.lw.utils subpackage to
	load documents, and the .write_*() functions to write documents.
830 831 832
	Those functions provide additional features such as support for
	gzip'ed documents, MD5 hash computation, and Condor eviction
	trapping to avoid writing broken documents to disk.
833 834 835

	See also:  PartialLIGOLWContentHandler,
	FilteringLIGOLWContentHandler.
836
	"""
837 838

	def __init__(self, document, start_handlers = {}):
839 840 841 842
		"""
		Initialize the handler by pointing it to the Document object
		into which the parsed file will be loaded.
		"""
843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862
		self.current = self.document = document

		self._startElementHandlers = {
			(None, AdcData.tagName): self.startAdcData,
			(None, AdcInterval.tagName): self.startAdcInterval,
			(None, Array.tagName): self.startArray,
			(None, Column.tagName): self.startColumn,
			(None, Comment.tagName): self.startComment,
			(None, Detector.tagName): self.startDetector,
			(None, Dim.tagName): self.startDim,
			(None, IGWDFrame.tagName): self.startIGWDFrame,
			(None, LIGO_LW.tagName): self.startLIGO_LW,
			(None, Param.tagName): self.startParam,
			(None, Stream.tagName): self.startStream,
			(None, Table.tagName): self.startTable,
			(None, Time.tagName): self.startTime,
		}
		self._startElementHandlers.update(start_handlers)

	def startAdcData(self, parent, attrs):
863 864
		return AdcData(attrs)

865
	def startAdcInterval(self, parent, attrs):
866 867
		return AdcInterval(attrs)

868
	def startArray(self, parent, attrs):
869 870
		return Array(attrs)

871
	def startColumn(self, parent, attrs):
872 873
		return Column(attrs)

874
	def startComment(self, parent, attrs):
875 876
		return Comment(attrs)

877
	def startDetector(self, parent, attrs):
878 879
		return Detector(attrs)

880
	def startDim(self, parent, attrs):
881 882
		return Dim(attrs)

883
	def startIGWDFrame(self, parent, attrs):
884 885
		return IGWDFrame(attrs)

886
	def startLIGO_LW(self, parent, attrs):
887 888
		return LIGO_LW(attrs)

889
	def startParam(self, parent, attrs):
890 891
		return Param(attrs)

892
	def startStream(self, parent, attrs):
893 894
		return Stream(attrs)

895
	def startTable(self, parent, attrs):
896 897
		return Table(attrs)

898
	def startTime(self, parent, attrs):
899 900
		return Time(attrs)

901 902
	def startElementNS(self, uri_localname, qname, attrs):
		(uri, localname) = uri_localname
903 904 905 906
		try:
			start_handler = self._startElementHandlers[(uri, localname)]
		except KeyError:
			raise ElementError("unknown element %s for namespace %s" % (localname, uri or NameSpace))
907
		attrs = AttributesImpl(dict((attrs.getQNameByName(name), value) for name, value in attrs.items()))
908 909 910 911
		try:
			self.current = self.current.appendChild(start_handler(self.current, attrs))
		except Exception as e:
			raise type(e)("line %d: %s" % (self._locator.getLineNumber(), str(e)))
912

913 914
	def endElementNS(self, uri_localname, qname):
		(uri, localname) = uri_localname
915 916 917 918
		try:
			self.current.endElement()
		except Exception as e:
			raise type(e)("line %d: %s" % (self._locator.getLineNumber(), str(e)))
919
		self.current = self.current.parentNode
920

921
	def characters(self, content):
922 923 924 925
		try:
			self.current.appendData(xmlunescape(content))
		except Exception as e:
			raise type(e)("line %d: %s" % (self._locator.getLineNumber(), str(e)))
926 927


928
class PartialLIGOLWContentHandler(LIGOLWContentHandler):
929
	"""
930 931 932
	LIGO LW content handler object that loads only those parts of the
	document matching some criteria.  Useful, for example, when one
	wishes to read only a single table from a file.
933 934 935

	Example:

936
	>>> from ligo.lw import utils as ligolw_utils
937
	>>> def contenthandler(document):
938
	...	return PartialLIGOLWContentHandler(document, lambda name, attrs: name == Table.tagName)
939
	...
940
	>>> xmldoc = ligolw_utils.load_filename("demo.xml", contenthandler = contenthandler)
941

942
	This parses "demo.xml" and returns an XML tree containing only the
943
	Table elements and their children.
944 945 946 947 948 949 950
	"""
	def __init__(self, document, element_filter):
		"""
		Only those elements for which element_filter(name, attrs)
		evaluates to True, and the children of those elements, will
		be loaded.
		"""
951
		super(PartialLIGOLWContentHandler, self).__init__(document)
952
		self.element_filter = element_filter
953
		self.depth = 0
954

955 956
	def startElementNS(self, uri_localname, qname, attrs):
		(uri, localname) = uri_localname
957
		filter_attrs = AttributesImpl(dict((attrs.getQNameByName(name), value) for name, value in attrs.items()))
958 959
		if self.depth > 0 or self.element_filter(localname, filter_attrs):
			super(PartialLIGOLWContentHandler, self).startElementNS((uri, localname), qname, attrs)
960
			self.depth += 1
961

962
	def endElementNS(self, *args):
963 964
		if self.depth > 0:
			self.depth -= 1
965
			super(PartialLIGOLWContentHandler, self).endElementNS(*args)
966

967 968 969 970
	def characters(self, content):
		if self.depth > 0:
			super(PartialLIGOLWContentHandler, self).characters(content)

971

972
class FilteringLIGOLWContentHandler(LIGOLWContentHandler):
973 974 975 976
	"""
	LIGO LW content handler that loads everything but those parts of a
	document that match some criteria.  Useful, for example, when one
	wishes to read everything except a single table from a file.
977 978 979

	Example:

980
	>>> from ligo.lw import utils as ligolw_utils
981
	>>> def contenthandler(document):
982
	...	return FilteringLIGOLWContentHandler(document, lambda name, attrs: name != Table.tagName)
983
	...
984
	>>> xmldoc = ligolw_utils.load_filename("demo.xml", contenthandler = contenthandler)
985

986
	This parses "demo.xml" and returns an XML tree with all the Table
987
	elements and their children removed.
988 989 990 991 992 993 994
	"""
	def __init__(self, document, element_filter):
		"""
		Those elements for which element_filter(name, attrs)
		evaluates to False, and the children of those elements,
		will not be loaded.
		"""
995
		super(FilteringLIGOLWContentHandler, self).__init__(document)
996 997 998
		self.element_filter = element_filter
		self.depth = 0

999 1000
	def startElementNS(self, uri_localname, qname, attrs):
		(uri, localname) = uri_localname