diff --git a/astroquery/simbad/__init__.py b/astroquery/simbad/__init__.py new file mode 100644 index 0000000000..3b7d08d887 --- /dev/null +++ b/astroquery/simbad/__init__.py @@ -0,0 +1,8 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +from .sim_queries import * +from .sim_result import * +from .sim_votable import * + +baseurl = 'http://simbad.u-strasbg.fr/simbad/sim-script?script=' +votabledef = 'main_id, coordinates' diff --git a/astroquery/simbad/sim_parameters.py b/astroquery/simbad/sim_parameters.py new file mode 100644 index 0000000000..295c78d1cb --- /dev/null +++ b/astroquery/simbad/sim_parameters.py @@ -0,0 +1,152 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +__all__ = ['_ScriptParameterWildcard', + '_ScriptParameterRadius', + '_ScriptParameterFrame', + '_ScriptParameterEquinox', + '_ScriptParameterEpoch', + '_ScriptParameterRowLimit', + 'ValidatedAttribute', + ] + + +class _ScriptParameter(object): + @property + def value(self): + return self.__value + + @value.setter + def value(self, value): + self.__value = value + + def __nonzero__(self): + return self.__value not in [None, False] + + def __str__(self): + if self: + return str(self.__value) + else: + return '' + + +def ValidatedAttribute(attr_name, attr_class): + def decorator(cls): + name = "__" + attr_name + + def getter(self): + return getattr(self, name) + + def setter(self, value): + v = attr_class(value) + setattr(self, name, v) + + setattr(cls, attr_name, property(getter, setter, None, + attr_class.__doc__)) + return cls + + return decorator + + +class _ScriptParameterWildcard(_ScriptParameter): + """ If set to True the query will be processed as an expression with + wildcards. + """ + def __init__(self, value=None): + if value is None: + self.value = None + elif value == False: + self.value = False + else: + self.value = True + + def __str__(self): + if self.value == True: + return 'wildcard' + else: + return '' + + +class _ScriptParameterRadius(_ScriptParameter): + """ Radius value for cone search. The value must be suffixed by + 'd' (degrees), 'm' (arcminutes) or 's' (arcseconds). + """ + def __init__(self, value): + if value is None: + self.value = None + return + if not isinstance(value, basestring): + raise ValueError("'radius' parameter must be a string object") + if value[-1].lower() not in ('d', 'm', 's'): + raise ValueError("'radius' parameter must be suffixed with " \ + "either 'd', 'm' or 's'") + try: + float(value[:-1]) + except: + raise ValueError("unable to interpret 'radius' parameter as a number") + self.value = str(value.lower()) + + +class _ScriptParameterFrame(_ScriptParameter): + """ Input frame for coordinate query. Allowed values are ICRS, FK5, FK4, + GAL, SGAL or ECL. + """ + _frames = ('ICRS', 'FK4', 'FK5', 'GAL', 'SGAL', 'ECL') + def __init__(self, value): + if value is None: + self.value = None + return + v = str(value).upper() + if v not in self._frames: + raise ValueError("'frame' parameter must be one of %s " \ + "('%s' was given)" % (str(self._frames), v)) + self.value = v + + +class _ScriptParameterEpoch(_ScriptParameter): + """ Epoch value for coordinate query. Example 'J2000', 'B1950'. + """ + def __init__(self, value): + if value is None: + self.value = None + return + v = str(value).upper() + if v[0] not in ['J', 'B']: + raise ValueError("'invalid value for parameter 'epoch' (%s)" % \ + value) + try: + float(v[1:]) + except: + raise ValueError("'invalid value for parameter 'epoch' (%s)" % \ + value) + self.value = v + + +class _ScriptParameterEquinox(_ScriptParameter): + """ Equinox value for coordinate query. For example '2006.5'. + """ + def __init__(self, value): + if value is None: + self.value = None + return + v = str(value) + try: + float(v) + except: + raise ValueError("invalid value for parameter 'equinox' (%s)" % \ + value) + self.value = v + + +class _ScriptParameterRowLimit(_ScriptParameter): + """ Limit of returnred rows (0 sets the limit to the maximum). + """ + def __init__(self, value): + if value is None: + self.value = None + return + v = str(value) + if not v.isdigit(): + raise ValueError("invalid value for 'row limit' parameter (%s)" % \ + value) + self.value = v + diff --git a/astroquery/simbad/sim_queries.py b/astroquery/simbad/sim_queries.py new file mode 100644 index 0000000000..3c6f30a6f3 --- /dev/null +++ b/astroquery/simbad/sim_queries.py @@ -0,0 +1,326 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +import urllib +import urllib2 + +from .sim_parameters import * +from .sim_result import * +from .sim_votable import * + +__all__ = ['QueryId', + 'QueryAroundId', + 'QueryCat', + 'QueryCoord', + 'QueryBibobj', + 'QueryMulti', + ] + + +class _Query(object): + def execute(self, votabledef=None, limit=None, pedantic=False): + """ Execute the query, returning a :class:`SimbadResult` object. + + Parameters + ---------- + votabledef: string or :class:`VoTableDef`, optional + Definition object for the output. + + limit: int, optional + Limits the number of rows returned. None sets the limit to + SIMBAD's server maximum. + + pedantic: bool, optional + The value to pass to the votable parser for the *pedantic* + parameters. + """ + + return execute_query(self, votabledef=votabledef, limit=limit, + pedantic=pedantic) + + +@ValidatedAttribute('wildcard', _ScriptParameterWildcard) +class QueryId(_Query): + """ Query by identifier. + + Parameters + ---------- + identifier: string + The identifier to query for. + + wildcard: bool, optional + If True, specifies that `identifier` should be understood as an + expression with wildcards. + + """ + + __command = 'query id ' + + def __init__(self, identifier, wildcard=None): + self.identifier = identifier + self.wildcard = wildcard + + def __str__(self): + return self.__command + (self.wildcard and 'wildcard ' or '') + \ + str(self.identifier) + '\n' + + def __repr__(self): + return '{%s(identifier=%s, wildcard=%s)}' % (self.__class__.__name__, + repr(self.identifier), repr(self.wildcard.value)) + + +@ValidatedAttribute('radius', _ScriptParameterRadius) +class QueryAroundId(_Query): + """ Query around identifier. + + Parameters + ---------- + identifier: string + The identifier around wich to query. + + radius: string, optional + The value of the cone search radius. The value must be suffixed by + 'd' (degrees), 'm' (arcminutes) or 's' (arcseconds). + If set to None the default value will be used. + + """ + + __command = 'query around ' + + def __init__(self, identifier, radius=None): + self.identifier = identifier + self.radius = radius + + def __str__(self): + s = self.__command + str(self.identifier) + if self.radius: + s += ' radius=%s' % self.radius + return s + '\n' + + def __repr__(self): + return '{%s(identifier=%s, radius=%s)}' % (self.__class__.__name__, + repr(self.identifier), repr(self.radius.value)) + + +class QueryCat(_Query): + """ Query for a whole catalog. + + Parameters + ---------- + + catalog: string + The catalog identifier, for example 'm', 'ngc'. + + """ + + __command = 'query cat ' + + def __init__(self, catalog): + self.catalog = catalog + + def __str__(self): + return self.__command + str(self.catalog) + '\n' + + def __repr__(self): + return '{%s(catalog=%s)}' % (self.__class__.__name__, + repr(self.catalog)) + + +@ValidatedAttribute('radius', _ScriptParameterRadius) +@ValidatedAttribute('frame', _ScriptParameterFrame) +@ValidatedAttribute('equinox', _ScriptParameterEquinox) +@ValidatedAttribute('epoch', _ScriptParameterEpoch) +class QueryCoord(_Query): + """ Query by coordinates. + + Parameters + ---------- + ra: string + Right ascension, for example '+12 30'. + + dec: string + Declination, for example '-20 17'. + + radius: string, optional + The value of the cone search radius. The value must be suffixed by + 'd' (degrees), 'm' (arcminutes) or 's' (arcseconds). + If set to None the default value will be used. + + frame: string, optional + Frame of input coordinates. + + equinox: string optional + Equinox of input coordinates. + + epoch: string, optional + Epoch of input coordinates. + + """ + + __command = 'query coo ' + + def __init__(self, ra, dec, radius=None, frame=None, equinox=None, + epoch=None): + self.ra = ra + self.dec = dec + self.radius = radius + self.frame = frame + self.equinox = equinox + self.epoch = epoch + + def __str__(self): + s = self.__command + str(self.ra) + ' ' + str(self.dec) + for item in ('radius', 'frame', 'equinox', 'epoch'): + if getattr(self, item): + s += ' %s=%s' % (item, str(getattr(self, item))) + return s + '\n' + + def __repr__(self): + return '{%s(ra=%s, dec=%s, radius=%s, frame=%s, equinox=%s, ' \ + 'epoch=%s)}' % \ + (self.__class__.__name__, repr(self.ra), repr(self.dec), + repr(self.radius), repr(self.frame), repr(self.equinox), + repr(self.epoch)) + + +class QueryBibobj(_Query): + """ Query by bibcode objects. Used to fetch objects contained in the + given article. + + Parameters + ---------- + bibcode: string + The bibcode of the article. + + """ + + __command = 'query bibobj ' + + def __init__(self, bibcode): + self.bibcode = bibcode + + def __str__(self): + return self.__command + str(self.bibcode) + '\n' + + def __repr__(self): + return '{%s(bibcode=%s)}' % (self.__class__.__name__, + repr(self.bibcode)) + + +@ValidatedAttribute('radius', _ScriptParameterRadius) +@ValidatedAttribute('frame', _ScriptParameterFrame) +@ValidatedAttribute('epoch', _ScriptParameterEpoch) +@ValidatedAttribute('equinox', _ScriptParameterEquinox) +class QueryMulti(_Query): + __command_ids = ('radius', 'frame', 'epoch', 'equinox') + __queries = [] + + def __init__(self, queries=None, radius=None, frame=None, epoch=None, + equinox=None): + """ A type of Query used to aggregate the values of multiple simple + queries into a single result. + + Parameters + ---------- + queries: iterable of Query objects + The list of Query objects to aggregate results for. + + radius: string, optional + The value of the cone search radius. The value must be suffixed by + 'd' (degrees), 'm' (arcminutes) or 's' (arcseconds). + If set to None the default value will be used. + + frame: string, optional + Frame of input coordinates. + + equinox: string optional + Equinox of input coordinates. + + epoch: string, optional + Epoch of input coordinates. + + .. note:: Each of the *radius*, *frame*, *equinox* et *epoch* arguments + acts as a default value for the whole MultiQuery object. + Individual queries may override these. + """ + + self.radius = radius + self.frame = frame + self.epoch = epoch + self.equinox = equinox + if queries is not None: + if isinstance(queries, _Query) and \ + not isinstance(queries, QueryMulti): + self.queries.append(queries) + elif iter(queries): + for query in queries: + self.queries.append(query) + elif isinstance(queries, QueryMulti): + for query in queries.queries: + self.queries.append(query) + + @property + def __commands(self): + """ The list of commands which are not None for this script. + """ + return tuple([x for x in self.__command_ids if getattr(self, x)]) + + @property + def _header(self): + s = '' + for comm in self.__commands: + s += 'set %s %s\n' % (comm, str(getattr(self, comm))) + return s + + @property + def queries(self): + return self.__queries + + @property + def __queries_string(self): + s = '' + for query in self.queries: + s += str(query) + return s + + def __str__(self): + return self._header + self.__queries_string + + def __repr__(self): + return repr(self.queries) + + +def execute_query(query, votabledef, limit, pedantic): + limit2 = _ScriptParameterRowLimit(limit) + + if votabledef is None: + # votabledef is None, use the module level default one + from . import votabledef as vodefault + if isinstance(vodefault, VoTableDef): + votabledef = vodefault + else: + votabledef = VoTableDef(vodefault) + elif not isinstance(votabledef, VoTableDef): + votabledef = VoTableDef(votabledef) + + # Create the 'script' string + script = '' + if limit is not None: + script += 'set limit %s\n' % str(limit2) + if isinstance(query, QueryMulti): + script += query._header + script += votabledef.def_str + script += votabledef.open_str + script += str(query) + script += votabledef.close_str + script = urllib.quote(script) + + from . import baseurl + req_str = baseurl + script + response = urllib2.urlopen(req_str) + result = b''.join(response.readlines()) + result = result.decode('utf-8') + response.close() + if not result: + raise TypeError + return SimbadResult(result, pedantic=pedantic) + diff --git a/astroquery/simbad/sim_result.py b/astroquery/simbad/sim_result.py new file mode 100644 index 0000000000..5ee986d2fd --- /dev/null +++ b/astroquery/simbad/sim_result.py @@ -0,0 +1,115 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +import re +import tempfile +import StringIO +from collections import namedtuple +import warnings + +from astropy.io.vo.table import parse +from astropy.table import Table + +__all__ = ['SimbadResult', + ] + + +error_regex = re.compile(r'(?ms)\[(?P\d+)\]\s?(?P.+?)(\[|\Z)') + +SimbadError = namedtuple('SimbadError', ('line', 'msg')) +VersionInfo = namedtuple('VersionInfo', ('major', 'minor', 'micro', 'patch')) + + +class SimbadResult(object): + __sections = ('script', 'console', 'error', 'data') + + def __init__(self, txt, pedantic=False): + self.__txt = txt + self.__pedantic = pedantic + self.__table = None + self.__stringio = None + self.__indexes = {} + self.exectime = None + self.sim_version = None + self.__split_sections() + self.__parse_console_section() + self.__warn() + self.__file = None + + def __split_sections(self): + for section in self.__sections: + match = re.search(r'(?ims)^::%s:+?$(?P.*?)(^::|\Z)' % \ + section, self.__txt) + if match: + self.__indexes[section] = (match.start('content'), + match.end('content')) + + def __parse_console_section(self): + if self.console is None: + return + m = re.search(r'(?ims)total execution time: ([.\d]+?)\s*?secs', + self.console) + if m: + try: + self.exectime = float(m.group(1)) + except: + # TODO: do something useful here. + pass + m = re.search(r'(?ms)SIMBAD(\d) rel (\d)[.](\d+)([^\d^\s])?', + self.console) + if m: + self.sim_version = VersionInfo(*m.groups(None)) + + def __warn(self): + for error in self.errors: + warnings.warn("Warning: The script line number %i raised " + "the error: %s." %\ + (error.line, error.msg)) + + def __get_section(self, section_name): + if section_name in self.__indexes: + return self.__txt[self.__indexes[section_name][0]:\ + self.__indexes[section_name][1]].strip() + + @property + def script(self): + return self.__get_section('script') + + @property + def console(self): + return self.__get_section('console') + + @property + def error_raw(self): + return self.__get_section('error') + + @property + def data(self): + return self.__get_section('data') + + @property + def errors(self): + result = [] + if self.error_raw is None: + return result + for err in error_regex.finditer(self.error_raw): + result.append(SimbadError(int(err.group('line')), + err.group('msg').replace('\n', ' '))) + return result + + @property + def nb_errors(self): + if self.error_raw is None: + return 0 + return len(self.errors) + + @property + def table(self): + if self.__file is None: + self.__file = tempfile.NamedTemporaryFile() + self.__file.write(self.data.encode('utf-8')) + self.__file.flush() + array = parse(self.__file, + pedantic=self.__pedantic).get_first_table().array + self.__table = Table(array) + return self.__table + diff --git a/astroquery/simbad/sim_votable.py b/astroquery/simbad/sim_votable.py new file mode 100644 index 0000000000..4b391cfd74 --- /dev/null +++ b/astroquery/simbad/sim_votable.py @@ -0,0 +1,39 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +__all__ = ['VoTableDef', + ] + +class VoTableDef(object): + def __init__(self, *args, **kwargs): + self.__fields = [] + for value in args: + names = str(value).split(',') + for name in [v.strip() for v in names if v.strip()]: + self.__fields.append(name) + if 'name' in kwargs: + self.name = kwargs['name'] + del kwargs['name'] + else: + self.name = '' + if kwargs: + raise ValueError("'name' is the only keyword argument allowed") + + @property + def fields(self): + return list(self.__fields) + + @property + def __fields_str(self): + return ', '.join(self.fields) + + @property + def def_str(self): + return 'votable %s {%s}\n' % (self.name, self.__fields_str) + + @property + def open_str(self): + return 'votable open %s\n' % self.name + + @property + def close_str(self): + return 'votable close %s\n' % self.name diff --git a/docs/astroquery/simbad.rst b/docs/astroquery/simbad.rst new file mode 100644 index 0000000000..a2b41e0658 --- /dev/null +++ b/docs/astroquery/simbad.rst @@ -0,0 +1,28 @@ +.. _astroquery.simbad: + +***************************************** +SIMBAD Queries (`astroquery.simbad`) +***************************************** + +Getting started +=============== + +The following example illustrates a SIMBAD query:: + + >>> from astroquery import simbad + >>> r = simbad.QueryAroundId('m31', radius='0.5s') + >>> print r.table + + MAIN_ID RA DEC RA_PREC DEC_PREC COO_ERR_MAJA COO_ERR_MINA COO_ERR_ANGLE COO_QUAL COO_WAVELENGTH COO_BIBCODE + ----------------------------- ------------ ------------ ------- -------- ------------ ------------ ------------- -------- -------------- ------------------- + M 31 00 42 44.330 +41 16 07.50 7 7 nan nan 0 B I 2006AJ....131.1163S + [BFS98] J004244.344+411607.70 00 42 44.344 +41 16 07.70 7 7 nan nan 0 D 1998ApJ...504..113B + [K2002] J004244.37+411607.6 00 42 44.365 +41 16 07.65 7 7 30.0 30.0 10 D 2002ApJ...577..738K + [BFS98] J004244.362+411607.20 00 42 44.362 +41 16 07.20 7 7 nan nan 0 D 1998ApJ...504..113B + [BFS98] J004244.303+411607.14 00 42 44.303 +41 16 07.14 7 7 nan nan 0 D 1998ApJ...504..113B + +Reference/API +============= + +.. automodapi:: astroquery.simbad + :no-inheritance-diagram: