Coverage for /usr/local/lib/python3.13/dist-packages/pyrocko/client/fdsn.py: 61%
288 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +0000
1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6'''
7Low-level FDSN web service client.
9This module provides basic functionality to download station metadata, time
10series data and event information from FDSN web services. Password and token
11authentication are supported. Query responses are returned as open file-like
12objects or can be parsed into Pyrocko's native data structures where
13appropriate.
15.. _registered-site-names:
17Registered site names
18.....................
20A list of known FDSN site names is maintained within the module for quick
21selection by the user. This list currently contains the following sites:
23%s
25Any other site can be specified by providing its full URL.
26'''
28import re
29import logging
30import socket
31import io
33import requests
35from pyrocko import util
36from pyrocko.util import DownloadError
37from pyrocko import config
39from pyrocko.util import \
40 urlencode, Request, build_opener, HTTPDigestAuthHandler, urlopen, HTTPError
43logger = logging.getLogger('pyrocko.client.fdsn')
45g_url = '%(site)s/fdsnws/%(service)s/%(majorversion)i/%(method)s'
47g_site_abbr = {
48 'auspass': 'http://auspass.edu.au:8080',
49 'bgr': 'http://eida.bgr.de',
50 'emsc': 'http://www.seismicportal.eu',
51 'ethz': 'http://eida.ethz.ch',
52 'geofon': 'https://geofon.gfz.de',
53 'geonet': 'http://service.geonet.org.nz',
54 'icgc': 'http://ws.icgc.cat',
55 'iesdmc': 'http://batsws.earth.sinica.edu.tw:8080',
56 'ingv': 'http://webservices.ingv.it',
57 'ipgp': 'http://eida.ipgp.fr',
58 'iris': 'http://service.iris.edu',
59 'isc': 'http://www.isc.ac.uk',
60 'kagsr': 'http://sdis.emsd.ru',
61 'knmi': 'http://rdsa.knmi.nl',
62 'koeri': 'http://eida-service.koeri.boun.edu.tr',
63 'lmu': 'http://erde.geophysik.uni-muenchen.de',
64 'ncedc': 'https://service.ncedc.org',
65 'niep': 'http://eida-sc3.infp.ro',
66 'noa': 'http://eida.gein.noa.gr',
67 'norsar': 'http://eida.geo.uib.no',
68 'nrcan': 'https://earthquakescanada.nrcan.gc.ca',
69 'orfeus': 'http://www.orfeus-eu.org',
70 'raspishake': 'https://data.raspberryshake.org',
71 'resif': 'http://ws.resif.fr',
72 'scedc': 'http://service.scedc.caltech.edu',
73 'usgs': 'http://earthquake.usgs.gov',
74 'usp': 'http://seisrequest.iag.usp.br',
75}
77g_default_site = 'geofon'
80g_default_query_args = {
81 'station': {
82 'starttime', 'endtime', 'startbefore', 'startafter', 'endbefore',
83 'endafter', 'network', 'station', 'location', 'channel', 'minlatitude',
84 'maxlatitude', 'minlongitude', 'maxlongitude', 'latitude', 'longitude',
85 'minradius', 'maxradius', 'level', 'includerestricted',
86 'includeavailability', 'updatedafter', 'matchtimeseries', 'format',
87 'nodata'},
88 'dataselect': {
89 'starttime', 'endtime', 'network', 'station', 'location', 'channel',
90 'quality', 'minimumlength', 'longestonly', 'format', 'nodata'},
91 'event': {
92 'starttime', 'endtime', 'minlatitude', 'maxlatitude', 'minlongitude',
93 'maxlongitude', 'latitude', 'longitude', 'minradius', 'maxradius',
94 'mindepth', 'maxdepth', 'minmagnitude', 'maxmagnitude', 'eventtype',
95 'includeallorigins', 'includeallmagnitudes', 'includearrivals',
96 'eventid', 'limit', 'offset', 'orderby', 'catalog', 'contributor',
97 'updatedafter', 'format', 'nodata'},
98 'availability': {
99 'starttime', 'endtime', 'network', 'station', 'location', 'channel',
100 'quality', 'merge', 'orderby', 'limit', 'includerestricted', 'format',
101 'nodata', 'mergegaps', 'show'}}
104def doc_escape_slist(li):
105 return ', '.join("``'%s'``" % s for s in li)
108def doc_table_dict(d, khead, vhead, indent=''):
109 keys, vals = zip(*sorted(d.items()))
111 lk = max(max(len(k) for k in keys), len(khead))
112 lv = max(max(len(v) for v in vals), len(vhead))
114 hr = '=' * lk + ' ' + '=' * lv
116 lines = [
117 hr,
118 '%s %s' % (khead.ljust(lk), vhead.ljust(lv)),
119 hr]
121 for k, v in zip(keys, vals):
122 lines.append('%s %s' % (k.ljust(lk), v.ljust(lv)))
124 lines.append(hr)
125 return '\n'.join(indent + line for line in lines)
128def strip_html(s):
129 if isinstance(s, bytes):
130 s = s.decode('utf-8')
131 s = re.sub(r'<[^>]+>', '', s)
132 s = re.sub(r'\r', '', s)
133 s = re.sub(r'\s*\n', '\n', s)
134 return s
137def indent(s, ind=' '):
138 return '\n'.join(ind + line for line in s.splitlines())
141def get_sites():
142 '''
143 Get sorted list of registered site names.
144 '''
145 return sorted(g_site_abbr.keys())
148if config.config().fdsn_timeout is None:
149 g_timeout = 20.
150else:
151 g_timeout = config.config().fdsn_timeout
153re_realm_from_auth_header = re.compile(r'(realm)\s*[:=]\s*"([^"]*)"?')
156class CannotGetRealmFromAuthHeader(DownloadError):
157 '''
158 Raised when failing to parse server response during authentication.
159 '''
160 pass
163class CannotGetCredentialsFromAuthRequest(DownloadError):
164 '''
165 Raised when failing to parse server response during token authentication.
166 '''
167 pass
170def get_realm_from_auth_header(headers):
171 realm = dict(re_realm_from_auth_header.findall(
172 headers['WWW-Authenticate'])).get('realm', None)
174 if realm is None:
175 raise CannotGetRealmFromAuthHeader('headers=%s' % str(headers))
177 return realm
180def sdatetime(t):
181 return util.time_to_str(t, format='%Y-%m-%dT%H:%M:%S')
184class EmptyResult(DownloadError):
185 '''
186 Raised when an empty server response is retrieved.
187 '''
188 def __init__(self, url):
189 DownloadError.__init__(self)
190 self._url = url
192 def __str__(self):
193 return 'No results for request %s' % self._url
196class RequestEntityTooLarge(DownloadError):
197 '''
198 Raised when the server indicates that too much data was requested.
199 '''
200 def __init__(self, url):
201 DownloadError.__init__(self)
202 self._url = url
204 def __str__(self):
205 return 'Request entity too large: %s' % self._url
208class InvalidRequest(DownloadError):
209 '''
210 Raised when an invalid request would be sent / has been sent.
211 '''
212 pass
215class Timeout(DownloadError):
216 '''
217 Raised when the server does not respond within the allowed timeout period.
218 '''
219 pass
222g_session = None
225def _request(
226 url,
227 post=False,
228 user=None,
229 passwd=None,
230 timeout=None,
231 **kwargs):
233 global g_session
235 if g_session is None:
236 g_session = requests.Session()
238 if user is not None and passwd is not None:
239 auth = requests.auth.HTTPDigestAuth(user, passwd)
240 else:
241 auth = None
243 if timeout is None:
244 timeout = g_timeout
246 logger.debug('Accessing URL %s' % url)
248 try:
249 if not post:
250 response = g_session.get(
251 url,
252 auth=auth,
253 timeout=timeout,
254 params=kwargs)
256 else:
257 if isinstance(post, str):
258 post = post.encode('utf8')
260 logger.debug('POST data: \n%s' % post.decode('utf8'))
262 response = g_session.post(
263 url,
264 auth=auth,
265 timeout=timeout,
266 params=kwargs,
267 data=post)
269 logger.debug('Response: %s' % response.status_code)
271 if response.status_code == 204:
272 raise EmptyResult(url)
274 elif response.status_code == 413:
275 raise RequestEntityTooLarge(url)
277 response.raise_for_status()
279 except requests.exceptions.ConnectionError as e:
280 raise DownloadError(
281 'Failed connection attempt: %s' % str(e))
283 except requests.exceptions.HTTPError as e:
284 raise DownloadError(
285 'Error content returned by server (HTML stripped):\n%s\n'
286 ' Original error was: %s' % (
287 indent(
288 strip_html(response.text),
289 ' ! '),
290 str(e)))
292 except requests.exceptions.Timeout:
293 raise Timeout(
294 'Timeout error. No response received within %i s. You '
295 'may want to retry with a longer timeout setting. The global '
296 'timeout can be set with the variable `fdsn_timeout` in '
297 '`~/.pyrocko/config.pf`, but this value may be overriden by '
298 'the script/application for a specific request.' % timeout)
300 return io.BytesIO(response.content)
303def _request_old(
304 url,
305 post=False,
306 user=None,
307 passwd=None,
308 timeout=None,
309 **kwargs):
311 if timeout is None:
312 timeout = g_timeout
314 url_values = urlencode(kwargs)
315 if url_values:
316 url += '?' + url_values
318 logger.debug('Accessing URL %s' % url)
319 url_args = {
320 'timeout': timeout
321 }
323 if util.g_ssl_context:
324 url_args['context'] = util.g_ssl_context
326 opener = None
328 req = Request(url)
329 if post:
330 if isinstance(post, str):
331 post = post.encode('utf8')
332 logger.debug('POST data: \n%s' % post.decode('utf8'))
333 req.data = post
335 req.add_header('Accept', '*/*')
337 itry = 0
338 while True:
339 itry += 1
340 try:
341 urlopen_ = opener.open if opener else urlopen
342 try:
343 resp = urlopen_(req, **url_args)
344 except TypeError:
345 # context and cafile not avail before 3.4.3, 2.7.9
346 url_args.pop('context', None)
347 url_args.pop('cafile', None)
348 resp = urlopen_(req, **url_args)
350 logger.debug('Response: %s' % resp.getcode())
351 if resp.getcode() == 204:
352 raise EmptyResult(url)
353 return resp
355 except HTTPError as e:
356 if e.code == 413:
357 raise RequestEntityTooLarge(url)
359 elif e.code == 401:
360 headers = getattr(e, 'headers', e.hdrs)
362 realm = get_realm_from_auth_header(headers)
364 if itry == 1 and user is not None:
365 auth_handler = HTTPDigestAuthHandler()
366 auth_handler.add_password(
367 realm=realm,
368 uri=url,
369 user=user,
370 passwd=passwd or '')
372 opener = build_opener(auth_handler)
373 continue
374 else:
375 raise DownloadError(
376 'Authentication failed for realm "%s" when accessing '
377 'url "%s". Original error was: %s' % (
378 realm, url, str(e)))
380 else:
381 raise DownloadError(
382 'Error content returned by server (HTML stripped):\n%s\n'
383 ' Original error was: %s' % (
384 indent(
385 strip_html(e.read()),
386 ' ! '),
387 str(e)))
389 except socket.timeout:
390 raise Timeout(
391 'Timeout error. No response received within %i s. You '
392 'may want to retry with a longer timeout setting. The global '
393 'timeout can be set with the variable `fdsn_timeout` in '
394 '`~/.pyrocko/config.pf`, but this value may be overriden by '
395 'the script/application for a specific request.' % timeout)
397 break
400def fillurl(service, site, url, majorversion, method):
401 return url % dict(
402 site=g_site_abbr.get(site, site),
403 service=service,
404 majorversion=majorversion,
405 method=method)
408def fix_params(d):
410 params = dict(d)
411 for k in ['starttime',
412 'endtime',
413 'startbefore',
414 'startafter',
415 'endbefore',
416 'endafter',
417 'updatedafter']:
419 if k in params:
420 params[k] = sdatetime(params[k])
422 if params.get('location', None) == '':
423 params['location'] = '--'
425 for k in params:
426 if isinstance(params[k], bool):
427 params[k] = ['false', 'true'][bool(params[k])]
429 return params
432def make_data_selection(
433 stations, tmin, tmax,
434 channel_prio=[['BHZ', 'HHZ'],
435 ['BH1', 'BHN', 'HH1', 'HHN'],
436 ['BH2', 'BHE', 'HH2', 'HHE']]):
438 selection = []
439 for station in stations:
440 wanted = []
441 for group in channel_prio:
442 gchannels = []
443 for channel in station.get_channels():
444 if channel.name in group:
445 gchannels.append(channel)
446 if gchannels:
447 gchannels.sort(key=lambda a: group.index(a.name))
448 wanted.append(gchannels[0])
450 if wanted:
451 for channel in wanted:
452 selection.append((station.network, station.station,
453 station.location, channel.name, tmin, tmax))
455 return selection
458def station(
459 site=g_default_site,
460 url=g_url,
461 majorversion=1,
462 timeout=None,
463 check=True,
464 selection=None,
465 parsed=True,
466 **kwargs):
468 '''
469 Query FDSN web service for station metadata.
471 :param site:
472 :ref:`Registered site name <registered-site-names>` or full base URL of
473 the service (e.g. ``'https://geofon.gfz-potsdam.de'``).
474 :type site: str
475 :param url:
476 URL template (default should work in 99% of cases).
477 :type url: str
478 :param majorversion:
479 Major version of the service to query (always ``1`` at the time of
480 writing).
481 :type majorversion: int
482 :param timeout:
483 Network timeout in [s]. Global default timeout can be configured in
484 Pyrocko's configuration file under ``fdsn_timeout``.
485 :type timeout: float
486 :param check:
487 If ``True`` arguments are checked against self-description (WADL) of
488 the queried web service if available or FDSN specification.
489 :type check: bool
490 :param selection:
491 If given, selection to be queried as a list of tuples
492 ``(network, station, location, channel, tmin, tmax)``. Useful for
493 detailed queries.
494 :type selection: :py:class:`list` of :py:class:`tuple`
495 :param parsed:
496 If ``True`` parse received content into
497 :py:class:`~pyrocko.io.stationxml.FDSNStationXML`
498 object, otherwise return open file handle to raw data stream.
499 :type parsed: bool
500 :param \\*\\*kwargs:
501 Parameters passed to the server (see `FDSN web services specification
502 <https://www.fdsn.org/webservices>`_).
504 :returns:
505 See description of ``parsed`` argument above.
507 :raises:
508 On failure, :py:exc:`~pyrocko.util.DownloadError` or one of its
509 sub-types defined in the :py:mod:`~pyrocko.client.fdsn` module is
510 raised.
511 '''
513 service = 'station'
515 if check:
516 check_params(service, site, url, majorversion, timeout, **kwargs)
518 params = fix_params(kwargs)
520 url = fillurl(service, site, url, majorversion, 'query')
521 if selection:
522 lst = []
523 for k, v in params.items():
524 lst.append('%s=%s' % (k, v))
526 for (network, station, location, channel, tmin, tmax) in selection:
527 if location == '':
528 location = '--'
530 lst.append(' '.join((network, station, location, channel,
531 sdatetime(tmin), sdatetime(tmax))))
533 post = '\n'.join(lst)
534 params = dict(post=post.encode())
536 if parsed:
537 from pyrocko.io import stationxml
538 format = kwargs.get('format', 'xml')
539 if format == 'text':
540 if kwargs.get('level', 'station') == 'channel':
541 return stationxml.load_channel_table(
542 stream=_request(url, timeout=timeout, **params))
543 else:
544 raise InvalidRequest('if format="text" shall be parsed, '
545 'level="channel" is required')
547 elif format == 'xml':
548 return stationxml.load_xml(
549 stream=_request(url, timeout=timeout, **params))
550 else:
551 raise InvalidRequest('format must be "xml" or "text"')
552 else:
553 return _request(url, timeout=timeout, **params)
556def get_auth_credentials(service, site, url, majorversion, token, timeout):
558 url = fillurl(service, site, url, majorversion, 'auth')
560 f = _request(url, timeout=timeout, post=token)
561 s = f.read().decode()
562 try:
563 user, passwd = s.strip().split(':')
564 except ValueError:
565 raise CannotGetCredentialsFromAuthRequest('data="%s"' % s)
567 return user, passwd
570def dataselect(
571 site=g_default_site,
572 url=g_url,
573 majorversion=1,
574 timeout=None,
575 check=True,
576 user=None,
577 passwd=None,
578 token=None,
579 selection=None,
580 **kwargs):
582 '''
583 Query FDSN web service for time series data in miniSEED format.
585 :param site:
586 :ref:`Registered site name <registered-site-names>` or full base URL of
587 the service (e.g. ``'https://geofon.gfz-potsdam.de'``).
588 :type site: str
589 :param url:
590 URL template (default should work in 99% of cases).
591 :type url: str
592 :param majorversion:
593 Major version of the service to query (always ``1`` at the time of
594 writing).
595 :type majorversion: int
596 :param timeout:
597 Network timeout in [s]. Global default timeout can be configured in
598 Pyrocko's configuration file under ``fdsn_timeout``.
599 :type timeout: float
600 :param check:
601 If ``True`` arguments are checked against self-description (WADL) of
602 the queried web service if available or FDSN specification.
603 :type check: bool
604 :param user: User name for user/password authentication.
605 :type user: str
606 :param passwd: Password for user/password authentication.
607 :type passwd: str
608 :param token: Token for `token authentication
609 <https://geofon.gfz-potsdam.de/waveform/archive/auth/auth-overview.php>`_.
610 :type token: str
611 :param selection:
612 If given, selection to be queried as a list of tuples
613 ``(network, station, location, channel, tmin, tmax)``.
614 :type selection: :py:class:`list` of :py:class:`tuple`
615 :param \\*\\*kwargs:
616 Parameters passed to the server (see `FDSN web services specification
617 <https://www.fdsn.org/webservices>`_).
619 :returns:
620 Open file-like object providing raw miniSEED data.
621 '''
623 service = 'dataselect'
625 if user or token:
626 method = 'queryauth'
627 else:
628 method = 'query'
630 if token is not None:
631 user, passwd = get_auth_credentials(
632 service, site, url, majorversion, token, timeout)
634 if check:
635 check_params(service, site, url, majorversion, timeout, **kwargs)
637 params = fix_params(kwargs)
639 url = fillurl(service, site, url, majorversion, method)
640 if selection:
641 lst = []
643 for k, v in params.items():
644 lst.append('%s=%s' % (k, v))
646 for (network, station, location, channel, tmin, tmax) in selection:
647 if location == '':
648 location = '--'
650 lst.append(' '.join((network, station, location, channel,
651 sdatetime(tmin), sdatetime(tmax))))
653 post = '\n'.join(lst)
654 return _request(
655 url, user=user, passwd=passwd, post=post.encode(), timeout=timeout)
656 else:
657 return _request(
658 url, user=user, passwd=passwd, timeout=timeout, **params)
661def event(
662 site=g_default_site,
663 url=g_url,
664 majorversion=1,
665 timeout=None,
666 check=True,
667 user=None,
668 passwd=None,
669 token=None,
670 parsed=False,
671 **kwargs):
673 '''
674 Query FDSN web service for events.
676 :param site:
677 :ref:`Registered site name <registered-site-names>` or full base URL of
678 the service (e.g. ``'https://geofon.gfz-potsdam.de'``).
679 :type site: str
680 :param url:
681 URL template (default should work in 99% of cases).
682 :type url: str
683 :param majorversion:
684 Major version of the service to query (always ``1`` at the time of
685 writing).
686 :type majorversion: int
687 :param timeout:
688 Network timeout in [s]. Global default timeout can be configured in
689 Pyrocko's configuration file under ``fdsn_timeout``.
690 :type timeout: float
691 :param check:
692 If ``True`` arguments are checked against self-description (WADL) of
693 the queried web service if available or FDSN specification.
694 :type check: bool
695 :param user: User name for user/password authentication.
696 :type user: str
697 :param passwd: Password for user/password authentication.
698 :type passwd: str
699 :param token: Token for `token authentication
700 <https://geofon.gfz-potsdam.de/waveform/archive/auth/auth-overview.php>`_.
701 :type token: str
702 :param parsed:
703 If ``True`` parse received content into
704 :py:class:`~pyrocko.io.quakeml.QuakeML`
705 object, otherwise return open file handle to raw data stream. Note:
706 by default unparsed data is retrieved, differently from the default
707 behaviour of :py:func:`station` (for backward compatibility).
708 :type parsed: bool
709 :param \\*\\*kwargs:
710 Parameters passed to the server (see `FDSN web services specification
711 <https://www.fdsn.org/webservices>`_).
713 :returns:
714 See description of ``parsed`` argument above.
715 '''
717 service = 'event'
719 if user or token:
720 method = 'queryauth'
721 else:
722 method = 'query'
724 if token is not None:
725 user, passwd = get_auth_credentials(
726 service, site, url, majorversion, token, timeout)
728 if check:
729 check_params(service, site, url, majorversion, timeout, **kwargs)
731 params = fix_params(kwargs)
733 url = fillurl(service, site, url, majorversion, method)
735 fh = _request(url, user=user, passwd=passwd, timeout=timeout, **params)
736 if parsed:
737 from pyrocko.io import quakeml
738 format = kwargs.get('format', 'xml')
739 if format != 'xml':
740 raise InvalidRequest(
741 'If parsed=True is selected, format="xml" must be selected.')
743 return quakeml.QuakeML.load_xml(stream=fh)
745 else:
746 return fh
749def availability(
750 method='query',
751 site=g_default_site,
752 url=g_url,
753 majorversion=1,
754 timeout=None,
755 check=True,
756 user=None,
757 passwd=None,
758 token=None,
759 selection=None,
760 **kwargs):
762 '''
763 Query FDSN web service for time series data availablity.
765 :param method: Availablility method to call: ``'query'``, or ``'extent'``.
766 :param site:
767 :ref:`Registered site name <registered-site-names>` or full base URL of
768 the service (e.g. ``'https://geofon.gfz-potsdam.de'``).
769 :type site: str
770 :param url:
771 URL template (default should work in 99% of cases).
772 :type url: str
773 :param majorversion:
774 Major version of the service to query (always ``1`` at the time of
775 writing).
776 :type majorversion: int
777 :param timeout:
778 Network timeout in [s]. Global default timeout can be configured in
779 Pyrocko's configuration file under ``fdsn_timeout``.
780 :type timeout: float
781 :param check:
782 If ``True`` arguments are checked against self-description (WADL) of
783 the queried web service if available or FDSN specification.
784 :type check: bool
785 :param user: User name for user/password authentication.
786 :type user: str
787 :param passwd: Password for user/password authentication.
788 :type passwd: str
789 :param token: Token for `token authentication
790 <https://geofon.gfz-potsdam.de/waveform/archive/auth/auth-overview.php>`_.
791 :type token: str
792 :param selection:
793 If given, selection to be queried as a list of tuples
794 ``(network, station, location, channel, tmin, tmax)``.
795 :type selection: :py:class:`list` of :py:class:`tuple`
796 :param \\*\\*kwargs:
797 Parameters passed to the server (see `FDSN web services specification
798 <https://www.fdsn.org/webservices>`_).
800 :returns:
801 Open file-like object providing raw response.
802 '''
804 service = 'availability'
806 assert method in ('query', 'extent')
808 if user or token:
809 method += 'auth'
811 if token is not None:
812 user, passwd = get_auth_credentials(
813 service, site, url, majorversion, token, timeout)
815 if check:
816 check_params(service, site, url, majorversion, timeout, **kwargs)
818 params = fix_params(kwargs)
820 url = fillurl(service, site, url, majorversion, method)
821 if selection:
822 lst = []
824 for k, v in params.items():
825 lst.append('%s=%s' % (k, v))
827 for (network, station, location, channel, tmin, tmax) in selection:
828 if location == '':
829 location = '--'
831 lst.append(' '.join((network, station, location, channel,
832 sdatetime(tmin), sdatetime(tmax))))
834 post = '\n'.join(lst)
835 return _request(
836 url, user=user, passwd=passwd, post=post.encode(), timeout=timeout)
837 else:
838 return _request(
839 url, user=user, passwd=passwd, timeout=timeout, **params)
842def check_params(
843 service,
844 site=g_default_site,
845 url=g_url,
846 majorversion=1,
847 timeout=None,
848 method='query',
849 **kwargs):
851 '''
852 Check query parameters against self-description of web service.
854 Downloads WADL description of the given service and site and checks
855 parameters if they are available. Queried WADLs are cached in memory.
857 :param service: ``'station'``, ``'dataselect'``, ``'event'`` or
858 ``'availability'``.
859 :param site:
860 :ref:`Registered site name <registered-site-names>` or full base URL of
861 the service (e.g. ``'https://geofon.gfz-potsdam.de'``).
862 :type site: str
863 :param url:
864 URL template (default should work in 99% of cases).
865 :type url: str
866 :param majorversion:
867 Major version of the service to query (always ``1`` at the time of
868 writing).
869 :type majorversion: int
870 :param timeout:
871 Network timeout in [s]. Global default timeout can be configured in
872 Pyrocko's configuration file under ``fdsn_timeout``.
873 :type timeout: float
874 :param \\*\\*kwargs:
875 Parameters that would be passed to the server (see `FDSN web services
876 specification <https://www.fdsn.org/webservices>`_).
878 :raises: :py:exc:`ValueError` is raised if unsupported parameters are
879 encountered.
880 '''
882 avail = supported_params_wadl(
883 service, site, url, majorversion, timeout, method)
885 unavail = sorted(set(kwargs.keys()) - avail)
886 if unavail:
887 raise ValueError(
888 'Unsupported parameter%s for service "%s" at site "%s": %s' % (
889 '' if len(unavail) == 1 else 's',
890 service,
891 site,
892 ', '.join(unavail)))
895def supported_params_wadl(
896 service,
897 site=g_default_site,
898 url=g_url,
899 majorversion=1,
900 timeout=None,
901 method='query'):
903 '''
904 Get query parameter names supported by a given FDSN site and service.
906 If no WADL is provided by the queried service, default parameter sets from
907 the FDSN standard are returned. Queried WADLs are cached in memory.
909 :param service: ``'station'``, ``'dataselect'``, ``'event'`` or
910 ``'availability'``.
911 :param site:
912 :ref:`Registered site name <registered-site-names>` or full base URL of
913 the service (e.g. ``'https://geofon.gfz-potsdam.de'``).
914 :type site: str
915 :param url:
916 URL template (default should work in 99% of cases).
917 :type url: str
918 :param majorversion:
919 Major version of the service to query (always ``1`` at the time of
920 writing).
921 :type majorversion: int
922 :param timeout:
923 Network timeout in [s]. Global default timeout can be configured in
924 Pyrocko's configuration file under ``fdsn_timeout``.
925 :type timeout: float
927 :returns: Supported parameter names.
928 :rtype: :py:class:`set` of :py:class:`str`
929 '''
931 wadl = cached_wadl(service, site, url, majorversion, timeout)
933 if wadl:
934 url = fillurl(service, site, url, majorversion, method)
935 return set(wadl.supported_param_names(url))
936 else:
937 return g_default_query_args[service]
940def patch_geonet_wadl(wadl):
941 for r in wadl.resources_list:
942 r.base = r.base.replace('1/station', 'station/1')
943 r.base = r.base.replace('1/dataselect', 'dataselect/1')
944 r.base = r.base.replace('1/event', 'event/1')
947def wadl(
948 service,
949 site=g_default_site,
950 url=g_url,
951 majorversion=1,
952 timeout=None):
954 '''
955 Retrieve self-description of a specific FDSN service.
957 :param service: ``'station'``, ``'dataselect'``, ``'event'`` or
958 ``'availability'``.
959 :param site:
960 :ref:`Registered site name <registered-site-names>` or full base URL of
961 the service (e.g. ``'https://geofon.gfz-potsdam.de'``).
962 :type site: str
963 :param url:
964 URL template (default should work in 99% of cases).
965 :type url: str
966 :param majorversion:
967 Major version of the service to query (always ``1`` at the time of
968 writing).
969 :type majorversion: int
970 :param timeout:
971 Network timeout in [s]. Global default timeout can be configured in
972 Pyrocko's configuration file under ``fdsn_timeout``.
973 :type timeout: float
974 '''
976 from pyrocko.client.wadl import load_xml
978 url = fillurl(service, site, url, majorversion, 'application.wadl')
980 wadl = load_xml(stream=_request(url, timeout=timeout))
982 if site == 'geonet' or site.find('geonet.org.nz') != -1:
983 patch_geonet_wadl(wadl)
985 return wadl
988g_wadls = {}
991def cached_wadl(
992 service,
993 site=g_default_site,
994 url=g_url,
995 majorversion=1,
996 timeout=None):
998 '''
999 Get self-description of a specific FDSN service.
1001 Same as :py:func:`wadl`, but results are cached in memory.
1002 '''
1004 k = (service, site, url, majorversion)
1005 if k not in g_wadls:
1006 try:
1007 g_wadls[k] = wadl(service, site, url, majorversion, timeout)
1009 except Timeout:
1010 raise
1012 except DownloadError:
1013 logger.info(
1014 'No service description (WADL) found for "%s" at site "%s".'
1015 % (service, site))
1017 g_wadls[k] = None
1019 return g_wadls[k]
1022__doc__ %= doc_table_dict(g_site_abbr, 'Site name', 'URL', ' ')
1025if __name__ == '__main__':
1026 import sys
1028 util.setup_logging('pyrocko.client.fdsn', 'info')
1030 if len(sys.argv) == 1:
1031 sites = get_sites()
1032 else:
1033 sites = sys.argv[1:]
1035 for site in sites:
1036 print('=== %s (%s) ===' % (site, g_site_abbr[site]))
1038 for service in ['station', 'dataselect', 'event']:
1039 try:
1040 app = wadl(service, site=site, timeout=2.0)
1041 print(indent(str(app)))
1043 except Timeout as e:
1044 logger.error(str(e))
1045 print('%s: timeout' % (service,))
1047 except util.DownloadError as e:
1048 logger.error(str(e))
1049 print('%s: no wadl' % (service,))