# -*- coding: utf-8 -*-
"""
SeedLink request client for ObsPy.
:copyright:
The ObsPy Development Team (devs@obspy.org)
:license:
GNU Lesser General Public License, Version 3
(https://www.gnu.org/copyleft/lesser.html)
"""
import fnmatch
import warnings
from lxml import etree
from obspy import Stream
from .slclient import SLClient, SLPacket
from .client.seedlinkconnection import SeedLinkConnection
[docs]class Client(object):
"""
SeedLink request client.
This client is intended for requests of specific, finite time windows.
To work with continuous realtime data streams please see
:class:`~obspy.clients.seedlink.slclient.SLClient` and
:class:`~obspy.clients.seedlink.easyseedlink.EasySeedLinkClient`.
:type server: str
:param server: Server name or IP address to connect to (e.g.
"localhost", "rtserver.ipgp.fr")
:type port: int
:param port: Port at which the seedlink server is operating (default is
`18000`).
:type timeout: float
:param timeout: Network timeout for low-level network connection in
seconds.
:type debug: bool
:param debug: Switches on debugging output.
"""
[docs] def __init__(self, server, port=18000, timeout=20, debug=False):
"""
Initializes the SeedLink request client.
"""
self.timeout = timeout
self.debug = debug
self._server_url = "%s:%i" % (server, port)
self._station_cache = None
self._station_cache_level = None
[docs] def _init_client(self):
"""
Make fresh connection to seedlink server
Should be done before any request to server, since SLClient keeps
things like multiselect etc for subsequent requests
"""
self._slclient = SLClient(timeout=self.timeout)
[docs] def _connect(self):
"""
Open new connection to seedlink server.
"""
self._slclient.slconn = SeedLinkConnection(timeout=self.timeout)
self._slclient.slconn.set_sl_address(self._server_url)
self._slclient.slconn.netto = self.timeout
[docs] def _multiselect_request(self, multiselect, starttime, endtime):
"""
Make a multiselect request to underlying seedlink client
Multiselect string is one or more comma separated
network/station/location/channel combinations as defined by seedlink
standard, e.g.
"NETWORK_STATION:LOCATIONCHANNEL,NETWORK_STATION:LOCATIONCHANNEL"
where location+channel may contain '?' characters but should be exactly
5 characters long.
:rtype: :class:`~obspy.core.stream.Stream`
"""
self._init_client()
self._slclient.multiselect = multiselect
self._slclient.begin_time = starttime
self._slclient.end_time = endtime
self._connect()
self._slclient.initialize()
self.stream = Stream()
self._slclient.run(packet_handler=self._packet_handler)
stream = self.stream
stream.trim(starttime, endtime)
self.stream = None
stream.sort()
return stream
[docs] def get_info(self, network=None, station=None, location=None, channel=None,
level='station', cache=True):
"""
Request available stations information from the seedlink server.
Supports ``fnmatch`` wildcards, e.g. ``*`` and ``?``, in ``network``,
``station``, ``location`` and ``channel``.
>>> client = Client('rtserver.ipgp.fr')
>>> info = client.get_info(station="FDFM")
>>> print(info)
[('G', 'FDFM')]
>>> info = client.get_info(station="FD?M", channel='*Z',
... level='channel')
>>> print(info) # doctest: +NORMALIZE_WHITESPACE
[('G', 'FDFM', '00', 'BHZ'), ('G', 'FDFM', '00', 'HHZ'),
('G', 'FDFM', '00', 'HNZ'), ('G', 'FDFM', '00', 'LHZ'),
('G', 'FDFM', '10', 'BHZ'), ('G', 'FDFM', '10', 'HHZ'),
('G', 'FDFM', '10', 'LHZ')]
Available station information is cached after the first request to the
server, so use ``cache=False`` on subsequent requests if there is a
need to force fetching new information from the server (should only
concern programs running in background for a very long time).
:type network: str
:param network: Network code. Supports ``fnmatch`` wildcards, e.g.
``*`` and ``?``.
:type station: str
:param station: Station code. Supports ``fnmatch`` wildcards, e.g.
``*`` and ``?``.
:type location: str
:param location: Location code. Supports ``fnmatch`` wildcards, e.g.
``*`` and ``?``.
:type channel: str
:param channel: Channel code. Supports ``fnmatch`` wildcards, e.g.
``*`` and ``?``.
:type cache: bool
:param cache: Subsequent function calls are cached, use ``cache=False``
to force fetching station metadata again from the server.
:rtype: list
:returns: list of 2-tuples (or 4-tuples with ``level='channel'``) with
network/station (network/station/location/channel, respectively)
code combinations for which data is served by the server.
"""
if level not in ('station', 'channel'):
msg = "Invalid option for 'level': '%s'" % str(level)
raise ValueError(msg)
if level == 'station' and \
any(x is not None for x in (location, channel)):
msg = ("location and channel options are ignored in get_info() if "
"level='station'.")
warnings.warn(msg)
# deteremine if we have a usable cache and check if it is at least the
# requested level of detail
if cache and self._station_cache is not None \
and level in ('station', self._station_cache_level):
if level == 'station':
if self._station_cache_level == 'station':
info = [(net, sta) for net, sta in self._station_cache
if fnmatch.fnmatch(net, network or '*') and
fnmatch.fnmatch(sta, station or '*')]
return sorted(info)
else:
info = [(net, sta) for net, sta, loc, cha
in self._station_cache
if fnmatch.fnmatch(net, network or '*') and
fnmatch.fnmatch(sta, station or '*')]
return sorted(set(info))
info = [(net, sta, loc, cha) for net, sta, loc, cha in
self._station_cache if
fnmatch.fnmatch(net, network or '*') and
fnmatch.fnmatch(sta, station or '*') and
fnmatch.fnmatch(loc, location or '*') and
fnmatch.fnmatch(cha, channel or '*')]
return sorted(info)
self._init_client()
if level == 'station':
self._slclient.infolevel = "STATIONS"
elif level == 'channel':
self._slclient.infolevel = "STREAMS"
self._slclient.verbose = 1
self._connect()
self._slclient.initialize()
# self._slclient.run()
self._slclient.run(packet_handler=self._packet_handler)
info = self._slclient.slconn.info_string
try:
xml = etree.fromstring(info)
except ValueError as e:
msg = 'Unicode strings with encoding declaration are not supported'
if msg not in str(e):
raise
parser = etree.XMLParser(encoding='utf-8')
xml = etree.fromstring(info.encode('utf-8'), parser=parser)
station_cache = set()
for tag in xml.xpath('./station'):
net = tag.attrib['network']
sta = tag.attrib['name']
item = (net, sta)
if level == 'channel':
subtags = tag.xpath('./stream')
for subtag in subtags:
loc = subtag.attrib['location']
cha = subtag.attrib['seedname']
station_cache.add(item + (loc, cha))
# If no data is in ring buffer (e.g. station outage?) then it
# seems the seedlink server replies with no subtags for the
# channels
if not subtags:
station_cache.add(item + (None, None))
else:
station_cache.add(item)
# change results to an Inventory object
self._station_cache = station_cache
self._station_cache_level = level
return self.get_info(
network=network, station=station, location=location,
channel=channel, cache=True, level=level)
[docs] def _packet_handler(self, count, slpack):
"""
Custom packet handler that accumulates all waveform packets in a
stream.
"""
# check if not a complete packet
if slpack is None or (slpack == SLPacket.SLNOPACKET) or \
(slpack == SLPacket.SLERROR):
return False
# get basic packet info
type_ = slpack.get_type()
if self.debug:
print(type_)
# process INFO packets here
if type_ == SLPacket.TYPE_SLINF:
if self.debug:
print(SLPacket.TYPE_SLINF)
return False
elif type_ == SLPacket.TYPE_SLINFT:
if self.debug:
print("Complete INFO:",
self._slclient.slconn.get_info_string())
return True
# process packet data
trace = slpack.get_trace()
if trace is None:
if self.debug:
print("Blockette contains no trace")
return False
# new samples add to the main stream which is then trimmed
self.stream += trace
self.stream.merge(-1)
return False
if __name__ == '__main__':
import doctest
doctest.testmod(exclude_empty=True)