Source code for pyvo.io.uws.tree

# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
This file contains xml element classes as defined in the VOResource standard.
"""
from functools import partial

from astropy.utils.collections import HomogeneousList
from astropy.time import Time, TimeDelta

from ...utils.xml.elements import (
    xmlattribute, xmlelement, Element, ContentMixin)

uwselement = partial(xmlelement, ns='uws')


def XSInDate(val):
    if not val:
        return None

    try:
        return Time(val, format='iso')
    except ValueError:
        pass

    try:
        return Time(val, format='isot')
    except ValueError:
        pass

    raise ValueError(f'Cannot parse datetime {val}')


InDuration = partial(TimeDelta, format='sec')
XSOutDate = partial(Time, out_subfmt='date')


__all__ = [
    'UWSElement', 'Reference', 'JobSummary', 'Parameters', 'Parameter',
    'Results', 'Result', 'ExtensibleUWSElement', 'Jobs', 'JobInfo']


def _convert_boolean(value, default=None):
    return {
        'false': False,
        '0': False,
        'true': True,
        '1': True
    }.get(value, default)


[docs] class UWSElement(Element): def __init__(self, config=None, pos=None, _name='', _ns='uws', **kwargs): super().__init__(config, pos, _name, 'uws', **kwargs)
[docs] class Reference(UWSElement): """standard xlink references""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.type = kwargs.get('xlink:type') self.href = kwargs.get('xlink:href') @xmlattribute(name='xlink:type') def type(self): """the type of the result""" return self._type @type.setter def type(self, type_): self._type = type_ @xmlattribute(name='xlink:href') def href(self): """the url the result can be retrieved""" return self._href @href.setter def href(self, href): self._href = href
[docs] class JobSummary(Element): def __init__(self, config=None, pos=None, _name='job', **kwargs): super().__init__(config, pos, _name, **kwargs) self.jobid = kwargs.get('id') self._runid = None self._ownerid = None self._phase = None self._quote = None self._creationtime = None self._starttime = None self._endtime = None self._executionduration = None self._destruction = None self._parameters = Parameters() self._results = Results() self._errorsummary = None self._message = None self._jobinfo = None @uwselement(name='jobId', plain=True) def jobid(self): """ The identifier for the job """ return self._jobid @jobid.setter def jobid(self, jobid): self._jobid = jobid @uwselement(name='runId', plain=True) def runid(self): """client supplied identifier""" return self._runid @runid.setter def runid(self, runid): self._runid = runid @uwselement(name='ownerId', plain=True) def ownerid(self): """the owner (creator) of the job""" return self._ownerid @ownerid.setter def ownerid(self, ownerid): self._ownerid = ownerid @uwselement(plain=True) def phase(self): """the execution phase""" return self._phase @phase.setter def phase(self, phase): self._phase = phase @uwselement(plain=True) def quote(self): """estimated completion time""" return self._quote @quote.setter def quote(self, quote): self._quote = XSInDate(quote) @quote.formatter def quote(self): try: return str(XSOutDate(self._quote)) except ValueError: return None @uwselement(name='creationTime', plain=True) def creationtime(self): """The instant at which the job was created.""" return self._creationtime @creationtime.setter def creationtime(self, creationtime): self._creationtime = XSInDate(creationtime) @creationtime.formatter def creationtime(self): try: return str(XSOutDate(self._creationtime)) except ValueError: return None @uwselement(name='startTime', plain=True) def starttime(self): """The instant at which the job started execution.""" return self._starttime @starttime.setter def starttime(self, starttime): self._starttime = XSInDate(starttime) @starttime.formatter def starttime(self): try: return str(XSOutDate(self._starttime)) except ValueError: return None @uwselement(name='endTime', plain=True) def endtime(self): """The instant at which the job finished execution""" return self._endtime @endtime.setter def endtime(self, endtime): self._endtime = XSInDate(endtime) @endtime.formatter def endtime(self): try: return str(XSOutDate(self._endtime)) except ValueError: return None @uwselement(name='executionDuration', plain=True) def executionduration(self): """ The duration (in seconds) for which the job should be allowed to run - a value of 0 is intended to mean unlimited """ return self._executionduration @executionduration.setter def executionduration(self, executionduration): if not isinstance(executionduration, TimeDelta): executionduration = InDuration(float(executionduration)) self._executionduration = executionduration @executionduration.formatter def executionduration(self): if self.executionduration: return str(int(self._executionduration.value)) @uwselement(plain=True) def destruction(self): """The time at which the whole job will be destroyed""" return self._destruction @destruction.setter def destruction(self, destruction): self._destruction = XSInDate(destruction) @destruction.formatter def destruction(self): try: return str(XSOutDate(self._destruction)) except ValueError: return None @uwselement def parameters(self): """The parameters to the job""" return self._parameters @parameters.adder def parameters(self, iterator, tag, data, config, pos): parameters = Parameters(config, pos, 'parameters', **data) parameters.parse(iterator, config) self._parameters = parameters @uwselement def results(self): """The results for the job""" return self._results @results.adder def results(self, iterator, tag, data, config, pos): results = Results(config, pos, 'results', **data) results.parse(iterator, config) self._results = results @uwselement(name='errorSummary', plain=True) def errorsummary(self): """The error summary of the job.""" return self._errorsummary @errorsummary.adder def errorsummary(self, iterator, tag, data, config, pos): res = ErrorSummary(config, pos, 'errorSummary', **data) res.parse(iterator, config) self._errorsummary = res @uwselement(name='jobInfo', plain=True) # ← Add plain=True def jobinfo(self): """Implementation-specific job information""" return self._jobinfo @jobinfo.adder def jobinfo(self, iterator, tag, data, config, pos): jobinfo = JobInfo(config, pos, 'jobInfo', **data) jobinfo.parse(iterator, config) self._jobinfo = jobinfo
[docs] class Jobs(HomogeneousList, UWSElement): """A parsed representation of the joblist endpoint. """ def __init__(self, config=None, pos=None, _name='jobs', **kwargs): HomogeneousList.__init__(self, JobSummary) UWSElement.__init__(self, config, pos, _name, **kwargs) @uwselement def jobs(self): return self @jobs.adder def jobs(self, iterator, tag, data, config, pos): return @uwselement(name='jobref') def joblist(self): return self @joblist.adder def joblist(self, iterator, tag, data, config, pos): job = JobSummary(config, pos, 'jobref', **data) job.parse(iterator, config) self.append(job)
[docs] class Parameters(UWSElement, HomogeneousList): """ Parameters element of a job """ def __init__(self, config=None, pos=None, _name='parameters', **kwargs): """ """ # Note: Above is a load-bearing empty comment. # Do not remove, or else the Sphinx build may fail (see PR #193). HomogeneousList.__init__(self, Parameter) UWSElement.__init__(self, config, pos, _name, **kwargs) @uwselement(name='parameter') def parameters(self): return self @parameters.adder def parameters(self, iterator, tag, data, config, pos): parameter = Parameter(config, pos, 'parameter', **data) parameter.parse(iterator, config) self.append(parameter)
[docs] class Parameter(ContentMixin, UWSElement): def __init__(self, config=None, pos=None, _name='parameter', **kwargs): super().__init__(config, pos, _name, **kwargs) self.byreference = _convert_boolean(kwargs.get('byReference')) self.id_ = kwargs.get('id') @xmlattribute def byreference(self): """ if this attribute is true then the content of the parameter represents a URL to retrieve the actual parameter value. """ return self._byreference @byreference.setter def byreference(self, byreference): self._byreference = byreference @xmlattribute(name='id') def id_(self): """the identifier for the parameter""" return self._id @id_.setter def id_(self, id_): self._id = id_
[docs] class Results(UWSElement, HomogeneousList): """ """ def __init__(self, config=None, pos=None, _name='results', **kwargs): HomogeneousList.__init__(self, Result) UWSElement.__init__(self, config, pos, _name, **kwargs) @uwselement(name='result') def results(self): return self @results.adder def results(self, iterator, tag, data, config, pos): result = Result(config, pos, 'result', **data) result.parse(iterator, config) self.append(result)
[docs] class Result(Reference, UWSElement): """A reference to a UWS result.""" def __init__(self, config=None, pos=None, _name='result', **kwargs): super().__init__(config, pos, _name, **kwargs) self.id_ = kwargs.get('id') self.size = int(kwargs.get('size') or 0) self.mimetype = kwargs.get('mime-type') @xmlattribute(name='id') def id_(self): """the identifier for the result""" return self._id @id_.setter def id_(self, id_): self._id = id_ @xmlattribute def size(self): """the size of the result""" return self._size @size.setter def size(self, size): self._size = size @xmlattribute def mimetype(self): """the mimetype of the result""" return self._mimetype @mimetype.setter def mimetype(self, mimetype): self._mimetype = mimetype
class ErrorSummary(UWSElement): """A UWS Error summary.""" def __init__(self, config=None, pos=None, _name='errorSummary', **kwargs): super().__init__(config, pos, _name, **kwargs) self.type_ = kwargs.get('type') self.has_detail = _convert_boolean(kwargs.get('hasDetail')) self.message = None @xmlattribute(name='type') def type_(self): """the type of the error""" return self._type @type_.setter def type_(self, type_): self._type = type_ @xmlattribute def has_detail(self): """whether error has details""" return self._has_detail @has_detail.setter def has_detail(self, has_detail): self._has_detail = has_detail @uwselement(name='message') def message(self): """The error message""" return self._message @message.setter def message(self, message): self._message = message class Message(ContentMixin, UWSElement): """The actual UWS Error message.""" def __init__(self, config=None, pos=None, _name='message', **kwargs): super().__init__(config, pos, _name, **kwargs)
[docs] class ExtensibleUWSElement(ContentMixin, UWSElement): """ UWS Element that can handle arbitrary child elements. """ def __init__(self, config=None, pos=None, _name='', **kwargs): super().__init__(config, pos, _name, **kwargs) self._elements = {} self._text_content = None self._name = _name def _add_unknown_tag(self, iterator, tag, data, config, pos): """Handle unknown tags without generating warnings Parameters ---------- iterator : iterator The iterator that provides the XML elements. tag : str The tag name of the unknown element. data : dict Additional data associated. config : dict Configuration options. pos : tuple The position of the element in the XML document (line, column). Returns ------- ExtensibleUWSElement object """ element = ExtensibleUWSElement(config, pos, tag, **data) element.parse(iterator, config) # Last element with the same tag wins self._elements[tag] = element return element
[docs] def parse(self, iterator, config): """Override parse to capture text content for leaf elements""" super().parse(iterator, config) # Capture text content from ContentMixin if hasattr(self, 'content') and self.content is not None: if isinstance(self.content, str): self._text_content = self.content.strip() else: self._text_content = str(self.content).strip() if self.content else None
@property def text(self): """Get the text content of this element""" if self._text_content is not None and self._text_content.strip(): return self._text_content if hasattr(self, 'content') and self.content is not None: content_str = str(self.content).strip() return content_str if content_str else None return None @property def value(self): """Get the text content converted to appropriate type""" text = self.text if not text: return None # Try to convert to int, float, if not leave as string try: return int(text) except ValueError: try: return float(text) except ValueError: return text
[docs] def get(self, name, default=None): """Get element by name (supports both local names and full namespaced names)""" return self._elements.get(name, default)
[docs] def keys(self): """Return all available keys (both local and namespaced)""" return list(self._elements.keys())
def __contains__(self, name): """Support 'in' operator""" return name in self._elements def __getitem__(self, name): """Dict-like access""" if name not in self._elements: raise KeyError(f"Element '{name}' not found") return self._elements[name] def __str__(self): if self._text_content: return self._text_content return f"<{self._name} with {len(set(self._elements.values()))} children>" def __repr__(self): unique_elements = len(set(self._elements.values())) return f"ExtensibleUWSElement(name='{self._name}', elements={unique_elements})"
[docs] class JobInfo(ExtensibleUWSElement): """JobInfo element that can contain arbitrary elements.""" def __init__(self, config=None, pos=None, _name='jobInfo', **kwargs): super().__init__(config, pos, _name, **kwargs)