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 'geonet': 'http://service.geonet.org.nz', 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

66} 

67 

68g_default_site = 'geofon' 

69 

70 

71g_default_query_args = { 

72 'station': { 

73 'starttime', 'endtime', 'startbefore', 'startafter', 'endbefore', 

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

75 'maxlatitude', 'minlongitude', 'maxlongitude', 'latitude', 'longitude', 

76 'minradius', 'maxradius', 'level', 'includerestricted', 

77 'includeavailability', 'updatedafter', 'matchtimeseries', 'format', 

78 'nodata'}, 

79 'dataselect': { 

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

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

82 'event': { 

83 'starttime', 'endtime', 'minlatitude', 'maxlatitude', 'minlongitude', 

84 'maxlongitude', 'latitude', 'longitude', 'minradius', 'maxradius', 

85 'mindepth', 'maxdepth', 'minmagnitude', 'maxmagnitude', 'eventtype', 

86 'includeallorigins', 'includeallmagnitudes', 'includearrivals', 

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

88 'updatedafter', 'format', 'nodata'}, 

89 'availability': { 

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

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

92 'nodata', 'mergegaps', 'show'}} 

93 

94 

95def doc_escape_slist(li): 

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

97 

98 

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

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

101 

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

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

104 

105 hr = '=' * lk + ' ' + '=' * lv 

106 

107 lines = [ 

108 hr, 

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

110 hr] 

111 

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

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

114 

115 lines.append(hr) 

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

117 

118 

119def strip_html(s): 

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

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

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

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

124 return s 

125 

126 

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

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

129 

130 

131def get_sites(): 

132 ''' 

133 Get sorted list of registered site names. 

134 ''' 

135 return sorted(g_site_abbr.keys()) 

136 

137 

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

139 g_timeout = 20. 

140else: 

141 g_timeout = config.config().fdsn_timeout 

142 

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

144 

145 

146class CannotGetRealmFromAuthHeader(DownloadError): 

147 ''' 

148 Raised when failing to parse server response during authentication. 

149 ''' 

150 pass 

151 

152 

153class CannotGetCredentialsFromAuthRequest(DownloadError): 

154 ''' 

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

156 ''' 

157 pass 

158 

159 

160def get_realm_from_auth_header(headers): 

161 realm = dict(re_realm_from_auth_header.findall( 

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

163 

164 if realm is None: 

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

166 

167 return realm 

168 

169 

170def sdatetime(t): 

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

172 

173 

174class EmptyResult(DownloadError): 

175 ''' 

176 Raised when an empty server response is retrieved. 

177 ''' 

178 def __init__(self, url): 

179 DownloadError.__init__(self) 

180 self._url = url 

181 

182 def __str__(self): 

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

184 

185 

186class RequestEntityTooLarge(DownloadError): 

187 ''' 

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

189 ''' 

190 def __init__(self, url): 

191 DownloadError.__init__(self) 

192 self._url = url 

193 

194 def __str__(self): 

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

196 

197 

198class InvalidRequest(DownloadError): 

199 ''' 

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

201 ''' 

202 pass 

203 

204 

205class Timeout(DownloadError): 

206 ''' 

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

208 ''' 

209 pass 

210 

211 

212def _request( 

213 url, 

214 post=False, 

215 user=None, 

216 passwd=None, 

217 timeout=g_timeout, 

218 **kwargs): 

219 

220 url_values = urlencode(kwargs) 

221 if url_values: 

222 url += '?' + url_values 

223 

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

225 url_args = { 

226 'timeout': timeout 

227 } 

228 

229 if util.g_ssl_context: 

230 url_args['context'] = util.g_ssl_context 

231 

232 opener = None 

233 

234 req = Request(url) 

235 if post: 

236 if isinstance(post, str): 

237 post = post.encode('utf8') 

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

239 req.data = post 

240 

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

242 

243 itry = 0 

244 while True: 

245 itry += 1 

246 try: 

247 urlopen_ = opener.open if opener else urlopen 

248 try: 

249 resp = urlopen_(req, **url_args) 

250 except TypeError: 

251 # context and cafile not avail before 3.4.3, 2.7.9 

252 url_args.pop('context', None) 

253 url_args.pop('cafile', None) 

254 resp = urlopen_(req, **url_args) 

255 

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

257 if resp.getcode() == 204: 

258 raise EmptyResult(url) 

259 return resp 

260 

261 except HTTPError as e: 

262 if e.code == 413: 

263 raise RequestEntityTooLarge(url) 

264 

265 elif e.code == 401: 

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

267 

268 realm = get_realm_from_auth_header(headers) 

269 

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

271 auth_handler = HTTPDigestAuthHandler() 

272 auth_handler.add_password( 

273 realm=realm, 

274 uri=url, 

275 user=user, 

276 passwd=passwd or '') 

277 

278 opener = build_opener(auth_handler) 

279 continue 

280 else: 

281 raise DownloadError( 

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

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

284 realm, url, str(e))) 

285 

286 else: 

287 raise DownloadError( 

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

289 ' Original error was: %s' % ( 

290 indent( 

291 strip_html(e.read()), 

292 ' ! '), 

293 str(e))) 

294 

295 except socket.timeout: 

296 raise Timeout( 

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

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

299 

300 break 

301 

302 

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

304 return url % dict( 

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

306 service=service, 

307 majorversion=majorversion, 

308 method=method) 

309 

310 

311def fix_params(d): 

312 

313 params = dict(d) 

314 for k in ['starttime', 

315 'endtime', 

316 'startbefore', 

317 'startafter', 

318 'endbefore', 

319 'endafter', 

320 'updatedafter']: 

321 

322 if k in params: 

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

324 

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

326 params['location'] = '--' 

327 

328 for k in params: 

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

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

331 

332 return params 

333 

334 

335def make_data_selection( 

336 stations, tmin, tmax, 

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

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

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

340 

341 selection = [] 

342 for station in stations: 

343 wanted = [] 

344 for group in channel_prio: 

345 gchannels = [] 

346 for channel in station.get_channels(): 

347 if channel.name in group: 

348 gchannels.append(channel) 

349 if gchannels: 

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

351 wanted.append(gchannels[0]) 

352 

353 if wanted: 

354 for channel in wanted: 

355 selection.append((station.network, station.station, 

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

357 

358 return selection 

359 

360 

361def station( 

362 site=g_default_site, 

363 url=g_url, 

364 majorversion=1, 

365 timeout=g_timeout, 

366 check=True, 

367 selection=None, 

368 parsed=True, 

369 **kwargs): 

370 

371 ''' 

372 Query FDSN web service for station metadata. 

373 

374 :param site: 

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

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

377 :type site: str, optional 

378 :param url: 

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

380 :type url: str, optional 

381 :param majorversion: 

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

383 writing). 

384 :type majorversion: int, optional 

385 :param timeout: 

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

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

388 :type timeout: float, optional 

389 :param check: 

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

391 the queried web service if available or FDSN specification. 

392 :type check: bool, optional 

393 :param selection: 

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

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

396 detailed queries. 

397 :type selection: list of tuples, optional 

398 :param parsed: 

399 If ``True`` parse received content into 

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

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

402 :type parsed: bool, optional 

403 :param \\*\\*kwargs: 

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

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

406 

407 :returns: 

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

409 

410 :raises: 

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

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

413 raised. 

414 ''' 

415 

416 service = 'station' 

417 

418 if check: 

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

420 

421 params = fix_params(kwargs) 

422 

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

424 if selection: 

425 lst = [] 

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

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

428 

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

430 if location == '': 

431 location = '--' 

432 

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

434 sdatetime(tmin), sdatetime(tmax)))) 

435 

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

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

438 

439 if parsed: 

440 from pyrocko.io import stationxml 

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

442 if format == 'text': 

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

444 return stationxml.load_channel_table( 

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

446 else: 

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

448 'level="channel" is required') 

449 

450 elif format == 'xml': 

451 return stationxml.load_xml( 

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

453 else: 

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

455 else: 

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

457 

458 

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

460 

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

462 

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

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

465 try: 

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

467 except ValueError: 

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

469 

470 return user, passwd 

471 

472 

473def dataselect( 

474 site=g_default_site, 

475 url=g_url, 

476 majorversion=1, 

477 timeout=g_timeout, 

478 check=True, 

479 user=None, 

480 passwd=None, 

481 token=None, 

482 selection=None, 

483 **kwargs): 

484 

485 ''' 

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

487 

488 :param site: 

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

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

491 :type site: str, optional 

492 :param url: 

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

494 :type url: str, optional 

495 :param majorversion: 

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

497 writing). 

498 :type majorversion: int, optional 

499 :param timeout: 

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

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

502 :type timeout: float, optional 

503 :param check: 

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

505 the queried web service if available or FDSN specification. 

506 :type check: bool, optional 

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

508 :type user: str, optional 

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

510 :type passwd: str, optional 

511 :param token: Token for `token authentication 

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

513 :type token: str, optional 

514 :param selection: 

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

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

517 :type selection: list of tuples, optional 

518 :param \\*\\*kwargs: 

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

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

521 

522 :returns: 

523 Open file-like object providing raw miniSEED data. 

524 ''' 

525 

526 service = 'dataselect' 

527 

528 if user or token: 

529 method = 'queryauth' 

530 else: 

531 method = 'query' 

532 

533 if token is not None: 

534 user, passwd = get_auth_credentials( 

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

536 

537 if check: 

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

539 

540 params = fix_params(kwargs) 

541 

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

543 if selection: 

544 lst = [] 

545 

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

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

548 

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

550 if location == '': 

551 location = '--' 

552 

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

554 sdatetime(tmin), sdatetime(tmax)))) 

555 

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

557 return _request( 

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

559 else: 

560 return _request( 

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

562 

563 

564def event( 

565 site=g_default_site, 

566 url=g_url, 

567 majorversion=1, 

568 timeout=g_timeout, 

569 check=True, 

570 user=None, 

571 passwd=None, 

572 token=None, 

573 parsed=False, 

574 **kwargs): 

575 

576 ''' 

577 Query FDSN web service for events. 

578 

579 :param site: 

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

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

582 :type site: str, optional 

583 :param url: 

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

585 :type url: str, optional 

586 :param majorversion: 

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

588 writing). 

589 :type majorversion: int, optional 

590 :param timeout: 

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

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

593 :type timeout: float, optional 

594 :param check: 

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

596 the queried web service if available or FDSN specification. 

597 :type check: bool, optional 

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

599 :type user: str, optional 

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

601 :type passwd: str, optional 

602 :param token: Token for `token authentication 

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

604 :type token: str, optional 

605 :param parsed: 

606 If ``True`` parse received content into 

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

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

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

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

611 :type parsed: bool, optional 

612 :param \\*\\*kwargs: 

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

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

615 

616 :returns: 

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

618 ''' 

619 

620 service = 'event' 

621 

622 if user or token: 

623 method = 'queryauth' 

624 else: 

625 method = 'query' 

626 

627 if token is not None: 

628 user, passwd = get_auth_credentials( 

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

630 

631 if check: 

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

633 

634 params = fix_params(kwargs) 

635 

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

637 

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

639 if parsed: 

640 from pyrocko.io import quakeml 

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

642 if format != 'xml': 

643 raise InvalidRequest( 

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

645 

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

647 

648 else: 

649 return fh 

650 

651 

652def availability( 

653 method='query', 

654 site=g_default_site, 

655 url=g_url, 

656 majorversion=1, 

657 timeout=g_timeout, 

658 check=True, 

659 user=None, 

660 passwd=None, 

661 token=None, 

662 selection=None, 

663 **kwargs): 

664 

665 ''' 

666 Query FDSN web service for time series data availablity. 

667 

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

669 :param site: 

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

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

672 :type site: str, optional 

673 :param url: 

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

675 :type url: str, optional 

676 :param majorversion: 

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

678 writing). 

679 :type majorversion: int, optional 

680 :param timeout: 

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

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

683 :type timeout: float, optional 

684 :param check: 

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

686 the queried web service if available or FDSN specification. 

687 :type check: bool, optional 

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

689 :type user: str, optional 

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

691 :type passwd: str, optional 

692 :param token: Token for `token authentication 

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

694 :type token: str, optional 

695 :param selection: 

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

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

698 :type selection: list of tuples, optional 

699 :param \\*\\*kwargs: 

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

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

702 

703 :returns: 

704 Open file-like object providing raw response. 

705 ''' 

706 

707 service = 'availability' 

708 

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

710 

711 if user or token: 

712 method += 'auth' 

713 

714 if token is not None: 

715 user, passwd = get_auth_credentials( 

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

717 

718 if check: 

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

720 

721 params = fix_params(kwargs) 

722 

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

724 if selection: 

725 lst = [] 

726 

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

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

729 

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

731 if location == '': 

732 location = '--' 

733 

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

735 sdatetime(tmin), sdatetime(tmax)))) 

736 

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

738 return _request( 

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

740 else: 

741 return _request( 

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

743 

744 

745def check_params( 

746 service, 

747 site=g_default_site, 

748 url=g_url, 

749 majorversion=1, 

750 timeout=g_timeout, 

751 method='query', 

752 **kwargs): 

753 

754 ''' 

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

756 

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

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

759 

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

761 ``'availability'``. 

762 :param site: 

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

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

765 :type site: str, optional 

766 :param url: 

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

768 :type url: str, optional 

769 :param majorversion: 

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

771 writing). 

772 :type majorversion: int, optional 

773 :param timeout: 

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

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

776 :type timeout: float, optional 

777 :param \\*\\*kwargs: 

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

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

780 

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

782 encountered. 

783 ''' 

784 

785 avail = supported_params_wadl( 

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

787 

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

789 if unavail: 

790 raise ValueError( 

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

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

793 service, 

794 site, 

795 ', '.join(unavail))) 

796 

797 

798def supported_params_wadl( 

799 service, 

800 site=g_default_site, 

801 url=g_url, 

802 majorversion=1, 

803 timeout=g_timeout, 

804 method='query'): 

805 

806 ''' 

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

808 

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

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

811 

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

813 ``'availability'``. 

814 :param site: 

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

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

817 :type site: str, optional 

818 :param url: 

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

820 :type url: str, optional 

821 :param majorversion: 

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

823 writing). 

824 :type majorversion: int, optional 

825 :param timeout: 

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

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

828 :type timeout: float, optional 

829 

830 :returns: Supported parameter names. 

831 :rtype: set of str 

832 ''' 

833 

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

835 

836 if wadl: 

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

838 return set(wadl.supported_param_names(url)) 

839 else: 

840 return g_default_query_args[service] 

841 

842 

843def patch_geonet_wadl(wadl): 

844 for r in wadl.resources_list: 

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

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

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

848 

849 

850def wadl( 

851 service, 

852 site=g_default_site, 

853 url=g_url, 

854 majorversion=1, 

855 timeout=g_timeout): 

856 

857 ''' 

858 Retrieve self-description of a specific FDSN service. 

859 

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

861 ``'availability'``. 

862 :param site: 

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

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

865 :type site: str, optional 

866 :param url: 

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

868 :type url: str, optional 

869 :param majorversion: 

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

871 writing). 

872 :type majorversion: int, optional 

873 :param timeout: 

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

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

876 :type timeout: float, optional 

877 ''' 

878 

879 from pyrocko.client.wadl import load_xml 

880 

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

882 

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

884 

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

886 patch_geonet_wadl(wadl) 

887 

888 return wadl 

889 

890 

891g_wadls = {} 

892 

893 

894def cached_wadl( 

895 service, 

896 site=g_default_site, 

897 url=g_url, 

898 majorversion=1, 

899 timeout=g_timeout): 

900 

901 ''' 

902 Get self-description of a specific FDSN service. 

903 

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

905 ''' 

906 

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

908 if k not in g_wadls: 

909 try: 

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

911 

912 except Timeout: 

913 raise 

914 

915 except DownloadError: 

916 logger.info( 

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

918 % (service, site)) 

919 

920 g_wadls[k] = None 

921 

922 return g_wadls[k] 

923 

924 

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

926 

927 

928if __name__ == '__main__': 

929 import sys 

930 

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

932 

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

934 sites = get_sites() 

935 else: 

936 sites = sys.argv[1:] 

937 

938 for site in sites: 

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

940 

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

942 try: 

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

944 print(indent(str(app))) 

945 

946 except Timeout as e: 

947 logger.error(str(e)) 

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

949 

950 except util.DownloadError as e: 

951 logger.error(str(e)) 

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