1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6''' 

7Low-level FDSN web service client. 

8 

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. 

14 

15.. _registered-site-names: 

16 

17Registered site names 

18..................... 

19 

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: 

22 

23%s 

24 

25Any other site can be specified by providing its full URL. 

26''' 

27 

28import re 

29import logging 

30import socket 

31 

32 

33from pyrocko import util 

34from pyrocko.util import DownloadError 

35from pyrocko import config 

36 

37from pyrocko.util import \ 

38 urlencode, Request, build_opener, HTTPDigestAuthHandler, urlopen, HTTPError 

39 

40 

41logger = logging.getLogger('pyrocko.client.fdsn') 

42 

43g_url = '%(site)s/fdsnws/%(service)s/%(majorversion)i/%(method)s' 

44 

45g_site_abbr = { 

46 'geofon': 'https://geofon.gfz-potsdam.de', 

47 'iris': 'http://service.iris.edu', 

48 'orfeus': 'http://www.orfeus-eu.org', 

49 'bgr': 'http://eida.bgr.de', 

50 'emsc': 'http://www.seismicportal.eu', 

51 'geonet': 'http://service.geonet.org.nz', 

52 'knmi': 'http://rdsa.knmi.nl', 

53 'ncedc': 'http://service.ncedc.org', 

54 'scedc': 'http://service.scedc.caltech.edu', 

55 'usgs': 'http://earthquake.usgs.gov', 

56 'koeri': 'http://eida-service.koeri.boun.edu.tr', 

57 'ethz': 'http://eida.ethz.ch', 

58 'icgc': 'http://ws.icgc.cat', 

59 'ipgp': 'http://eida.ipgp.fr', 

60 'ingv': 'http://webservices.ingv.it', 

61 'isc': 'http://www.isc.ac.uk', 

62 'lmu': 'http://erde.geophysik.uni-muenchen.de', 

63 'noa': 'http://eida.gein.noa.gr', 

64 'resif': 'http://ws.resif.fr', 

65 'usp': 'http://seisrequest.iag.usp.br', 

66 'niep': 'http://eida-sc3.infp.ro' 

67} 

68 

69g_default_site = 'geofon' 

70 

71 

72g_default_query_args = { 

73 'station': { 

74 'starttime', 'endtime', 'startbefore', 'startafter', 'endbefore', 

75 'endafter', 'network', 'station', 'location', 'channel', 'minlatitude', 

76 'maxlatitude', 'minlongitude', 'maxlongitude', 'latitude', 'longitude', 

77 'minradius', 'maxradius', 'level', 'includerestricted', 

78 'includeavailability', 'updatedafter', 'matchtimeseries', 'format', 

79 'nodata'}, 

80 'dataselect': { 

81 'starttime', 'endtime', 'network', 'station', 'location', 'channel', 

82 'quality', 'minimumlength', 'longestonly', 'format', 'nodata'}, 

83 'event': { 

84 'starttime', 'endtime', 'minlatitude', 'maxlatitude', 'minlongitude', 

85 'maxlongitude', 'latitude', 'longitude', 'minradius', 'maxradius', 

86 'mindepth', 'maxdepth', 'minmagnitude', 'maxmagnitude', 'eventtype', 

87 'includeallorigins', 'includeallmagnitudes', 'includearrivals', 

88 'eventid', 'limit', 'offset', 'orderby', 'catalog', 'contributor', 

89 'updatedafter', 'format', 'nodata'}, 

90 'availability': { 

91 'starttime', 'endtime', 'network', 'station', 'location', 'channel', 

92 'quality', 'merge', 'orderby', 'limit', 'includerestricted', 'format', 

93 'nodata', 'mergegaps', 'show'}} 

94 

95 

96def doc_escape_slist(li): 

97 return ', '.join("``'%s'``" % s for s in li) 

98 

99 

100def doc_table_dict(d, khead, vhead, indent=''): 

101 keys, vals = zip(*sorted(d.items())) 

102 

103 lk = max(max(len(k) for k in keys), len(khead)) 

104 lv = max(max(len(v) for v in vals), len(vhead)) 

105 

106 hr = '=' * lk + ' ' + '=' * lv 

107 

108 lines = [ 

109 hr, 

110 '%s %s' % (khead.ljust(lk), vhead.ljust(lv)), 

111 hr] 

112 

113 for k, v in zip(keys, vals): 

114 lines.append('%s %s' % (k.ljust(lk), v.ljust(lv))) 

115 

116 lines.append(hr) 

117 return '\n'.join(indent + line for line in lines) 

118 

119 

120def strip_html(s): 

121 s = s.decode('utf-8') 

122 s = re.sub(r'<[^>]+>', '', s) 

123 s = re.sub(r'\r', '', s) 

124 s = re.sub(r'\s*\n', '\n', s) 

125 return s 

126 

127 

128def indent(s, ind=' '): 

129 return '\n'.join(ind + line for line in s.splitlines()) 

130 

131 

132def get_sites(): 

133 ''' 

134 Get sorted list of registered site names. 

135 ''' 

136 return sorted(g_site_abbr.keys()) 

137 

138 

139if config.config().fdsn_timeout is None: 

140 g_timeout = 20. 

141else: 

142 g_timeout = config.config().fdsn_timeout 

143 

144re_realm_from_auth_header = re.compile(r'(realm)\s*[:=]\s*"([^"]*)"?') 

145 

146 

147class CannotGetRealmFromAuthHeader(DownloadError): 

148 ''' 

149 Raised when failing to parse server response during authentication. 

150 ''' 

151 pass 

152 

153 

154class CannotGetCredentialsFromAuthRequest(DownloadError): 

155 ''' 

156 Raised when failing to parse server response during token authentication. 

157 ''' 

158 pass 

159 

160 

161def get_realm_from_auth_header(headers): 

162 realm = dict(re_realm_from_auth_header.findall( 

163 headers['WWW-Authenticate'])).get('realm', None) 

164 

165 if realm is None: 

166 raise CannotGetRealmFromAuthHeader('headers=%s' % str(headers)) 

167 

168 return realm 

169 

170 

171def sdatetime(t): 

172 return util.time_to_str(t, format='%Y-%m-%dT%H:%M:%S') 

173 

174 

175class EmptyResult(DownloadError): 

176 ''' 

177 Raised when an empty server response is retrieved. 

178 ''' 

179 def __init__(self, url): 

180 DownloadError.__init__(self) 

181 self._url = url 

182 

183 def __str__(self): 

184 return 'No results for request %s' % self._url 

185 

186 

187class RequestEntityTooLarge(DownloadError): 

188 ''' 

189 Raised when the server indicates that too much data was requested. 

190 ''' 

191 def __init__(self, url): 

192 DownloadError.__init__(self) 

193 self._url = url 

194 

195 def __str__(self): 

196 return 'Request entity too large: %s' % self._url 

197 

198 

199class InvalidRequest(DownloadError): 

200 ''' 

201 Raised when an invalid request would be sent / has been sent. 

202 ''' 

203 pass 

204 

205 

206class Timeout(DownloadError): 

207 ''' 

208 Raised when the server does not respond within the allowed timeout period. 

209 ''' 

210 pass 

211 

212 

213def _request( 

214 url, 

215 post=False, 

216 user=None, 

217 passwd=None, 

218 timeout=g_timeout, 

219 **kwargs): 

220 

221 url_values = urlencode(kwargs) 

222 if url_values: 

223 url += '?' + url_values 

224 

225 logger.debug('Accessing URL %s' % url) 

226 url_args = { 

227 'timeout': timeout 

228 } 

229 

230 if util.g_ssl_context: 

231 url_args['context'] = util.g_ssl_context 

232 

233 opener = None 

234 

235 req = Request(url) 

236 if post: 

237 if isinstance(post, str): 

238 post = post.encode('utf8') 

239 logger.debug('POST data: \n%s' % post.decode('utf8')) 

240 req.data = post 

241 

242 req.add_header('Accept', '*/*') 

243 

244 itry = 0 

245 while True: 

246 itry += 1 

247 try: 

248 urlopen_ = opener.open if opener else urlopen 

249 try: 

250 resp = urlopen_(req, **url_args) 

251 except TypeError: 

252 # context and cafile not avail before 3.4.3, 2.7.9 

253 url_args.pop('context', None) 

254 url_args.pop('cafile', None) 

255 resp = urlopen_(req, **url_args) 

256 

257 logger.debug('Response: %s' % resp.getcode()) 

258 if resp.getcode() == 204: 

259 raise EmptyResult(url) 

260 return resp 

261 

262 except HTTPError as e: 

263 if e.code == 413: 

264 raise RequestEntityTooLarge(url) 

265 

266 elif e.code == 401: 

267 headers = getattr(e, 'headers', e.hdrs) 

268 

269 realm = get_realm_from_auth_header(headers) 

270 

271 if itry == 1 and user is not None: 

272 auth_handler = HTTPDigestAuthHandler() 

273 auth_handler.add_password( 

274 realm=realm, 

275 uri=url, 

276 user=user, 

277 passwd=passwd or '') 

278 

279 opener = build_opener(auth_handler) 

280 continue 

281 else: 

282 raise DownloadError( 

283 'Authentication failed for realm "%s" when accessing ' 

284 'url "%s". Original error was: %s' % ( 

285 realm, url, str(e))) 

286 

287 else: 

288 raise DownloadError( 

289 'Error content returned by server (HTML stripped):\n%s\n' 

290 ' Original error was: %s' % ( 

291 indent( 

292 strip_html(e.read()), 

293 ' ! '), 

294 str(e))) 

295 

296 except socket.timeout: 

297 raise Timeout( 

298 'Timeout error. No response received within %i s. You ' 

299 'may want to retry with a longer timeout setting.' % timeout) 

300 

301 break 

302 

303 

304def fillurl(service, site, url, majorversion, method): 

305 return url % dict( 

306 site=g_site_abbr.get(site, site), 

307 service=service, 

308 majorversion=majorversion, 

309 method=method) 

310 

311 

312def fix_params(d): 

313 

314 params = dict(d) 

315 for k in ['starttime', 

316 'endtime', 

317 'startbefore', 

318 'startafter', 

319 'endbefore', 

320 'endafter', 

321 'updatedafter']: 

322 

323 if k in params: 

324 params[k] = sdatetime(params[k]) 

325 

326 if params.get('location', None) == '': 

327 params['location'] = '--' 

328 

329 for k in params: 

330 if isinstance(params[k], bool): 

331 params[k] = ['false', 'true'][bool(params[k])] 

332 

333 return params 

334 

335 

336def make_data_selection( 

337 stations, tmin, tmax, 

338 channel_prio=[['BHZ', 'HHZ'], 

339 ['BH1', 'BHN', 'HH1', 'HHN'], 

340 ['BH2', 'BHE', 'HH2', 'HHE']]): 

341 

342 selection = [] 

343 for station in stations: 

344 wanted = [] 

345 for group in channel_prio: 

346 gchannels = [] 

347 for channel in station.get_channels(): 

348 if channel.name in group: 

349 gchannels.append(channel) 

350 if gchannels: 

351 gchannels.sort(key=lambda a: group.index(a.name)) 

352 wanted.append(gchannels[0]) 

353 

354 if wanted: 

355 for channel in wanted: 

356 selection.append((station.network, station.station, 

357 station.location, channel.name, tmin, tmax)) 

358 

359 return selection 

360 

361 

362def station( 

363 site=g_default_site, 

364 url=g_url, 

365 majorversion=1, 

366 timeout=g_timeout, 

367 check=True, 

368 selection=None, 

369 parsed=True, 

370 **kwargs): 

371 

372 ''' 

373 Query FDSN web service for station metadata. 

374 

375 :param site: 

376 :ref:`Registered site name <registered-site-names>` or full base URL of 

377 the service (e.g. ``'https://geofon.gfz-potsdam.de'``). 

378 :type site: str, optional 

379 :param url: 

380 URL template (default should work in 99% of cases). 

381 :type url: str, optional 

382 :param majorversion: 

383 Major version of the service to query (always ``1`` at the time of 

384 writing). 

385 :type majorversion: int, optional 

386 :param timeout: 

387 Network timeout in [s]. Global default timeout can be configured in 

388 Pyrocko's configuration file under ``fdsn_timeout``. 

389 :type timeout: float, optional 

390 :param check: 

391 If ``True`` arguments are checked against self-description (WADL) of 

392 the queried web service if available or FDSN specification. 

393 :type check: bool, optional 

394 :param selection: 

395 If given, selection to be queried as a list of tuples 

396 ``(network, station, location, channel, tmin, tmax)``. Useful for 

397 detailed queries. 

398 :type selection: list of tuples, optional 

399 :param parsed: 

400 If ``True`` parse received content into 

401 :py:class:`~pyrocko.io.stationxml.FDSNStationXML` 

402 object, otherwise return open file handle to raw data stream. 

403 :type parsed: bool, optional 

404 :param \\*\\*kwargs: 

405 Parameters passed to the server (see `FDSN web services specification 

406 <https://www.fdsn.org/webservices>`_). 

407 

408 :returns: 

409 See description of ``parsed`` argument above. 

410 

411 :raises: 

412 On failure, :py:exc:`~pyrocko.util.DownloadError` or one of its 

413 sub-types defined in the :py:mod:`~pyrocko.client.fdsn` module is 

414 raised. 

415 ''' 

416 

417 service = 'station' 

418 

419 if check: 

420 check_params(service, site, url, majorversion, timeout, **kwargs) 

421 

422 params = fix_params(kwargs) 

423 

424 url = fillurl(service, site, url, majorversion, 'query') 

425 if selection: 

426 lst = [] 

427 for k, v in params.items(): 

428 lst.append('%s=%s' % (k, v)) 

429 

430 for (network, station, location, channel, tmin, tmax) in selection: 

431 if location == '': 

432 location = '--' 

433 

434 lst.append(' '.join((network, station, location, channel, 

435 sdatetime(tmin), sdatetime(tmax)))) 

436 

437 post = '\n'.join(lst) 

438 params = dict(post=post.encode()) 

439 

440 if parsed: 

441 from pyrocko.io import stationxml 

442 format = kwargs.get('format', 'xml') 

443 if format == 'text': 

444 if kwargs.get('level', 'station') == 'channel': 

445 return stationxml.load_channel_table( 

446 stream=_request(url, timeout=timeout, **params)) 

447 else: 

448 raise InvalidRequest('if format="text" shall be parsed, ' 

449 'level="channel" is required') 

450 

451 elif format == 'xml': 

452 return stationxml.load_xml( 

453 stream=_request(url, timeout=timeout, **params)) 

454 else: 

455 raise InvalidRequest('format must be "xml" or "text"') 

456 else: 

457 return _request(url, timeout=timeout, **params) 

458 

459 

460def get_auth_credentials(service, site, url, majorversion, token, timeout): 

461 

462 url = fillurl(service, site, url, majorversion, 'auth') 

463 

464 f = _request(url, timeout=timeout, post=token) 

465 s = f.read().decode() 

466 try: 

467 user, passwd = s.strip().split(':') 

468 except ValueError: 

469 raise CannotGetCredentialsFromAuthRequest('data="%s"' % s) 

470 

471 return user, passwd 

472 

473 

474def dataselect( 

475 site=g_default_site, 

476 url=g_url, 

477 majorversion=1, 

478 timeout=g_timeout, 

479 check=True, 

480 user=None, 

481 passwd=None, 

482 token=None, 

483 selection=None, 

484 **kwargs): 

485 

486 ''' 

487 Query FDSN web service for time series data in miniSEED format. 

488 

489 :param site: 

490 :ref:`Registered site name <registered-site-names>` or full base URL of 

491 the service (e.g. ``'https://geofon.gfz-potsdam.de'``). 

492 :type site: str, optional 

493 :param url: 

494 URL template (default should work in 99% of cases). 

495 :type url: str, optional 

496 :param majorversion: 

497 Major version of the service to query (always ``1`` at the time of 

498 writing). 

499 :type majorversion: int, optional 

500 :param timeout: 

501 Network timeout in [s]. Global default timeout can be configured in 

502 Pyrocko's configuration file under ``fdsn_timeout``. 

503 :type timeout: float, optional 

504 :param check: 

505 If ``True`` arguments are checked against self-description (WADL) of 

506 the queried web service if available or FDSN specification. 

507 :type check: bool, optional 

508 :param user: User name for user/password authentication. 

509 :type user: str, optional 

510 :param passwd: Password for user/password authentication. 

511 :type passwd: str, optional 

512 :param token: Token for `token authentication 

513 <https://geofon.gfz-potsdam.de/waveform/archive/auth/auth-overview.php>`_. 

514 :type token: str, optional 

515 :param selection: 

516 If given, selection to be queried as a list of tuples 

517 ``(network, station, location, channel, tmin, tmax)``. 

518 :type selection: list of tuples, optional 

519 :param \\*\\*kwargs: 

520 Parameters passed to the server (see `FDSN web services specification 

521 <https://www.fdsn.org/webservices>`_). 

522 

523 :returns: 

524 Open file-like object providing raw miniSEED data. 

525 ''' 

526 

527 service = 'dataselect' 

528 

529 if user or token: 

530 method = 'queryauth' 

531 else: 

532 method = 'query' 

533 

534 if token is not None: 

535 user, passwd = get_auth_credentials( 

536 service, site, url, majorversion, token, timeout) 

537 

538 if check: 

539 check_params(service, site, url, majorversion, timeout, **kwargs) 

540 

541 params = fix_params(kwargs) 

542 

543 url = fillurl(service, site, url, majorversion, method) 

544 if selection: 

545 lst = [] 

546 

547 for k, v in params.items(): 

548 lst.append('%s=%s' % (k, v)) 

549 

550 for (network, station, location, channel, tmin, tmax) in selection: 

551 if location == '': 

552 location = '--' 

553 

554 lst.append(' '.join((network, station, location, channel, 

555 sdatetime(tmin), sdatetime(tmax)))) 

556 

557 post = '\n'.join(lst) 

558 return _request( 

559 url, user=user, passwd=passwd, post=post.encode(), timeout=timeout) 

560 else: 

561 return _request( 

562 url, user=user, passwd=passwd, timeout=timeout, **params) 

563 

564 

565def event( 

566 site=g_default_site, 

567 url=g_url, 

568 majorversion=1, 

569 timeout=g_timeout, 

570 check=True, 

571 user=None, 

572 passwd=None, 

573 token=None, 

574 parsed=False, 

575 **kwargs): 

576 

577 ''' 

578 Query FDSN web service for events. 

579 

580 :param site: 

581 :ref:`Registered site name <registered-site-names>` or full base URL of 

582 the service (e.g. ``'https://geofon.gfz-potsdam.de'``). 

583 :type site: str, optional 

584 :param url: 

585 URL template (default should work in 99% of cases). 

586 :type url: str, optional 

587 :param majorversion: 

588 Major version of the service to query (always ``1`` at the time of 

589 writing). 

590 :type majorversion: int, optional 

591 :param timeout: 

592 Network timeout in [s]. Global default timeout can be configured in 

593 Pyrocko's configuration file under ``fdsn_timeout``. 

594 :type timeout: float, optional 

595 :param check: 

596 If ``True`` arguments are checked against self-description (WADL) of 

597 the queried web service if available or FDSN specification. 

598 :type check: bool, optional 

599 :param user: User name for user/password authentication. 

600 :type user: str, optional 

601 :param passwd: Password for user/password authentication. 

602 :type passwd: str, optional 

603 :param token: Token for `token authentication 

604 <https://geofon.gfz-potsdam.de/waveform/archive/auth/auth-overview.php>`_. 

605 :type token: str, optional 

606 :param parsed: 

607 If ``True`` parse received content into 

608 :py:class:`~pyrocko.io.quakeml.QuakeML` 

609 object, otherwise return open file handle to raw data stream. Note: 

610 by default unparsed data is retrieved, differently from the default 

611 behaviour of :py:func:`station` (for backward compatibility). 

612 :type parsed: bool, optional 

613 :param \\*\\*kwargs: 

614 Parameters passed to the server (see `FDSN web services specification 

615 <https://www.fdsn.org/webservices>`_). 

616 

617 :returns: 

618 See description of ``parsed`` argument above. 

619 ''' 

620 

621 service = 'event' 

622 

623 if user or token: 

624 method = 'queryauth' 

625 else: 

626 method = 'query' 

627 

628 if token is not None: 

629 user, passwd = get_auth_credentials( 

630 service, site, url, majorversion, token, timeout) 

631 

632 if check: 

633 check_params(service, site, url, majorversion, timeout, **kwargs) 

634 

635 params = fix_params(kwargs) 

636 

637 url = fillurl(service, site, url, majorversion, method) 

638 

639 fh = _request(url, user=user, passwd=passwd, timeout=timeout, **params) 

640 if parsed: 

641 from pyrocko.io import quakeml 

642 format = kwargs.get('format', 'xml') 

643 if format != 'xml': 

644 raise InvalidRequest( 

645 'If parsed=True is selected, format="xml" must be selected.') 

646 

647 return quakeml.QuakeML.load_xml(stream=fh) 

648 

649 else: 

650 return fh 

651 

652 

653def availability( 

654 method='query', 

655 site=g_default_site, 

656 url=g_url, 

657 majorversion=1, 

658 timeout=g_timeout, 

659 check=True, 

660 user=None, 

661 passwd=None, 

662 token=None, 

663 selection=None, 

664 **kwargs): 

665 

666 ''' 

667 Query FDSN web service for time series data availablity. 

668 

669 :param method: Availablility method to call: ``'query'``, or ``'extent'``. 

670 :param site: 

671 :ref:`Registered site name <registered-site-names>` or full base URL of 

672 the service (e.g. ``'https://geofon.gfz-potsdam.de'``). 

673 :type site: str, optional 

674 :param url: 

675 URL template (default should work in 99% of cases). 

676 :type url: str, optional 

677 :param majorversion: 

678 Major version of the service to query (always ``1`` at the time of 

679 writing). 

680 :type majorversion: int, optional 

681 :param timeout: 

682 Network timeout in [s]. Global default timeout can be configured in 

683 Pyrocko's configuration file under ``fdsn_timeout``. 

684 :type timeout: float, optional 

685 :param check: 

686 If ``True`` arguments are checked against self-description (WADL) of 

687 the queried web service if available or FDSN specification. 

688 :type check: bool, optional 

689 :param user: User name for user/password authentication. 

690 :type user: str, optional 

691 :param passwd: Password for user/password authentication. 

692 :type passwd: str, optional 

693 :param token: Token for `token authentication 

694 <https://geofon.gfz-potsdam.de/waveform/archive/auth/auth-overview.php>`_. 

695 :type token: str, optional 

696 :param selection: 

697 If given, selection to be queried as a list of tuples 

698 ``(network, station, location, channel, tmin, tmax)``. 

699 :type selection: list of tuples, optional 

700 :param \\*\\*kwargs: 

701 Parameters passed to the server (see `FDSN web services specification 

702 <https://www.fdsn.org/webservices>`_). 

703 

704 :returns: 

705 Open file-like object providing raw response. 

706 ''' 

707 

708 service = 'availability' 

709 

710 assert method in ('query', 'extent') 

711 

712 if user or token: 

713 method += 'auth' 

714 

715 if token is not None: 

716 user, passwd = get_auth_credentials( 

717 service, site, url, majorversion, token, timeout) 

718 

719 if check: 

720 check_params(service, site, url, majorversion, timeout, **kwargs) 

721 

722 params = fix_params(kwargs) 

723 

724 url = fillurl(service, site, url, majorversion, method) 

725 if selection: 

726 lst = [] 

727 

728 for k, v in params.items(): 

729 lst.append('%s=%s' % (k, v)) 

730 

731 for (network, station, location, channel, tmin, tmax) in selection: 

732 if location == '': 

733 location = '--' 

734 

735 lst.append(' '.join((network, station, location, channel, 

736 sdatetime(tmin), sdatetime(tmax)))) 

737 

738 post = '\n'.join(lst) 

739 return _request( 

740 url, user=user, passwd=passwd, post=post.encode(), timeout=timeout) 

741 else: 

742 return _request( 

743 url, user=user, passwd=passwd, timeout=timeout, **params) 

744 

745 

746def check_params( 

747 service, 

748 site=g_default_site, 

749 url=g_url, 

750 majorversion=1, 

751 timeout=g_timeout, 

752 method='query', 

753 **kwargs): 

754 

755 ''' 

756 Check query parameters against self-description of web service. 

757 

758 Downloads WADL description of the given service and site and checks 

759 parameters if they are available. Queried WADLs are cached in memory. 

760 

761 :param service: ``'station'``, ``'dataselect'``, ``'event'`` or 

762 ``'availability'``. 

763 :param site: 

764 :ref:`Registered site name <registered-site-names>` or full base URL of 

765 the service (e.g. ``'https://geofon.gfz-potsdam.de'``). 

766 :type site: str, optional 

767 :param url: 

768 URL template (default should work in 99% of cases). 

769 :type url: str, optional 

770 :param majorversion: 

771 Major version of the service to query (always ``1`` at the time of 

772 writing). 

773 :type majorversion: int, optional 

774 :param timeout: 

775 Network timeout in [s]. Global default timeout can be configured in 

776 Pyrocko's configuration file under ``fdsn_timeout``. 

777 :type timeout: float, optional 

778 :param \\*\\*kwargs: 

779 Parameters that would be passed to the server (see `FDSN web services 

780 specification <https://www.fdsn.org/webservices>`_). 

781 

782 :raises: :py:exc:`ValueError` is raised if unsupported parameters are 

783 encountered. 

784 ''' 

785 

786 avail = supported_params_wadl( 

787 service, site, url, majorversion, timeout, method) 

788 

789 unavail = sorted(set(kwargs.keys()) - avail) 

790 if unavail: 

791 raise ValueError( 

792 'Unsupported parameter%s for service "%s" at site "%s": %s' % ( 

793 '' if len(unavail) == 1 else 's', 

794 service, 

795 site, 

796 ', '.join(unavail))) 

797 

798 

799def supported_params_wadl( 

800 service, 

801 site=g_default_site, 

802 url=g_url, 

803 majorversion=1, 

804 timeout=g_timeout, 

805 method='query'): 

806 

807 ''' 

808 Get query parameter names supported by a given FDSN site and service. 

809 

810 If no WADL is provided by the queried service, default parameter sets from 

811 the FDSN standard are returned. Queried WADLs are cached in memory. 

812 

813 :param service: ``'station'``, ``'dataselect'``, ``'event'`` or 

814 ``'availability'``. 

815 :param site: 

816 :ref:`Registered site name <registered-site-names>` or full base URL of 

817 the service (e.g. ``'https://geofon.gfz-potsdam.de'``). 

818 :type site: str, optional 

819 :param url: 

820 URL template (default should work in 99% of cases). 

821 :type url: str, optional 

822 :param majorversion: 

823 Major version of the service to query (always ``1`` at the time of 

824 writing). 

825 :type majorversion: int, optional 

826 :param timeout: 

827 Network timeout in [s]. Global default timeout can be configured in 

828 Pyrocko's configuration file under ``fdsn_timeout``. 

829 :type timeout: float, optional 

830 

831 :returns: Supported parameter names. 

832 :rtype: set of str 

833 ''' 

834 

835 wadl = cached_wadl(service, site, url, majorversion, timeout) 

836 

837 if wadl: 

838 url = fillurl(service, site, url, majorversion, method) 

839 return set(wadl.supported_param_names(url)) 

840 else: 

841 return g_default_query_args[service] 

842 

843 

844def patch_geonet_wadl(wadl): 

845 for r in wadl.resources_list: 

846 r.base = r.base.replace('1/station', 'station/1') 

847 r.base = r.base.replace('1/dataselect', 'dataselect/1') 

848 r.base = r.base.replace('1/event', 'event/1') 

849 

850 

851def wadl( 

852 service, 

853 site=g_default_site, 

854 url=g_url, 

855 majorversion=1, 

856 timeout=g_timeout): 

857 

858 ''' 

859 Retrieve self-description of a specific FDSN service. 

860 

861 :param service: ``'station'``, ``'dataselect'``, ``'event'`` or 

862 ``'availability'``. 

863 :param site: 

864 :ref:`Registered site name <registered-site-names>` or full base URL of 

865 the service (e.g. ``'https://geofon.gfz-potsdam.de'``). 

866 :type site: str, optional 

867 :param url: 

868 URL template (default should work in 99% of cases). 

869 :type url: str, optional 

870 :param majorversion: 

871 Major version of the service to query (always ``1`` at the time of 

872 writing). 

873 :type majorversion: int, optional 

874 :param timeout: 

875 Network timeout in [s]. Global default timeout can be configured in 

876 Pyrocko's configuration file under ``fdsn_timeout``. 

877 :type timeout: float, optional 

878 ''' 

879 

880 from pyrocko.client.wadl import load_xml 

881 

882 url = fillurl(service, site, url, majorversion, 'application.wadl') 

883 

884 wadl = load_xml(stream=_request(url, timeout=timeout)) 

885 

886 if site == 'geonet' or site.find('geonet.org.nz') != -1: 

887 patch_geonet_wadl(wadl) 

888 

889 return wadl 

890 

891 

892g_wadls = {} 

893 

894 

895def cached_wadl( 

896 service, 

897 site=g_default_site, 

898 url=g_url, 

899 majorversion=1, 

900 timeout=g_timeout): 

901 

902 ''' 

903 Get self-description of a specific FDSN service. 

904 

905 Same as :py:func:`wadl`, but results are cached in memory. 

906 ''' 

907 

908 k = (service, site, url, majorversion) 

909 if k not in g_wadls: 

910 try: 

911 g_wadls[k] = wadl(service, site, url, majorversion, timeout) 

912 

913 except Timeout: 

914 raise 

915 

916 except DownloadError: 

917 logger.info( 

918 'No service description (WADL) found for "%s" at site "%s".' 

919 % (service, site)) 

920 

921 g_wadls[k] = None 

922 

923 return g_wadls[k] 

924 

925 

926__doc__ %= doc_table_dict(g_site_abbr, 'Site name', 'URL', ' ') 

927 

928 

929if __name__ == '__main__': 

930 import sys 

931 

932 util.setup_logging('pyrocko.client.fdsn', 'info') 

933 

934 if len(sys.argv) == 1: 

935 sites = get_sites() 

936 else: 

937 sites = sys.argv[1:] 

938 

939 for site in sites: 

940 print('=== %s (%s) ===' % (site, g_site_abbr[site])) 

941 

942 for service in ['station', 'dataselect', 'event']: 

943 try: 

944 app = wadl(service, site=site, timeout=2.0) 

945 print(indent(str(app))) 

946 

947 except Timeout as e: 

948 logger.error(str(e)) 

949 print('%s: timeout' % (service,)) 

950 

951 except util.DownloadError as e: 

952 logger.error(str(e)) 

953 print('%s: no wadl' % (service,))