Coverage for /usr/local/lib/python3.13/dist-packages/pyrocko/util.py: 75%

1305 statements  

« 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---------- 

5''' 

6Utility functions for Pyrocko. 

7 

8.. _time-handling-mode: 

9 

10High precision time handling mode 

11................................. 

12 

13Pyrocko can treat timestamps either as standard double precision (64 bit) 

14floating point values, or as high precision floats (``numpy.float128`` or 

15``numpy.float96``, whichever is available, `see NumPy Scalars 

16<https://numpy.org/doc/stable/reference/arrays.scalars.html>`_), aliased here 

17as :py:class:`~pyrocko.util.hpfloat`. High precision time stamps are required 

18when handling data with sub-millisecond precision, i.e. kHz/MHz data streams 

19and event catalogs derived from such data. 

20 

21Not all functions in Pyrocko and in programs depending on Pyrocko may work 

22correctly with high precision times. Therefore, Pyrocko's high precision time 

23handling mode has to be actively activated by user config, command line option 

24or enforced within a certain script/program. 

25 

26The default high precision time handling mode can be configured globally with 

27the user configuration variable 

28:py:gattr:`~pyrocko.config.PyrockoConfig.use_high_precision_time`. Calling the 

29function :py:func:`use_high_precision_time` overrides the default from the 

30config file. This function may be called at startup of a program/script 

31requiring a specific time handling mode. 

32 

33To create a valid time stamp for use in Pyrocko (e.g. in 

34:py:class:`~pyrocko.model.event.Event` or 

35:py:class:`~pyrocko.trace.Trace` objects), use: 

36 

37.. code-block :: python 

38 

39 import time 

40 from pyrocko import util 

41 

42 # By default using mode selected in user config, override with: 

43 # util.use_high_precision_time(True) # force high precision mode 

44 # util.use_high_precision_time(False) # force low precision mode 

45 

46 t1 = util.str_to_time('2020-08-27 10:22:00') 

47 t2 = util.str_to_time('2020-08-27 10:22:00.111222') 

48 t3 = util.to_time_float(time.time()) 

49 

50 # To get the appropriate float class, use: 

51 

52 time_float = util.get_time_float() 

53 # -> float, numpy.float128 or numpy.float96 

54 [isinstance(t, time_float) for t in [t1, t2, t3]] 

55 # -> [True, True, True] 

56 

57 # Shortcut: 

58 util.check_time_class(t1) 

59 

60Module content 

61.............. 

62 

63.. py:class:: hpfloat 

64 

65 Alias for NumPy's high precision float data type ``float128`` or 

66 ``float96``, if available. 

67 

68 On platforms lacking support for high precision floats, an attempt to 

69 create a ``hpfloat`` instance, raises :py:exc:`HPFloatUnavailable`. 

70 

71''' 

72 

73import time 

74import logging 

75import os 

76import sys 

77import re 

78import calendar 

79import math 

80import fnmatch 

81import inspect 

82import warnings 

83import weakref 

84import signal as psignal 

85try: 

86 import fcntl 

87except ImportError: 

88 fcntl = None 

89import optparse 

90import os.path as op 

91import errno 

92from collections import defaultdict 

93 

94import numpy as num 

95from scipy import signal 

96import pyrocko 

97from pyrocko import dummy_progressbar 

98 

99 

100from urllib.parse import urlencode, quote, unquote # noqa 

101from urllib.request import Request, build_opener, HTTPDigestAuthHandler # noqa 

102from urllib.request import urlopen as _urlopen # noqa 

103from urllib.error import HTTPError, URLError # noqa 

104 

105 

106try: 

107 import certifi 

108 import ssl 

109 g_ssl_context = ssl.create_default_context(cafile=certifi.where()) 

110except ImportError: 

111 g_ssl_context = None 

112 

113 

114class URLErrorSSL(URLError): 

115 

116 def __init__(self, urlerror): 

117 self.__dict__ = urlerror.__dict__.copy() 

118 

119 def __str__(self): 

120 return ( 

121 'Requesting web resource failed and the problem could be ' 

122 'related to SSL. Python standard libraries on some older ' 

123 'systems (like Ubuntu 14.04) are known to have trouble ' 

124 "with some SSL setups of today's servers: %s" 

125 % URLError.__str__(self)) 

126 

127 

128def urlopen_ssl_check(*args, **kwargs): 

129 try: 

130 return urlopen(*args, **kwargs) 

131 except URLError as e: 

132 if str(e).find('SSL') != -1: 

133 raise URLErrorSSL(e) 

134 else: 

135 raise 

136 

137 

138def urlopen(*args, **kwargs): 

139 

140 if 'context' not in kwargs and g_ssl_context is not None: 

141 kwargs['context'] = g_ssl_context 

142 

143 return _urlopen(*args, **kwargs) 

144 

145 

146try: 

147 long 

148except NameError: 

149 long = int 

150 

151 

152force_dummy_progressbar = False 

153 

154 

155try: 

156 from pyrocko import util_ext 

157except ImportError: 

158 util_ext = None 

159 

160 

161logger = logging.getLogger('pyrocko.util') 

162 

163 

164# fallbacks num_full and num_full_like are not needed anymore but 

165# kept here because downstream code may still use these. 

166try: 

167 num_full = num.full 

168except AttributeError: 

169 def num_full(shape, fill_value, dtype=None, order='C'): 

170 a = num.empty(shape, dtype=dtype, order=order) 

171 a.fill(fill_value) 

172 return a 

173 

174try: 

175 num_full_like = num.full_like 

176except AttributeError: 

177 def num_full_like(arr, fill_value, dtype=None, order='K', subok=True): 

178 a = num.empty_like(arr, dtype=dtype, order=order, subok=subok) 

179 a.fill(fill_value) 

180 return a 

181 

182 

183g_setup_logging_args = 'pyrocko', 'warning' 

184 

185 

186def setup_logging(programname='pyrocko', levelname='warning'): 

187 ''' 

188 Initialize logging. 

189 

190 :param programname: program name to be written in log 

191 :param levelname: string indicating the logging level ('debug', 'info', 

192 'warning', 'error', 'critical') 

193 

194 This function is called at startup by most pyrocko programs to set up a 

195 consistent logging format. This is simply a shortcut to a call to 

196 :py:func:`logging.basicConfig()`. 

197 ''' 

198 

199 global g_setup_logging_args 

200 g_setup_logging_args = (programname, levelname) 

201 

202 levels = {'debug': logging.DEBUG, 

203 'info': logging.INFO, 

204 'warning': logging.WARNING, 

205 'error': logging.ERROR, 

206 'critical': logging.CRITICAL} 

207 

208 logging.basicConfig( 

209 level=levels[levelname], 

210 format=programname+':%(name)-25s - %(levelname)-8s - %(message)s') 

211 

212 

213def subprocess_setup_logging_args(): 

214 ''' 

215 Get arguments from previous call to setup_logging. 

216 

217 These can be sent down to a worker process so it can setup its logging 

218 in the same way as the main process. 

219 ''' 

220 return g_setup_logging_args 

221 

222 

223g_experimental_features_used = set() 

224 

225 

226def experimental_feature_used(feature): 

227 ''' 

228 Warn once that an experimental feature is used. 

229 ''' 

230 

231 if feature not in g_experimental_features_used: 

232 warnings.warn( 

233 'Experimental feature used: %s' % feature, 

234 stacklevel=2) 

235 g_experimental_features_used.add(feature) 

236 

237 

238def data_file(fn): 

239 return os.path.join(os.path.split(__file__)[0], 'data', fn) 

240 

241 

242class DownloadError(Exception): 

243 ''' 

244 Raised when a download failed. 

245 ''' 

246 pass 

247 

248 

249class PathExists(DownloadError): 

250 ''' 

251 Raised when the download target file already exists. 

252 ''' 

253 pass 

254 

255 

256def _download(url, fpath, username=None, password=None, 

257 force=False, method='download', stats=None, 

258 status_callback=None, entries_wanted=None, 

259 recursive=False, header=None): 

260 

261 import requests 

262 from requests.auth import HTTPBasicAuth 

263 from requests.exceptions import HTTPError as req_HTTPError 

264 

265 requests.adapters.DEFAULT_RETRIES = 5 

266 urljoin = requests.compat.urljoin 

267 

268 session = requests.Session() 

269 session.header = header 

270 session.auth = None if username is None\ 

271 else HTTPBasicAuth(username, password) 

272 

273 status = { 

274 'ntotal_files': 0, 

275 'nread_files': 0, 

276 'ntotal_bytes_all_files': 0, 

277 'nread_bytes_all_files': 0, 

278 'ntotal_bytes_current_file': 0, 

279 'nread_bytes_current_file': 0, 

280 'finished': False 

281 } 

282 

283 try: 

284 url_to_size = {} 

285 

286 if callable(status_callback): 

287 status_callback(status) 

288 

289 if not recursive and url.endswith('/'): 

290 raise DownloadError( 

291 'URL: %s appears to be a directory' 

292 ' but recurvise download is False' % url) 

293 

294 if recursive and not url.endswith('/'): 

295 url += '/' 

296 

297 def parse_directory_tree(url, subdir=''): 

298 r = session.get(urljoin(url, subdir)) 

299 r.raise_for_status() 

300 

301 entries = re.findall(r'href="([a-zA-Z0-9_.-]+/?)"', r.text) 

302 

303 files = sorted(set(subdir + fn for fn in entries 

304 if not fn.endswith('/'))) 

305 

306 if entries_wanted is not None: 

307 files = [fn for fn in files 

308 if (fn in wanted for wanted in entries_wanted)] 

309 

310 status['ntotal_files'] += len(files) 

311 

312 dirs = sorted(set(subdir + dn for dn in entries 

313 if dn.endswith('/') 

314 and dn not in ('./', '../'))) 

315 

316 for dn in dirs: 

317 files.extend(parse_directory_tree( 

318 url, subdir=dn)) 

319 

320 return files 

321 

322 def get_content_length(url): 

323 if url not in url_to_size: 

324 r = session.head(url, headers={'Accept-Encoding': ''}) 

325 

326 content_length = r.headers.get('content-length', None) 

327 if content_length is None: 

328 logger.debug('Could not get HTTP header ' 

329 'Content-Length for %s' % url) 

330 

331 content_length = None 

332 

333 else: 

334 content_length = int(content_length) 

335 status['ntotal_bytes_all_files'] += content_length 

336 if callable(status_callback): 

337 status_callback(status) 

338 

339 url_to_size[url] = content_length 

340 

341 return url_to_size[url] 

342 

343 def download_file(url, fn): 

344 logger.info('starting download of %s...' % url) 

345 ensuredirs(fn) 

346 

347 fsize = get_content_length(url) 

348 status['ntotal_bytes_current_file'] = fsize 

349 status['nread_bytes_current_file'] = 0 

350 if callable(status_callback): 

351 status_callback(status) 

352 

353 r = session.get(url, stream=True, timeout=5) 

354 r.raise_for_status() 

355 

356 frx = 0 

357 fn_tmp = fn + '.%i.temp' % os.getpid() 

358 with open(fn_tmp, 'wb') as f: 

359 for d in r.iter_content(chunk_size=1024): 

360 f.write(d) 

361 frx += len(d) 

362 

363 status['nread_bytes_all_files'] += len(d) 

364 status['nread_bytes_current_file'] += len(d) 

365 if callable(status_callback): 

366 status_callback(status) 

367 

368 os.rename(fn_tmp, fn) 

369 

370 if fsize is not None and frx != fsize: 

371 logger.warning( 

372 'HTTP header Content-Length: %i bytes does not match ' 

373 'download size %i bytes' % (fsize, frx)) 

374 

375 logger.info('finished download of %s' % url) 

376 

377 status['nread_files'] += 1 

378 if callable(status_callback): 

379 status_callback(status) 

380 

381 if recursive: 

382 if op.exists(fpath) and not force: 

383 raise PathExists('path %s already exists' % fpath) 

384 

385 files = parse_directory_tree(url) 

386 

387 dsize = 0 

388 for fn in files: 

389 file_url = urljoin(url, fn) 

390 dsize += get_content_length(file_url) or 0 

391 

392 if method == 'calcsize': 

393 return dsize 

394 

395 else: 

396 for fn in files: 

397 file_url = urljoin(url, fn) 

398 download_file(file_url, op.join(fpath, fn)) 

399 

400 else: 

401 status['ntotal_files'] += 1 

402 if callable(status_callback): 

403 status_callback(status) 

404 

405 fsize = get_content_length(url) 

406 if method == 'calcsize': 

407 return fsize 

408 else: 

409 download_file(url, fpath) 

410 

411 except req_HTTPError as e: 

412 logging.warning('http error: %s' % e) 

413 raise DownloadError('could not download file(s) from: %s' % url) 

414 

415 finally: 

416 status['finished'] = True 

417 if callable(status_callback): 

418 status_callback(status) 

419 session.close() 

420 

421 

422def download_file( 

423 url, fpath, username=None, password=None, status_callback=None, 

424 **kwargs): 

425 return _download( 

426 url, fpath, username, password, 

427 recursive=False, 

428 status_callback=status_callback, 

429 **kwargs) 

430 

431 

432def download_dir( 

433 url, fpath, username=None, password=None, status_callback=None, 

434 **kwargs): 

435 

436 return _download( 

437 url, fpath, username, password, 

438 recursive=True, 

439 status_callback=status_callback, 

440 **kwargs) 

441 

442 

443class HPFloatUnavailable(Exception): 

444 ''' 

445 Raised when a high precision float type would be required but is not 

446 available. 

447 ''' 

448 pass 

449 

450 

451class dummy_hpfloat(object): 

452 def __init__(self, *args, **kwargs): 

453 raise HPFloatUnavailable( 

454 'NumPy lacks support for float128 or float96 data type on this ' 

455 'platform.') 

456 

457 

458if hasattr(num, 'float128'): 

459 hpfloat = num.float128 

460 

461elif hasattr(num, 'float96'): 

462 hpfloat = num.float96 

463 

464else: 

465 hpfloat = dummy_hpfloat 

466 

467 

468g_time_float = None 

469g_time_dtype = None 

470 

471 

472class TimeFloatSettingError(Exception): 

473 pass 

474 

475 

476def use_high_precision_time(enabled): 

477 ''' 

478 Globally force a specific time handling mode. 

479 

480 See :ref:`High precision time handling mode <time-handling-mode>`. 

481 

482 :param enabled: enable/disable use of high precision time type 

483 :type enabled: bool 

484 

485 This function should be called before handling/reading any time data. 

486 It can only be called once. 

487 

488 Special attention is required when using multiprocessing on a platform 

489 which does not use fork under the hood. In such cases, the desired setting 

490 must be set also in the subprocess. 

491 ''' 

492 _setup_high_precision_time_mode(enabled_app=enabled) 

493 

494 

495def _setup_high_precision_time_mode(enabled_app=False): 

496 global g_time_float 

497 global g_time_dtype 

498 

499 if not (g_time_float is None and g_time_dtype is None): 

500 raise TimeFloatSettingError( 

501 'Cannot set time handling mode: too late, it has already been ' 

502 'fixed by an earlier call.') 

503 

504 from pyrocko import config 

505 

506 conf = config.config() 

507 enabled_config = conf.use_high_precision_time 

508 

509 enabled_env = os.environ.get('PYROCKO_USE_HIGH_PRECISION_TIME', None) 

510 if enabled_env is not None: 

511 try: 

512 enabled_env = int(enabled_env) == 1 

513 except ValueError: 

514 raise TimeFloatSettingError( 

515 'Environment variable PYROCKO_USE_HIGH_PRECISION_TIME ' 

516 'should be set to 0 or 1.') 

517 

518 enabled = enabled_config 

519 mode_from = 'config variable `use_high_precision_time`' 

520 notify = enabled 

521 

522 if enabled_env is not None: 

523 if enabled_env != enabled: 

524 notify = True 

525 enabled = enabled_env 

526 mode_from = 'environment variable `PYROCKO_USE_HIGH_PRECISION_TIME`' 

527 

528 if enabled_app is not None: 

529 if enabled_app != enabled: 

530 notify = True 

531 enabled = enabled_app 

532 mode_from = 'application override' 

533 

534 logger.debug(''' 

535Pyrocko high precision time mode selection (latter override earlier): 

536 config: %s 

537 env: %s 

538 app: %s 

539 -> enabled: %s'''.lstrip() % ( 

540 enabled_config, enabled_env, enabled_app, enabled)) 

541 

542 if notify: 

543 logger.info('Pyrocko high precision time mode %s by %s.' % ( 

544 'activated' if enabled else 'deactivated', 

545 mode_from)) 

546 

547 if enabled: 

548 g_time_float = hpfloat 

549 g_time_dtype = hpfloat 

550 else: 

551 g_time_float = float 

552 g_time_dtype = num.float64 

553 

554 

555def get_time_float(): 

556 ''' 

557 Get the effective float class for timestamps. 

558 

559 See :ref:`High precision time handling mode <time-handling-mode>`. 

560 

561 :returns: :py:class:`float` or :py:class:`hpfloat`, depending on the 

562 current time handling mode 

563 ''' 

564 global g_time_float # noqa 

565 

566 if g_time_float is None: 

567 _setup_high_precision_time_mode() 

568 

569 return g_time_float 

570 

571 

572def get_time_dtype(): 

573 ''' 

574 Get effective NumPy float class to handle timestamps. 

575 

576 See :ref:`High precision time handling mode <time-handling-mode>`. 

577 ''' 

578 

579 global g_time_dtype # noqa 

580 

581 if g_time_dtype is None: 

582 _setup_high_precision_time_mode() 

583 

584 return g_time_dtype 

585 

586 

587def to_time_float(t): 

588 ''' 

589 Convert float to valid time stamp in the current time handling mode. 

590 

591 See :ref:`High precision time handling mode <time-handling-mode>`. 

592 ''' 

593 return get_time_float()(t) 

594 

595 

596class TimestampTypeError(ValueError): 

597 pass 

598 

599 

600def check_time_class(t, error='raise'): 

601 ''' 

602 Type-check variable against current time handling mode. 

603 

604 See :ref:`High precision time handling mode <time-handling-mode>`. 

605 ''' 

606 

607 if t == 0.0: 

608 return 

609 

610 if not isinstance(t, get_time_float()): 

611 message = ( 

612 'Timestamp %g is of type %s but should be of type %s with ' 

613 "Pyrocko's currently selected time handling mode.\n\n" 

614 'See https://pyrocko.org/docs/current/library/reference/util.html' 

615 '#high-precision-time-handling-mode' % ( 

616 t, type(t), get_time_float())) 

617 

618 if error == 'raise': 

619 raise TimestampTypeError(message) 

620 elif error == 'warn': 

621 logger.warning(message) 

622 else: 

623 assert False 

624 

625 

626def iter_windows( 

627 tmin=None, 

628 tmax=None, 

629 tinc=None, 

630 tpad=0.0, 

631 snap_window=False, 

632 tmin_content=None, 

633 tmax_content=None): 

634 

635 from pyrocko.squirrel import Batch 

636 

637 if snap_window and tinc is not None: 

638 tmin = tmin if tmin is not None else tmin_content 

639 tmax = tmax if tmax is not None else tmax_content 

640 tmin = math.floor(tmin / tinc) * tinc 

641 tmax = math.ceil(tmax / tinc) * tinc 

642 else: 

643 tmin = tmin if tmin is not None else ( 

644 tmin_content + tpad if tmin_content is not None else None) 

645 tmax = tmax if tmax is not None else ( 

646 tmax_content - tpad if tmax_content is not None else None) 

647 

648 if None in (tmin, tmax): 

649 return 

650 

651 if tinc is None: 

652 tinc = tmax - tmin 

653 nwin = 1 

654 elif tinc == 0.0: 

655 nwin = 1 

656 else: 

657 eps = 1e-6 

658 nwin = max(1, int((tmax - tmin) / tinc - eps) + 1) 

659 

660 for iwin in range(nwin): 

661 wmin, wmax = tmin+iwin*tinc, min(tmin+(iwin+1)*tinc, tmax) 

662 

663 yield Batch( 

664 tmin=wmin, 

665 tmax=wmax, 

666 tpad=tpad, 

667 i=iwin, 

668 n=nwin, 

669 igroup=0, 

670 ngroups=1, 

671 traces=[]) 

672 

673 

674class Stopwatch(object): 

675 ''' 

676 Simple stopwatch to measure elapsed wall clock time. 

677 

678 Usage:: 

679 

680 s = Stopwatch() 

681 time.sleep(1) 

682 print s() 

683 time.sleep(1) 

684 print s() 

685 ''' 

686 

687 def __init__(self): 

688 self.start = time.time() 

689 

690 def __call__(self): 

691 return time.time() - self.start 

692 

693 

694def wrap(text, line_length=80): 

695 ''' 

696 Paragraph and list-aware wrapping of text. 

697 ''' 

698 

699 text = text.strip('\n') 

700 at_lineend = re.compile(r' *\n') 

701 at_para = re.compile(r'((^|(\n\s*)?\n)(\s+[*] )|\n\s*\n)') 

702 

703 paragraphs = at_para.split(text)[::5] 

704 listindents = at_para.split(text)[4::5] 

705 newlist = at_para.split(text)[3::5] 

706 

707 listindents[0:0] = [None] 

708 listindents.append(True) 

709 newlist.append(None) 

710 

711 det_indent = re.compile(r'^ *') 

712 

713 outlines = [] 

714 for ip, p in enumerate(paragraphs): 

715 if not p: 

716 continue 

717 

718 if listindents[ip] is None: 

719 _indent = det_indent.findall(p)[0] 

720 findent = _indent 

721 else: 

722 findent = listindents[ip] 

723 _indent = ' ' * len(findent) 

724 

725 ll = line_length - len(_indent) 

726 llf = ll 

727 

728 oldlines = [s.strip() for s in at_lineend.split(p.rstrip())] 

729 p1 = ' '.join(oldlines) 

730 possible = re.compile(r'(^.{1,%i}|.{1,%i})( |$)' % (llf, ll)) 

731 for imatch, match in enumerate(possible.finditer(p1)): 

732 parout = match.group(1) 

733 if imatch == 0: 

734 outlines.append(findent + parout) 

735 else: 

736 outlines.append(_indent + parout) 

737 

738 if ip != len(paragraphs)-1 and ( 

739 listindents[ip] is None 

740 or newlist[ip] is not None 

741 or listindents[ip+1] is None): 

742 

743 outlines.append('') 

744 

745 return outlines 

746 

747 

748def ewrap(lines, width=80, indent=''): 

749 lines = list(lines) 

750 if not lines: 

751 return '' 

752 fwidth = max(len(s) for s in lines) 

753 nx = max(1, (80-len(indent)) // (fwidth+1)) 

754 i = 0 

755 rows = [] 

756 while i < len(lines): 

757 rows.append(indent + ' '.join(x.ljust(fwidth) for x in lines[i:i+nx])) 

758 i += nx 

759 

760 return '\n'.join(rows) 

761 

762 

763class BetterHelpFormatter(optparse.IndentedHelpFormatter): 

764 

765 def __init__(self, *args, **kwargs): 

766 optparse.IndentedHelpFormatter.__init__(self, *args, **kwargs) 

767 

768 def format_option(self, option): 

769 ''' 

770 From IndentedHelpFormatter but using a different wrap method. 

771 ''' 

772 

773 help_text_position = 4 + self.current_indent 

774 help_text_width = self.width - help_text_position 

775 

776 opts = self.option_strings[option] 

777 opts = '%*s%s' % (self.current_indent, '', opts) 

778 if option.help: 

779 help_text = self.expand_default(option) 

780 

781 if self.help_position + len(help_text) + 1 <= self.width: 

782 lines = [ 

783 '', 

784 '%-*s %s' % (self.help_position, opts, help_text), 

785 ''] 

786 else: 

787 lines = [''] 

788 lines.append(opts) 

789 lines.append('') 

790 if option.help: 

791 help_lines = wrap(help_text, help_text_width) 

792 lines.extend(['%*s%s' % (help_text_position, '', line) 

793 for line in help_lines]) 

794 lines.append('') 

795 

796 return '\n'.join(lines) 

797 

798 def format_description(self, description): 

799 if not description: 

800 return '' 

801 

802 if self.current_indent == 0: 

803 lines = [] 

804 else: 

805 lines = [''] 

806 

807 lines.extend(wrap(description, self.width - self.current_indent)) 

808 if self.current_indent == 0: 

809 lines.append('\n') 

810 

811 return '\n'.join( 

812 ['%*s%s' % (self.current_indent, '', line) for line in lines]) 

813 

814 

815class ProgressBar: 

816 def __init__(self, label, n): 

817 from pyrocko import progress 

818 self._context = progress.view() 

819 self._context.__enter__() 

820 self._task = progress.task(label, n) 

821 

822 def update(self, i): 

823 self._task.update(i) 

824 

825 def finish(self): 

826 self._task.done() 

827 if self._context: 

828 self._context.__exit__() 

829 self._context = None 

830 

831 

832def progressbar(label, maxval): 

833 if force_dummy_progressbar: 

834 return dummy_progressbar.ProgressBar(maxval=maxval).start() 

835 

836 return ProgressBar(label, maxval) 

837 

838 

839def progress_beg(label): 

840 ''' 

841 Notify user that an operation has started. 

842 

843 :param label: name of the operation 

844 

845 To be used in conjuction with :py:func:`progress_end`. 

846 ''' 

847 

848 sys.stderr.write(label) 

849 sys.stderr.flush() 

850 

851 

852def progress_end(label=''): 

853 ''' 

854 Notify user that an operation has ended. 

855 

856 :param label: name of the operation 

857 

858 To be used in conjuction with :py:func:`progress_beg`. 

859 ''' 

860 

861 sys.stderr.write(' done. %s\n' % label) 

862 sys.stderr.flush() 

863 

864 

865class ArangeError(ValueError): 

866 ''' 

867 Raised by :py:func:`arange2` for inconsistent range specifications. 

868 ''' 

869 pass 

870 

871 

872def arange2(start, stop, step, dtype=float, epsilon=1e-6, error='raise'): 

873 ''' 

874 Return evenly spaced numbers over a specified interval. 

875 

876 Like :py:func:`numpy.arange` but returning floating point numbers by 

877 default and with defined behaviour when stepsize is inconsistent with 

878 interval bounds. It is considered inconsistent if the difference between 

879 the closest multiple of ``step`` and ``stop`` is larger than ``epsilon * 

880 step``. Inconsistencies are handled according to the ``error`` parameter. 

881 If it is set to ``'raise'`` an exception of type :py:exc:`ArangeError` is 

882 raised. If it is set to ``'round'``, ``'floor'``, or ``'ceil'``, ``stop`` 

883 is silently changed to the closest, the next smaller, or next larger 

884 multiple of ``step``, respectively. 

885 ''' 

886 

887 assert error in ('raise', 'round', 'floor', 'ceil') 

888 

889 start = dtype(start) 

890 stop = dtype(stop) 

891 step = dtype(step) 

892 

893 rnd = {'floor': math.floor, 'ceil': math.ceil}.get(error, round) 

894 

895 n = int(rnd((stop - start) / step)) + 1 

896 stop_check = start + (n-1) * step 

897 

898 if error == 'raise' and abs(stop_check - stop) > step * epsilon: 

899 raise ArangeError( 

900 'inconsistent range specification: start=%g, stop=%g, step=%g' 

901 % (start, stop, step)) 

902 

903 x = num.arange(n, dtype=dtype) 

904 x *= step 

905 x += start 

906 return x 

907 

908 

909def polylinefit(x, y, n_or_xnodes): 

910 ''' 

911 Fit piece-wise linear function to data. 

912 

913 :param x,y: arrays with coordinates of data 

914 :param n_or_xnodes: int, number of segments or x coordinates of polyline 

915 

916 :returns: `(xnodes, ynodes, rms_error)` arrays with coordinates of 

917 polyline, root-mean-square error 

918 ''' 

919 

920 x = num.asarray(x) 

921 y = num.asarray(y) 

922 

923 if isinstance(n_or_xnodes, int): 

924 n = n_or_xnodes 

925 xmin = x.min() 

926 xmax = x.max() 

927 xnodes = num.linspace(xmin, xmax, n+1) 

928 else: 

929 xnodes = num.asarray(n_or_xnodes) 

930 n = xnodes.size - 1 

931 

932 assert len(x) == len(y) 

933 assert n > 0 

934 

935 ndata = len(x) 

936 a = num.zeros((ndata+(n-1), n*2)) 

937 for i in range(n): 

938 xmin_block = xnodes[i] 

939 xmax_block = xnodes[i+1] 

940 if i == n-1: # don't loose last point 

941 indices = num.where( 

942 num.logical_and(xmin_block <= x, x <= xmax_block))[0] 

943 else: 

944 indices = num.where( 

945 num.logical_and(xmin_block <= x, x < xmax_block))[0] 

946 

947 a[indices, i*2] = x[indices] 

948 a[indices, i*2+1] = 1.0 

949 

950 w = float(ndata)*100. 

951 if i < n-1: 

952 a[ndata+i, i*2] = xmax_block*w 

953 a[ndata+i, i*2+1] = 1.0*w 

954 a[ndata+i, i*2+2] = -xmax_block*w 

955 a[ndata+i, i*2+3] = -1.0*w 

956 

957 d = num.concatenate((y, num.zeros(n-1))) 

958 model = num.linalg.lstsq(a, d, rcond=-1)[0].reshape((n, 2)) 

959 

960 ynodes = num.zeros(n+1) 

961 ynodes[:n] = model[:, 0]*xnodes[:n] + model[:, 1] 

962 ynodes[1:] += model[:, 0]*xnodes[1:] + model[:, 1] 

963 ynodes[1:n] *= 0.5 

964 

965 rms_error = num.sqrt(num.mean((num.interp(x, xnodes, ynodes) - y)**2)) 

966 

967 return xnodes, ynodes, rms_error 

968 

969 

970def plf_integrate_piecewise(x_edges, x, y): 

971 ''' 

972 Calculate definite integral of piece-wise linear function on intervals. 

973 

974 Use trapezoidal rule to calculate definite integral of a piece-wise linear 

975 function for a series of consecutive intervals. ``x_edges`` and ``x`` must 

976 be sorted. 

977 

978 :param x_edges: array with edges of the intervals 

979 :param x,y: arrays with coordinates of piece-wise linear function's 

980 control points 

981 ''' 

982 

983 x_all = num.concatenate((x, x_edges)) 

984 ii = num.argsort(x_all) 

985 y_edges = num.interp(x_edges, x, y) 

986 y_all = num.concatenate((y, y_edges)) 

987 xs = x_all[ii] 

988 ys = y_all[ii] 

989 y_all[ii[1:]] = num.cumsum((xs[1:] - xs[:-1]) * 0.5 * (ys[1:] + ys[:-1])) 

990 return num.diff(y_all[-len(y_edges):]) 

991 

992 

993def diff_fd_1d_4o(dt, data): 

994 ''' 

995 Approximate first derivative of an array (forth order, central FD). 

996 

997 :param dt: sampling interval 

998 :param data: NumPy array with data samples 

999 

1000 :returns: NumPy array with same shape as input 

1001 

1002 Interior points are approximated to fourth order, edge points to first 

1003 order right- or left-sided respectively, points next to edge to second 

1004 order central. 

1005 ''' 

1006 import scipy.signal 

1007 

1008 ddata = num.empty_like(data, dtype=float) 

1009 

1010 if data.size >= 5: 

1011 ddata[2:-2] = scipy.signal.lfilter( 

1012 [-1., +8., 0., -8., 1.], [1.], data)[4:] / (12.*dt) 

1013 

1014 if data.size >= 3: 

1015 ddata[1] = (data[2] - data[0]) / (2. * dt) 

1016 ddata[-2] = (data[-1] - data[-3]) / (2. * dt) 

1017 

1018 if data.size >= 2: 

1019 ddata[0] = (data[1] - data[0]) / dt 

1020 ddata[-1] = (data[-1] - data[-2]) / dt 

1021 

1022 if data.size == 1: 

1023 ddata[0] = 0.0 

1024 

1025 return ddata 

1026 

1027 

1028def diff_fd_1d_2o(dt, data): 

1029 ''' 

1030 Approximate first derivative of an array (second order, central FD). 

1031 

1032 :param dt: sampling interval 

1033 :param data: NumPy array with data samples 

1034 

1035 :returns: NumPy array with same shape as input 

1036 

1037 Interior points are approximated to second order, edge points to first 

1038 order right- or left-sided respectively. 

1039 

1040 Uses :py:func:`numpy.gradient`. 

1041 ''' 

1042 

1043 return num.gradient(data, dt) 

1044 

1045 

1046def diff_fd_2d_4o(dt, data): 

1047 ''' 

1048 Approximate second derivative of an array (forth order, central FD). 

1049 

1050 :param dt: sampling interval 

1051 :param data: NumPy array with data samples 

1052 

1053 :returns: NumPy array with same shape as input 

1054 

1055 Interior points are approximated to fourth order, next-to-edge points to 

1056 second order, edge points repeated. 

1057 ''' 

1058 import scipy.signal 

1059 

1060 ddata = num.empty_like(data, dtype=float) 

1061 

1062 if data.size >= 5: 

1063 ddata[2:-2] = scipy.signal.lfilter( 

1064 [-1., +16., -30., +16., -1.], [1.], data)[4:] / (12.*dt**2) 

1065 

1066 if data.size >= 3: 

1067 ddata[:2] = (data[2] - 2.0 * data[1] + data[0]) / dt**2 

1068 ddata[-2:] = (data[-1] - 2.0 * data[-2] + data[-3]) / dt**2 

1069 

1070 if data.size < 3: 

1071 ddata[:] = 0.0 

1072 

1073 return ddata 

1074 

1075 

1076def diff_fd_2d_2o(dt, data): 

1077 ''' 

1078 Approximate second derivative of an array (second order, central FD). 

1079 

1080 :param dt: sampling interval 

1081 :param data: NumPy array with data samples 

1082 

1083 :returns: NumPy array with same shape as input 

1084 

1085 Interior points are approximated to second order, edge points repeated. 

1086 ''' 

1087 import scipy.signal 

1088 

1089 ddata = num.empty_like(data, dtype=float) 

1090 

1091 if data.size >= 3: 

1092 ddata[1:-1] = scipy.signal.lfilter( 

1093 [1., -2., 1.], [1.], data)[2:] / (dt**2) 

1094 

1095 ddata[0] = ddata[1] 

1096 ddata[-1] = ddata[-2] 

1097 

1098 if data.size < 3: 

1099 ddata[:] = 0.0 

1100 

1101 return ddata 

1102 

1103 

1104def diff_fd(n, order, dt, data): 

1105 ''' 

1106 Approximate 1st or 2nd derivative of an array. 

1107 

1108 :param n: 1 for first derivative, 2 for second 

1109 :param order: order of the approximation 2 and 4 are supported 

1110 :param dt: sampling interval 

1111 :param data: NumPy array with data samples 

1112 

1113 :returns: NumPy array with same shape as input 

1114 

1115 This is a frontend to the functions :py:func:`diff_fd_1d_2o`, 

1116 :py:func:`diff_fd_1d_4o`, :py:func:`diff_fd_2d_2o`, and 

1117 :py:func:`diff_fd_2d_4o`. 

1118 

1119 Raises :py:exc:`ValueError` for unsupported `n` or `order`. 

1120 ''' 

1121 

1122 funcs = { 

1123 1: {2: diff_fd_1d_2o, 4: diff_fd_1d_4o}, 

1124 2: {2: diff_fd_2d_2o, 4: diff_fd_2d_4o}} 

1125 

1126 try: 

1127 funcs_n = funcs[n] 

1128 except KeyError: 

1129 raise ValueError( 

1130 'pyrocko.util.diff_fd: ' 

1131 'Only 1st and 2sd derivatives are supported.') 

1132 

1133 try: 

1134 func = funcs_n[order] 

1135 except KeyError: 

1136 raise ValueError( 

1137 'pyrocko.util.diff_fd: ' 

1138 'Order %i is not supported for %s derivative. Supported: %s' % ( 

1139 order, ['', '1st', '2nd'][n], 

1140 ', '.join('%i' % order for order in sorted(funcs_n.keys())))) 

1141 

1142 return func(dt, data) 

1143 

1144 

1145class GlobalVars(object): 

1146 reuse_store = dict() 

1147 decitab_nmax = 0 

1148 decitab = {} 

1149 decimate_fir_coeffs = {} 

1150 decimate_fir_remez_coeffs = {} 

1151 decimate_iir_coeffs = {} 

1152 re_frac = None 

1153 

1154 

1155def decimate_coeffs(q, n=None, ftype='iir'): 

1156 

1157 q = int(q) 

1158 

1159 if n is None: 

1160 if ftype == 'fir': 

1161 n = 30 

1162 elif ftype == 'fir-remez': 

1163 n = 45*q 

1164 else: 

1165 n = 8 

1166 

1167 if ftype == 'fir': 

1168 coeffs = GlobalVars.decimate_fir_coeffs 

1169 if (n, 1./q) not in coeffs: 

1170 coeffs[n, 1./q] = signal.firwin(n+1, .75/q, window='hamming') 

1171 

1172 b = coeffs[n, 1./q] 

1173 return b, [1.], n 

1174 

1175 elif ftype == 'fir-remez': 

1176 coeffs = GlobalVars.decimate_fir_remez_coeffs 

1177 if (n, 1./q) not in coeffs: 

1178 coeffs[n, 1./q] = signal.remez( 

1179 n+1, (0., .75/q, 1./q, 1.), 

1180 (1., 0.), fs=2, weight=(1, 50)) 

1181 b = coeffs[n, 1./q] 

1182 return b, [1.], n 

1183 

1184 else: 

1185 coeffs = GlobalVars.decimate_iir_coeffs 

1186 if (n, 0.05, 0.8/q) not in coeffs: 

1187 coeffs[n, 0.05, 0.8/q] = signal.cheby1(n, 0.05, 0.8/q) 

1188 

1189 b, a = coeffs[n, 0.05, 0.8/q] 

1190 return b, a, n 

1191 

1192 

1193def decimate(x, q, n=None, ftype='iir', zi=None, ioff=0): 

1194 ''' 

1195 Downsample the signal x by an integer factor q, using an order n filter 

1196 

1197 By default, an order 8 Chebyshev type I filter is used or a 30 point FIR 

1198 filter with hamming window if ftype is 'fir'. 

1199 

1200 :param x: the signal to be downsampled (1D :class:`numpy.ndarray`) 

1201 :param q: the downsampling factor 

1202 :param n: order of the filter (1 less than the length of the filter for a 

1203 `fir` filter) 

1204 :param ftype: type of the filter; can be `iir`, `fir` or `fir-remez` 

1205 

1206 :returns: the downsampled signal (1D :class:`numpy.ndarray`) 

1207 

1208 ''' 

1209 

1210 b, a, n = decimate_coeffs(q, n, ftype) 

1211 

1212 if zi is None or zi is True: 

1213 zi_ = num.zeros(max(len(a), len(b))-1, dtype=float) 

1214 else: 

1215 zi_ = zi 

1216 

1217 y, zf = signal.lfilter(b, a, x, zi=zi_) 

1218 

1219 if zi is not None: 

1220 return y[n//2+ioff::q].copy(), zf 

1221 else: 

1222 return y[n//2+ioff::q].copy() 

1223 

1224 

1225class UnavailableDecimation(Exception): 

1226 ''' 

1227 Exception raised by :py:func:`decitab` for unavailable decimation factors. 

1228 ''' 

1229 

1230 pass 

1231 

1232 

1233def gcd(a, b, epsilon=1e-7): 

1234 ''' 

1235 Greatest common divisor. 

1236 ''' 

1237 

1238 while b > epsilon*a: 

1239 a, b = b, a % b 

1240 

1241 return a 

1242 

1243 

1244def lcm(a, b): 

1245 ''' 

1246 Least common multiple. 

1247 ''' 

1248 

1249 return a*b // gcd(a, b) 

1250 

1251 

1252def mk_decitab(nmax=100): 

1253 ''' 

1254 Make table with decimation sequences. 

1255 

1256 Decimation from one sampling rate to a lower one is achieved by a 

1257 successive application of :py:func:`decimate` with small integer 

1258 downsampling factors (because using large downsampling factors can make the 

1259 decimation unstable or slow). This function sets up a table with downsample 

1260 sequences for factors up to ``nmax``. 

1261 ''' 

1262 

1263 tab = GlobalVars.decitab 

1264 for i in range(1, 10): 

1265 for j in range(1, i+1): 

1266 for k in range(1, j+1): 

1267 for l_ in range(1, k+1): 

1268 for m in range(1, l_+1): 

1269 p = i*j*k*l_*m 

1270 if p > nmax: 

1271 break 

1272 if p not in tab: 

1273 tab[p] = (i, j, k, l_, m) 

1274 if i*j*k*l_ > nmax: 

1275 break 

1276 if i*j*k > nmax: 

1277 break 

1278 if i*j > nmax: 

1279 break 

1280 if i > nmax: 

1281 break 

1282 

1283 GlobalVars.decitab_nmax = nmax 

1284 

1285 

1286def zfmt(n): 

1287 return '%%0%ii' % (int(math.log10(n - 1)) + 1) 

1288 

1289 

1290def _year_to_time(year): 

1291 tt = (year, 1, 1, 0, 0, 0) 

1292 return to_time_float(calendar.timegm(tt)) 

1293 

1294 

1295def _working_year(year): 

1296 try: 

1297 tt = (year, 1, 1, 0, 0, 0) 

1298 t = calendar.timegm(tt) 

1299 tt2_ = time.gmtime(t) 

1300 tt2 = tuple(tt2_)[:6] 

1301 if tt != tt2: 

1302 return False 

1303 

1304 s = '%i-01-01 00:00:00' % year 

1305 s2 = time.strftime('%Y-%m-%d %H:%M:%S', tt2_) 

1306 if s != s2: 

1307 return False 

1308 

1309 t3 = str_to_time(s2, format='%Y-%m-%d %H:%M:%S') 

1310 s3 = time_to_str(t3, format='%Y-%m-%d %H:%M:%S') 

1311 if s3 != s2: 

1312 return False 

1313 

1314 except Exception: 

1315 return False 

1316 

1317 return True 

1318 

1319 

1320def working_system_time_range(year_min_lim=None, year_max_lim=None): 

1321 ''' 

1322 Check time range supported by the systems's time conversion functions. 

1323 

1324 Returns system time stamps of start of year of first/last fully supported 

1325 year span. If this is before 1900 or after 2100, return first/last century 

1326 which is fully supported. 

1327 

1328 :returns: ``(tmin, tmax, year_min, year_max)`` 

1329 ''' 

1330 

1331 year0 = 2000 

1332 year_min = year0 

1333 year_max = year0 

1334 

1335 itests = list(range(101)) 

1336 for i in range(19): 

1337 itests.append(200 + i*100) 

1338 

1339 for i in itests: 

1340 year = year0 - i 

1341 if year_min_lim is not None and year < year_min_lim: 

1342 year_min = year_min_lim 

1343 break 

1344 elif not _working_year(year): 

1345 break 

1346 else: 

1347 year_min = year 

1348 

1349 for i in itests: 

1350 year = year0 + i 

1351 if year_max_lim is not None and year > year_max_lim: 

1352 year_max = year_max_lim 

1353 break 

1354 elif not _working_year(year + 1): 

1355 break 

1356 else: 

1357 year_max = year 

1358 

1359 return ( 

1360 _year_to_time(year_min), 

1361 _year_to_time(year_max), 

1362 year_min, year_max) 

1363 

1364 

1365g_working_system_time_range = None 

1366 

1367 

1368def get_working_system_time_range(): 

1369 ''' 

1370 Caching variant of :py:func:`working_system_time_range`. 

1371 ''' 

1372 

1373 global g_working_system_time_range 

1374 if g_working_system_time_range is None: 

1375 g_working_system_time_range = working_system_time_range() 

1376 

1377 return g_working_system_time_range 

1378 

1379 

1380def is_working_time(t): 

1381 tmin, tmax, _, _ = get_working_system_time_range() 

1382 return tmin <= t <= tmax 

1383 

1384 

1385def julian_day_of_year(timestamp): 

1386 ''' 

1387 Get the day number after the 1st of January of year in ``timestamp``. 

1388 

1389 :returns: day number as int 

1390 ''' 

1391 

1392 return time.gmtime(int(timestamp)).tm_yday 

1393 

1394 

1395def hour_start(timestamp): 

1396 ''' 

1397 Get beginning of hour for any point in time. 

1398 

1399 :param timestamp: time instant as system timestamp (in seconds) 

1400 

1401 :returns: instant of hour start as system timestamp 

1402 ''' 

1403 

1404 tt = time.gmtime(int(timestamp)) 

1405 tts = tt[0:4] + (0, 0) 

1406 return to_time_float(calendar.timegm(tts)) 

1407 

1408 

1409def day_start(timestamp): 

1410 ''' 

1411 Get beginning of day for any point in time. 

1412 

1413 :param timestamp: time instant as system timestamp (in seconds) 

1414 

1415 :returns: instant of day start as system timestamp 

1416 ''' 

1417 

1418 tt = time.gmtime(int(timestamp)) 

1419 tts = tt[0:3] + (0, 0, 0) 

1420 return to_time_float(calendar.timegm(tts)) 

1421 

1422 

1423def month_start(timestamp): 

1424 ''' 

1425 Get beginning of month for any point in time. 

1426 

1427 :param timestamp: time instant as system timestamp (in seconds) 

1428 

1429 :returns: instant of month start as system timestamp 

1430 ''' 

1431 

1432 tt = time.gmtime(int(timestamp)) 

1433 tts = tt[0:2] + (1, 0, 0, 0) 

1434 return to_time_float(calendar.timegm(tts)) 

1435 

1436 

1437def year_start(timestamp): 

1438 ''' 

1439 Get beginning of year for any point in time. 

1440 

1441 :param timestamp: time instant as system timestamp (in seconds) 

1442 

1443 :returns: instant of year start as system timestamp 

1444 ''' 

1445 

1446 tt = time.gmtime(int(timestamp)) 

1447 tts = tt[0:1] + (1, 1, 0, 0, 0) 

1448 return to_time_float(calendar.timegm(tts)) 

1449 

1450 

1451def iter_days(tmin, tmax): 

1452 ''' 

1453 Yields begin and end of days until given time span is covered. 

1454 

1455 :param tmin,tmax: input time span 

1456 

1457 :yields: tuples with (begin, end) of days as system timestamps 

1458 ''' 

1459 

1460 t = day_start(tmin) 

1461 while t < tmax: 

1462 tend = day_start(t + 26*60*60) 

1463 yield t, tend 

1464 t = tend 

1465 

1466 

1467def iter_months(tmin, tmax): 

1468 ''' 

1469 Yields begin and end of months until given time span is covered. 

1470 

1471 :param tmin,tmax: input time span 

1472 

1473 :yields: tuples with (begin, end) of months as system timestamps 

1474 ''' 

1475 

1476 t = month_start(tmin) 

1477 while t < tmax: 

1478 tend = month_start(t + 24*60*60*33) 

1479 yield t, tend 

1480 t = tend 

1481 

1482 

1483def iter_years(tmin, tmax): 

1484 ''' 

1485 Yields begin and end of years until given time span is covered. 

1486 

1487 :param tmin,tmax: input time span 

1488 

1489 :yields: tuples with (begin, end) of years as system timestamps 

1490 ''' 

1491 

1492 t = year_start(tmin) 

1493 while t < tmax: 

1494 tend = year_start(t + 24*60*60*369) 

1495 yield t, tend 

1496 t = tend 

1497 

1498 

1499def yesterday(): 

1500 return day_start(time.time() - 24*60*60) 

1501 

1502 

1503def today(): 

1504 return day_start(time.time()) 

1505 

1506 

1507def tomorrow(): 

1508 return day_start(time.time() + 24*60*60) 

1509 

1510 

1511def decitab(n): 

1512 ''' 

1513 Get integer decimation sequence for given downampling factor. 

1514 

1515 :param n: target decimation factor 

1516 

1517 :returns: tuple with downsampling sequence 

1518 ''' 

1519 

1520 if n > GlobalVars.decitab_nmax: 

1521 mk_decitab(n*2) 

1522 if n not in GlobalVars.decitab: 

1523 raise UnavailableDecimation('ratio = %g' % n) 

1524 return GlobalVars.decitab[n] 

1525 

1526 

1527def ctimegm(s, format='%Y-%m-%d %H:%M:%S'): 

1528 ''' 

1529 Convert string representing UTC time to system time. 

1530 

1531 :param s: string to be interpreted 

1532 :param format: format string passed to :py:func:`time.strptime` 

1533 

1534 :returns: system time stamp 

1535 

1536 Interpretes string with format ``'%Y-%m-%d %H:%M:%S'``, using strptime. 

1537 

1538 .. note:: 

1539 This function is to be replaced by :py:func:`str_to_time`. 

1540 ''' 

1541 

1542 return calendar.timegm(time.strptime(s, format)) 

1543 

1544 

1545def gmctime(t, format='%Y-%m-%d %H:%M:%S'): 

1546 ''' 

1547 Get string representation from system time, UTC. 

1548 

1549 Produces string with format ``'%Y-%m-%d %H:%M:%S'``, using strftime. 

1550 

1551 .. note:: 

1552 This function is to be repaced by :py:func:`time_to_str`. 

1553 ''' 

1554 

1555 return time.strftime(format, time.gmtime(t)) 

1556 

1557 

1558def gmctime_v(t, format='%a, %d %b %Y %H:%M:%S'): 

1559 ''' 

1560 Get string representation from system time, UTC. Same as 

1561 :py:func:`gmctime` but with a more verbose default format. 

1562 

1563 .. note:: 

1564 This function is to be replaced by :py:func:`time_to_str`. 

1565 ''' 

1566 

1567 return time.strftime(format, time.gmtime(t)) 

1568 

1569 

1570def gmctime_fn(t, format='%Y-%m-%d_%H-%M-%S'): 

1571 ''' 

1572 Get string representation from system time, UTC. Same as 

1573 :py:func:`gmctime` but with a default usable in filenames. 

1574 

1575 .. note:: 

1576 This function is to be replaced by :py:func:`time_to_str`. 

1577 ''' 

1578 

1579 return time.strftime(format, time.gmtime(t)) 

1580 

1581 

1582class TimeStrError(Exception): 

1583 ''' 

1584 Raised for invalid time strings. 

1585 ''' 

1586 pass 

1587 

1588 

1589class FractionalSecondsMissing(TimeStrError): 

1590 ''' 

1591 Exception raised by :py:func:`str_to_time` when the given string lacks 

1592 fractional seconds. 

1593 ''' 

1594 

1595 pass 

1596 

1597 

1598class FractionalSecondsWrongNumberOfDigits(TimeStrError): 

1599 ''' 

1600 Exception raised by :py:func:`str_to_time` when the given string has an 

1601 incorrect number of digits in the fractional seconds part. 

1602 ''' 

1603 

1604 pass 

1605 

1606 

1607def _endswith_n(s, endings): 

1608 for ix, x in enumerate(endings): 

1609 if s.endswith(x): 

1610 return ix 

1611 return -1 

1612 

1613 

1614def str_to_time(s, format='%Y-%m-%d %H:%M:%S.OPTFRAC'): 

1615 ''' 

1616 Convert string representing UTC time to floating point system time. 

1617 

1618 :param s: string representing UTC time 

1619 :param format: time string format 

1620 :returns: system time stamp as floating point value 

1621 

1622 Uses the semantics of :py:func:`time.strptime` but allows for fractional 

1623 seconds. If the format ends with ``'.FRAC'``, anything after a dot is 

1624 interpreted as fractional seconds. If the format ends with ``'.OPTFRAC'``, 

1625 the fractional part, including the dot is made optional. The latter has the 

1626 consequence, that the time strings and the format may not contain any other 

1627 dots. If the format ends with ``'.xFRAC'`` where x is 1, 2, or 3, it is 

1628 ensured, that exactly that number of digits are present in the fractional 

1629 seconds. 

1630 ''' 

1631 

1632 time_float = get_time_float() 

1633 

1634 if util_ext is not None: 

1635 try: 

1636 t, tfrac = util_ext.stt(s, format) 

1637 except util_ext.UtilExtError as e: 

1638 raise TimeStrError( 

1639 '%s, string=%s, format=%s' % (str(e), s, format)) 

1640 

1641 return time_float(t)+tfrac 

1642 

1643 fracsec = 0. 

1644 fixed_endings = '.FRAC', '.1FRAC', '.2FRAC', '.3FRAC' 

1645 

1646 iend = _endswith_n(format, fixed_endings) 

1647 if iend != -1: 

1648 dotpos = s.rfind('.') 

1649 if dotpos == -1: 

1650 raise FractionalSecondsMissing( 

1651 'string=%s, format=%s' % (s, format)) 

1652 

1653 if iend > 0 and iend != (len(s)-dotpos-1): 

1654 raise FractionalSecondsWrongNumberOfDigits( 

1655 'string=%s, format=%s' % (s, format)) 

1656 

1657 format = format[:-len(fixed_endings[iend])] 

1658 fracsec = float(s[dotpos:]) 

1659 s = s[:dotpos] 

1660 

1661 elif format.endswith('.OPTFRAC'): 

1662 dotpos = s.rfind('.') 

1663 format = format[:-8] 

1664 if dotpos != -1 and len(s[dotpos:]) > 1: 

1665 fracsec = float(s[dotpos:]) 

1666 

1667 if dotpos != -1: 

1668 s = s[:dotpos] 

1669 

1670 try: 

1671 return time_float(calendar.timegm(time.strptime(s, format))) \ 

1672 + fracsec 

1673 except ValueError as e: 

1674 raise TimeStrError('%s, string=%s, format=%s' % (str(e), s, format)) 

1675 

1676 

1677stt = str_to_time 

1678 

1679 

1680def str_to_time_fillup(s): 

1681 ''' 

1682 Default :py:func:`str_to_time` with filling in of missing values. 

1683 

1684 Allows e.g. `'2010-01-01 00:00:00'` as `'2010-01-01 00:00'`, 

1685 `'2010-01-01 00'`, ..., or `'2010'`. 

1686 ''' 

1687 s = s.strip().replace('T', ' ').replace('_', ' ') 

1688 

1689 if s == 'now': 

1690 return time.time() 

1691 elif s == 'yesterday': 

1692 return yesterday() 

1693 elif s == 'today': 

1694 return today() 

1695 elif s == 'tomorrow': 

1696 return tomorrow() 

1697 

1698 if len(s) in (4, 7, 10, 13, 16): 

1699 s += '0000-01-01 00:00:00'[len(s):] 

1700 return str_to_time(s) 

1701 

1702 

1703def time_to_str(t, format='%Y-%m-%d %H:%M:%S.3FRAC'): 

1704 ''' 

1705 Get string representation for floating point system time. 

1706 

1707 :param t: floating point system time 

1708 :param format: time string format 

1709 :returns: string representing UTC time 

1710 

1711 Uses the semantics of :py:func:`time.strftime` but additionally allows for 

1712 fractional seconds. If ``format`` contains ``'.xFRAC'``, where ``x`` is a 

1713 digit between 1 and 9, this is replaced with the fractional part of ``t`` 

1714 with ``x`` digits precision. 

1715 ''' 

1716 

1717 if pyrocko.grumpy > 0: 

1718 check_time_class(t, 'warn' if pyrocko.grumpy == 1 else 'raise') 

1719 

1720 if isinstance(format, int): 

1721 if format > 0: 

1722 format = '%Y-%m-%d %H:%M:%S.' + '%iFRAC' % format 

1723 else: 

1724 format = '%Y-%m-%d %H:%M:%S' 

1725 

1726 if util_ext is not None: 

1727 t0 = num.floor(t) 

1728 try: 

1729 return util_ext.tts(int(t0), float(t - t0), format) 

1730 except util_ext.UtilExtError as e: 

1731 raise TimeStrError( 

1732 '%s, timestamp=%f, format=%s' % (str(e), t, format)) 

1733 

1734 if not GlobalVars.re_frac: 

1735 GlobalVars.re_frac = re.compile(r'\.[1-9]FRAC') 

1736 GlobalVars.frac_formats = dict( 

1737 [('.%sFRAC' % x, '%.'+x+'f') for x in '123456789']) 

1738 

1739 ts = float(num.floor(t)) 

1740 tfrac = t-ts 

1741 

1742 m = GlobalVars.re_frac.search(format) 

1743 if m: 

1744 sfrac = (GlobalVars.frac_formats[m.group(0)] % tfrac) 

1745 if sfrac[0] == '1': 

1746 ts += 1. 

1747 

1748 format, nsub = GlobalVars.re_frac.subn(sfrac[1:], format, 1) 

1749 

1750 return time.strftime(format, time.gmtime(ts)) 

1751 

1752 

1753tts = time_to_str 

1754_abbr_weekday = 'Mon Tue Wed Thu Fri Sat Sun'.split() 

1755_abbr_month = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split() 

1756 

1757 

1758def mystrftime(fmt=None, tt=None, milliseconds=0): 

1759 # Needed by Snuffler for the time axis. In other cases `time_to_str` 

1760 # should be used. 

1761 

1762 if fmt is None: 

1763 fmt = '%Y-%m-%d %H:%M:%S .%r' 

1764 

1765 # Get these two locale independent, needed by Snuffler. 

1766 # Setting the locale seems to have no effect. 

1767 fmt = fmt.replace('%a', _abbr_weekday[tt.tm_wday]) 

1768 fmt = fmt.replace('%b', _abbr_month[tt.tm_mon-1]) 

1769 

1770 fmt = fmt.replace('%r', '%03i' % int(round(milliseconds))) 

1771 fmt = fmt.replace('%u', '%06i' % int(round(milliseconds*1000))) 

1772 fmt = fmt.replace('%n', '%09i' % int(round(milliseconds*1000000))) 

1773 return time.strftime(fmt, tt) 

1774 

1775 

1776def gmtime_x(timestamp): 

1777 etimestamp = float(num.floor(timestamp)) 

1778 tt = time.gmtime(etimestamp) 

1779 ms = (timestamp-etimestamp)*1000 

1780 return tt, ms 

1781 

1782 

1783def plural_s(n): 

1784 if not isinstance(n, int): 

1785 n = len(n) 

1786 

1787 return 's' if n != 1 else '' 

1788 

1789 

1790def ensuredirs(dst): 

1791 ''' 

1792 Create all intermediate path components for a target path. 

1793 

1794 :param dst: target path 

1795 

1796 The leaf part of the target path is not created (use :py:func:`ensuredir` 

1797 if a the target path is a directory to be created). 

1798 ''' 

1799 

1800 d, x = os.path.split(dst.rstrip(os.sep)) 

1801 dirs = [] 

1802 while d and not os.path.exists(d): 

1803 dirs.append(d) 

1804 d, x = os.path.split(d) 

1805 

1806 dirs.reverse() 

1807 

1808 for d in dirs: 

1809 try: 

1810 os.mkdir(d) 

1811 except OSError as e: 

1812 if not e.errno == errno.EEXIST: 

1813 raise 

1814 

1815 

1816def ensuredir(dst): 

1817 ''' 

1818 Create directory and all intermediate path components to it as needed. 

1819 

1820 :param dst: directory name 

1821 

1822 Nothing is done if the given target already exists. 

1823 ''' 

1824 

1825 if os.path.exists(dst): 

1826 return 

1827 

1828 dst.rstrip(os.sep) 

1829 

1830 ensuredirs(dst) 

1831 try: 

1832 os.mkdir(dst) 

1833 except OSError as e: 

1834 if not e.errno == errno.EEXIST: 

1835 raise 

1836 

1837 

1838def reuse(x): 

1839 ''' 

1840 Get unique instance of an object. 

1841 

1842 :param x: hashable object 

1843 :returns: reference to x or an equivalent object 

1844 

1845 Cache object ``x`` in a global dict for reuse, or if x already 

1846 is in that dict, return a reference to it. 

1847 ''' 

1848 

1849 grs = GlobalVars.reuse_store 

1850 if x not in grs: 

1851 grs[x] = x 

1852 return grs[x] 

1853 

1854 

1855def deuse(x): 

1856 grs = GlobalVars.reuse_store 

1857 if x in grs: 

1858 del grs[x] 

1859 

1860 

1861class Anon(object): 

1862 ''' 

1863 Dict-to-object utility. 

1864 

1865 Any given arguments are stored as attributes. 

1866 

1867 Example:: 

1868 

1869 a = Anon(x=1, y=2) 

1870 print a.x, a.y 

1871 ''' 

1872 

1873 def __init__(self, **dict): 

1874 for k in dict: 

1875 self.__dict__[k] = dict[k] 

1876 

1877 

1878def iter_select_files( 

1879 paths, 

1880 include=None, 

1881 exclude=None, 

1882 selector=None, 

1883 show_progress=True, 

1884 pass_through=None): 

1885 

1886 ''' 

1887 Recursively select files (generator variant). 

1888 

1889 See :py:func:`select_files`. 

1890 ''' 

1891 

1892 if show_progress: 

1893 progress_beg('selecting files...') 

1894 

1895 ngood = 0 

1896 if include is not None: 

1897 rinclude = re.compile(include) 

1898 

1899 def check_include(path): 

1900 m = rinclude.search(path) 

1901 if not m: 

1902 return False 

1903 

1904 if selector is None: 

1905 return True 

1906 

1907 infos = Anon(**m.groupdict()) 

1908 return selector(infos) 

1909 else: 

1910 check_include = None 

1911 

1912 if exclude is not None: 

1913 rexclude = re.compile(exclude) 

1914 

1915 def check_exclude(path): 

1916 return not bool(rexclude.search(path)) 

1917 else: 

1918 check_exclude = None 

1919 

1920 if check_include and check_exclude: 

1921 

1922 def check(path): 

1923 return check_include(path) and check_exclude(path) 

1924 

1925 elif check_include: 

1926 check = check_include 

1927 

1928 elif check_exclude: 

1929 check = check_exclude 

1930 

1931 else: 

1932 check = None 

1933 

1934 if isinstance(paths, str): 

1935 paths = [paths] 

1936 

1937 for path in paths: 

1938 if pass_through and pass_through(path): 

1939 if check is None or check(path): 

1940 yield path 

1941 

1942 elif os.path.isdir(path): 

1943 for (dirpath, dirnames, filenames) in os.walk(path): 

1944 dirnames.sort() 

1945 filenames.sort() 

1946 for filename in filenames: 

1947 path = op.join(dirpath, filename) 

1948 if check is None or check(path): 

1949 yield os.path.abspath(path) 

1950 ngood += 1 

1951 else: 

1952 if check is None or check(path): 

1953 yield os.path.abspath(path) 

1954 ngood += 1 

1955 

1956 if show_progress: 

1957 progress_end('%i file%s selected.' % (ngood, plural_s(ngood))) 

1958 

1959 

1960def select_files( 

1961 paths, include=None, exclude=None, selector=None, show_progress=True, 

1962 regex=None): 

1963 

1964 ''' 

1965 Recursively select files. 

1966 

1967 :param paths: entry path names 

1968 :param include: pattern for conditional inclusion 

1969 :param exclude: pattern for conditional exclusion 

1970 :param selector: callback for conditional inclusion 

1971 :param show_progress: if True, indicate start and stop of processing 

1972 :param regex: alias for ``include`` (backwards compatibility) 

1973 :returns: list of path names 

1974 

1975 Recursively finds all files under given entry points ``paths``. If 

1976 parameter ``include`` is a regular expression, only files with matching 

1977 path names are included. If additionally parameter ``selector`` is given a 

1978 callback function, only files for which the callback returns ``True`` are 

1979 included. The callback should take a single argument. The callback is 

1980 called with a single argument, an object, having as attributes, any named 

1981 groups given in ``include``. 

1982 

1983 Examples 

1984 

1985 To find all files ending in ``'.mseed'`` or ``'.msd'``:: 

1986 

1987 select_files(paths, 

1988 include=r'\\.(mseed|msd)$') 

1989 

1990 To find all files ending with ``'$Year.$DayOfYear'``, having set 2009 for 

1991 the year:: 

1992 

1993 select_files(paths, 

1994 include=r'(?P<year>\\d\\d\\d\\d)\\.(?P<doy>\\d\\d\\d)$', 

1995 selector=(lambda x: int(x.year) == 2009)) 

1996 ''' 

1997 if regex is not None: 

1998 assert include is None 

1999 include = regex 

2000 

2001 return list(iter_select_files( 

2002 paths, include, exclude, selector, show_progress)) 

2003 

2004 

2005def base36encode(number, alphabet='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'): 

2006 ''' 

2007 Convert positive integer to a base36 string. 

2008 ''' 

2009 

2010 if not isinstance(number, (int, long)): 

2011 raise TypeError('number must be an integer') 

2012 if number < 0: 

2013 raise ValueError('number must be positive') 

2014 

2015 # Special case for small numbers 

2016 if number < 36: 

2017 return alphabet[number] 

2018 

2019 base36 = '' 

2020 while number != 0: 

2021 number, i = divmod(number, 36) 

2022 base36 = alphabet[i] + base36 

2023 

2024 return base36 

2025 

2026 

2027def base36decode(number): 

2028 ''' 

2029 Decode base36 endcoded positive integer. 

2030 ''' 

2031 

2032 return int(number, 36) 

2033 

2034 

2035class UnpackError(Exception): 

2036 ''' 

2037 Exception raised when :py:func:`unpack_fixed` encounters an error. 

2038 ''' 

2039 

2040 pass 

2041 

2042 

2043ruler = ''.join(['%-10i' % i for i in range(8)]) \ 

2044 + '\n' + '0123456789' * 8 + '\n' 

2045 

2046 

2047def unpack_fixed(format, line, *callargs): 

2048 ''' 

2049 Unpack fixed format string, as produced by many fortran codes. 

2050 

2051 :param format: format specification 

2052 :param line: string to be processed 

2053 :param callargs: callbacks for callback fields in the format 

2054 

2055 The format is described by a string of comma-separated fields. Each field 

2056 is defined by a character for the field type followed by the field width. A 

2057 questionmark may be appended to the field description to allow the argument 

2058 to be optional (The data string is then allowed to be filled with blanks 

2059 and ``None`` is returned in this case). 

2060 

2061 The following field types are available: 

2062 

2063 ==== ================================================================ 

2064 Type Description 

2065 ==== ================================================================ 

2066 A string (full field width is extracted) 

2067 a string (whitespace at the beginning and the end is removed) 

2068 i integer value 

2069 f floating point value 

2070 @ special type, a callback must be given for the conversion 

2071 x special field type to skip parts of the string 

2072 ==== ================================================================ 

2073 ''' 

2074 

2075 ipos = 0 

2076 values = [] 

2077 icall = 0 

2078 for form in format.split(','): 

2079 form = form.strip() 

2080 optional = form[-1] == '?' 

2081 form = form.rstrip('?') 

2082 typ = form[0] 

2083 ln = int(form[1:]) 

2084 s = line[ipos:ipos+ln] 

2085 cast = { 

2086 'x': None, 

2087 'A': str, 

2088 'a': lambda x: x.strip(), 

2089 'i': int, 

2090 'f': float, 

2091 '@': 'extra'}[typ] 

2092 

2093 if cast == 'extra': 

2094 cast = callargs[icall] 

2095 icall += 1 

2096 

2097 if cast is not None: 

2098 if optional and s.strip() == '': 

2099 values.append(None) 

2100 else: 

2101 try: 

2102 values.append(cast(s)) 

2103 except Exception: 

2104 mark = [' '] * 80 

2105 mark[ipos:ipos+ln] = ['^'] * ln 

2106 mark = ''.join(mark) 

2107 raise UnpackError( 

2108 'Invalid cast to type "%s" at position [%i:%i] of ' 

2109 'line: \n%s%s\n%s' % ( 

2110 typ, ipos, ipos+ln, ruler, line.rstrip(), mark)) 

2111 

2112 ipos += ln 

2113 

2114 return values 

2115 

2116 

2117_pattern_cache = {} 

2118 

2119 

2120def _nslc_pattern(pattern): 

2121 if pattern not in _pattern_cache: 

2122 rpattern = re.compile(fnmatch.translate(pattern), re.I) 

2123 _pattern_cache[pattern] = rpattern 

2124 else: 

2125 rpattern = _pattern_cache[pattern] 

2126 

2127 return rpattern 

2128 

2129 

2130def match_nslc(patterns, nslc): 

2131 ''' 

2132 Match network-station-location-channel code against pattern or list of 

2133 patterns. 

2134 

2135 :param patterns: pattern or list of patterns 

2136 :param nslc: tuple with (network, station, location, channel) as strings 

2137 

2138 :returns: ``True`` if the pattern matches or if any of the given patterns 

2139 match; or ``False``. 

2140 

2141 The patterns may contain shell-style wildcards: \\*, ?, [seq], [!seq]. 

2142 

2143 Example:: 

2144 

2145 match_nslc('*.HAM3.*.BH?', ('GR', 'HAM3', '', 'BHZ')) # -> True 

2146 ''' 

2147 

2148 if isinstance(patterns, str): 

2149 patterns = [patterns] 

2150 

2151 if not isinstance(nslc, str): 

2152 s = '.'.join(nslc) 

2153 else: 

2154 s = nslc 

2155 

2156 for pattern in patterns: 

2157 if _nslc_pattern(pattern).match(s): 

2158 return True 

2159 

2160 return False 

2161 

2162 

2163def match_nslcs(patterns, nslcs): 

2164 ''' 

2165 Get network-station-location-channel codes that match given pattern or any 

2166 of several given patterns. 

2167 

2168 :param patterns: pattern or list of patterns 

2169 :param nslcs: list of (network, station, location, channel) tuples 

2170 

2171 See also :py:func:`match_nslc` 

2172 ''' 

2173 

2174 matching = [] 

2175 for nslc in nslcs: 

2176 if match_nslc(patterns, nslc): 

2177 matching.append(nslc) 

2178 

2179 return matching 

2180 

2181 

2182def glob_filter(patterns, names): 

2183 ''' 

2184 Select names matching any of the given patterns. 

2185 ''' 

2186 

2187 if not patterns: 

2188 return names 

2189 

2190 rpattern = re.compile(r'|'.join( 

2191 fnmatch.translate(pattern) for pattern in patterns)) 

2192 

2193 return [name for name in names if rpattern.match(name)] 

2194 

2195 

2196class Timeout(Exception): 

2197 pass 

2198 

2199 

2200def create_lockfile(fn, timeout=None, timewarn=10.): 

2201 t0 = time.time() 

2202 

2203 while True: 

2204 try: 

2205 f = os.open(fn, os.O_CREAT | os.O_WRONLY | os.O_EXCL) 

2206 os.close(f) 

2207 return 

2208 

2209 except OSError as e: 

2210 if e.errno in (errno.EEXIST, 13): # 13 occurs on windows 

2211 pass # retry 

2212 else: 

2213 raise 

2214 

2215 tnow = time.time() 

2216 

2217 if timeout is not None and tnow - t0 > timeout: 

2218 raise Timeout( 

2219 'Timeout (%gs) occured while waiting to get exclusive ' 

2220 'access to: %s' % (timeout, fn)) 

2221 

2222 if timewarn is not None and tnow - t0 > timewarn: 

2223 logger.warning( 

2224 'Waiting since %gs to get exclusive access to: %s' % ( 

2225 timewarn, fn)) 

2226 

2227 timewarn *= 2 

2228 

2229 time.sleep(0.01) 

2230 

2231 

2232def delete_lockfile(fn): 

2233 os.unlink(fn) 

2234 

2235 

2236class Lockfile(Exception): 

2237 

2238 def __init__(self, path, timeout=5, timewarn=10.): 

2239 self._path = path 

2240 self._timeout = timeout 

2241 self._timewarn = timewarn 

2242 

2243 def __enter__(self): 

2244 create_lockfile( 

2245 self._path, timeout=self._timeout, timewarn=self._timewarn) 

2246 return None 

2247 

2248 def __exit__(self, type, value, traceback): 

2249 delete_lockfile(self._path) 

2250 

2251 

2252class SoleError(Exception): 

2253 ''' 

2254 Exception raised by objects of type :py:class:`Sole`, when an concurrent 

2255 instance is running. 

2256 ''' 

2257 

2258 pass 

2259 

2260 

2261class Sole(object): 

2262 ''' 

2263 Use POSIX advisory file locking to ensure that only a single instance of a 

2264 program is running. 

2265 

2266 :param pid_path: path to lockfile to be used 

2267 

2268 Usage:: 

2269 

2270 from pyrocko.util import Sole, SoleError, setup_logging 

2271 import os 

2272 

2273 setup_logging('my_program') 

2274 

2275 pid_path = os.path.join(os.environ['HOME'], '.my_program_lock') 

2276 try: 

2277 sole = Sole(pid_path) 

2278 

2279 except SoleError, e: 

2280 logger.fatal( str(e) ) 

2281 sys.exit(1) 

2282 ''' 

2283 

2284 def __init__(self, pid_path): 

2285 self._pid_path = pid_path 

2286 self._other_running = False 

2287 ensuredirs(self._pid_path) 

2288 self._lockfile = None 

2289 self._os = os 

2290 

2291 if not fcntl: 

2292 raise SoleError( 

2293 'Python standard library module "fcntl" not available on ' 

2294 'this platform.') 

2295 

2296 self._fcntl = fcntl 

2297 

2298 try: 

2299 self._lockfile = os.open(self._pid_path, os.O_CREAT | os.O_WRONLY) 

2300 except Exception: 

2301 raise SoleError( 

2302 'Cannot open lockfile (path = %s)' % self._pid_path) 

2303 

2304 try: 

2305 fcntl.lockf(self._lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) 

2306 

2307 except IOError: 

2308 self._other_running = True 

2309 try: 

2310 f = open(self._pid_path, 'r') 

2311 pid = f.read().strip() 

2312 f.close() 

2313 except Exception: 

2314 pid = '?' 

2315 

2316 raise SoleError('Other instance is running (pid = %s)' % pid) 

2317 

2318 try: 

2319 os.ftruncate(self._lockfile, 0) 

2320 os.write(self._lockfile, '%i\n' % os.getpid()) 

2321 os.fsync(self._lockfile) 

2322 

2323 except Exception: 

2324 # the pid is only stored for user information, so this is allowed 

2325 # to fail 

2326 pass 

2327 

2328 def __del__(self): 

2329 if not self._other_running: 

2330 if self._lockfile is not None: 

2331 self._fcntl.lockf(self._lockfile, self._fcntl.LOCK_UN) 

2332 self._os.close(self._lockfile) 

2333 try: 

2334 self._os.unlink(self._pid_path) 

2335 except Exception: 

2336 pass 

2337 

2338 

2339re_escapequotes = re.compile(r"(['\\])") 

2340 

2341 

2342def escapequotes(s): 

2343 return re_escapequotes.sub(r'\\\1', s) 

2344 

2345 

2346class TableWriter(object): 

2347 ''' 

2348 Write table of space separated values to a file. 

2349 

2350 :param f: file like object 

2351 

2352 Strings containing spaces are quoted on output. 

2353 ''' 

2354 

2355 def __init__(self, f): 

2356 self._f = f 

2357 

2358 def writerow(self, row, minfieldwidths=None): 

2359 

2360 ''' 

2361 Write one row of values to underlying file. 

2362 

2363 :param row: iterable of values 

2364 :param minfieldwidths: minimum field widths for the values 

2365 

2366 Each value in in ``row`` is converted to a string and optionally padded 

2367 with blanks. The resulting strings are output separated with blanks. If 

2368 any values given are strings and if they contain whitespace, they are 

2369 quoted with single quotes, and any internal single quotes are 

2370 backslash-escaped. 

2371 ''' 

2372 

2373 out = [] 

2374 

2375 for i, x in enumerate(row): 

2376 w = 0 

2377 if minfieldwidths and i < len(minfieldwidths): 

2378 w = minfieldwidths[i] 

2379 

2380 if isinstance(x, str): 

2381 if re.search(r"\s|'", x): 

2382 x = "'%s'" % escapequotes(x) 

2383 

2384 x = x.ljust(w) 

2385 else: 

2386 x = str(x).rjust(w) 

2387 

2388 out.append(x) 

2389 

2390 self._f.write(' '.join(out).rstrip() + '\n') 

2391 

2392 

2393class TableReader(object): 

2394 

2395 ''' 

2396 Read table of space separated values from a file. 

2397 

2398 :param f: file-like object 

2399 

2400 This uses Pythons shlex module to tokenize lines. Should deal correctly 

2401 with quoted strings. 

2402 ''' 

2403 

2404 def __init__(self, f): 

2405 self._f = f 

2406 self.eof = False 

2407 

2408 def readrow(self): 

2409 ''' 

2410 Read one row from the underlying file, tokenize it with shlex. 

2411 

2412 :returns: tokenized line as a list of strings. 

2413 ''' 

2414 

2415 line = self._f.readline() 

2416 if not line: 

2417 self.eof = True 

2418 return [] 

2419 line.strip() 

2420 if line.startswith('#'): 

2421 return [] 

2422 

2423 return qsplit(line) 

2424 

2425 

2426def gform(number, significant_digits=3): 

2427 ''' 

2428 Pretty print floating point numbers. 

2429 

2430 Align floating point numbers at the decimal dot. 

2431 

2432 :: 

2433 

2434 | -d.dde+xxx| 

2435 | -d.dde+xx | 

2436 |-ddd. | 

2437 | -dd.d | 

2438 | -d.dd | 

2439 | -0.ddd | 

2440 | -0.0ddd | 

2441 | -0.00ddd | 

2442 | -d.dde-xx | 

2443 | -d.dde-xxx| 

2444 | nan| 

2445 

2446 

2447 The formatted string has length ``significant_digits * 2 + 6``. 

2448 ''' 

2449 

2450 no_exp_range = (pow(10., -1), 

2451 pow(10., significant_digits)) 

2452 width = significant_digits+significant_digits-1+1+1+5 

2453 

2454 if (no_exp_range[0] <= abs(number) < no_exp_range[1]) or number == 0.: 

2455 s = ('%#.*g' % (significant_digits, number)).rstrip('0') 

2456 else: 

2457 s = '%.*E' % (significant_digits-1, number) 

2458 s = (' '*(-s.find('.')+(significant_digits+1))+s).ljust(width) 

2459 if s.strip().lower() == 'nan': 

2460 s = 'nan'.rjust(width) 

2461 return s 

2462 

2463 

2464def human_bytesize(value): 

2465 

2466 exts = 'Bytes kB MB GB TB PB EB ZB YB'.split() 

2467 

2468 if value == 1: 

2469 return '1 Byte' 

2470 

2471 for i, ext in enumerate(exts): 

2472 x = float(value) / 1000**i 

2473 if round(x) < 10. and not value < 1000: 

2474 return '%.1f %s' % (x, ext) 

2475 if round(x) < 1000.: 

2476 return '%.0f %s' % (x, ext) 

2477 

2478 return '%i Bytes' % value 

2479 

2480 

2481def human_intsize(value): 

2482 

2483 exts = 'x k M G T P E Z Y'.split() 

2484 

2485 for i, ext in enumerate(exts): 

2486 x = float(value) / 1000**i 

2487 if round(x) < 10. and not value < 1000: 

2488 return '%.1f%s' % (x, ' ' + ext if ext != 'x' else '') 

2489 if round(x) < 1000.: 

2490 return '%.0f%s' % (x, ' ' + ext if ext != 'x' else '') 

2491 

2492 return '%i' % value 

2493 

2494 

2495re_compatibility = re.compile( 

2496 r'!pyrocko\.(trace|gf\.(meta|seismosizer)|fomosto\.' + 

2497 r'(dummy|poel|qseis|qssp))\.' 

2498) 

2499 

2500 

2501def pf_is_old(fn): 

2502 oldstyle = False 

2503 with open(fn, 'r') as f: 

2504 for line in f: 

2505 if re_compatibility.search(line): 

2506 oldstyle = True 

2507 

2508 return oldstyle 

2509 

2510 

2511def pf_upgrade(fn): 

2512 need = pf_is_old(fn) 

2513 if need: 

2514 fn_temp = fn + '.temp' 

2515 

2516 with open(fn, 'r') as fin: 

2517 with open(fn_temp, 'w') as fout: 

2518 for line in fin: 

2519 line = re_compatibility.sub('!pf.', line) 

2520 fout.write(line) 

2521 

2522 os.rename(fn_temp, fn) 

2523 

2524 return need 

2525 

2526 

2527def get_tzdata_path(): 

2528 for path in [ 

2529 '/usr/share/zoneinfo/right/UTC', 

2530 '/usr/share/zoneinfo/Etc/UTC']: 

2531 

2532 if os.path.exists(path): 

2533 return path 

2534 

2535 raise LeapSecondsError( 

2536 'Could not find system-provided leap time zone data file.') 

2537 

2538 

2539def read_leap_seconds(tzfile=None): 

2540 ''' 

2541 Extract leap second information from tzdata. 

2542 

2543 Based on example at http://stackoverflow.com/questions/19332902/\ 

2544 extract-historic-leap-seconds-from-tzdata 

2545 

2546 See also 'man 5 tzfile'. 

2547 ''' 

2548 

2549 if tzfile is None: 

2550 tzfile = get_tzdata_path() 

2551 

2552 from struct import unpack, calcsize 

2553 out = [] 

2554 with open(tzfile, 'rb') as f: 

2555 # read header 

2556 fmt = '>4s c 15x 6l' 

2557 (magic, format, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, 

2558 typecnt, charcnt) = unpack(fmt, f.read(calcsize(fmt))) 

2559 assert magic == 'TZif'.encode('US-ASCII'), 'Not a timezone file' 

2560 

2561 # skip over some uninteresting data 

2562 fmt = '>%(timecnt)dl %(timecnt)dB %(ttinfo)s %(charcnt)ds' % dict( 

2563 timecnt=timecnt, ttinfo='lBB'*typecnt, charcnt=charcnt) 

2564 f.read(calcsize(fmt)) 

2565 

2566 # read leap-seconds 

2567 fmt = '>2l' 

2568 for i in range(leapcnt): 

2569 tleap, nleap = unpack(fmt, f.read(calcsize(fmt))) 

2570 out.append((tleap-nleap+1, nleap)) 

2571 

2572 return out 

2573 

2574 

2575class LeapSecondsError(Exception): 

2576 pass 

2577 

2578 

2579class LeapSecondsOutdated(LeapSecondsError): 

2580 pass 

2581 

2582 

2583class InvalidLeapSecondsFile(LeapSecondsOutdated): 

2584 pass 

2585 

2586 

2587def parse_leap_seconds_list(fn): 

2588 data = [] 

2589 texpires = None 

2590 try: 

2591 t0 = int(round(str_to_time('1900-01-01 00:00:00'))) 

2592 except TimeStrError: 

2593 t0 = int(round(str_to_time('1970-01-01 00:00:00'))) - 2208988800 

2594 

2595 tnow = int(round(time.time())) 

2596 

2597 if not op.exists(fn): 

2598 raise LeapSecondsOutdated('no leap seconds file found') 

2599 

2600 try: 

2601 with open(fn, 'rb') as f: 

2602 for line in f: 

2603 if line.strip().startswith(b'<!DOCTYPE'): 

2604 raise InvalidLeapSecondsFile('invalid leap seconds file') 

2605 

2606 if line.startswith(b'#@'): 

2607 texpires = int(line.split()[1]) + t0 

2608 elif line.startswith(b'#') or len(line) < 5: 

2609 pass 

2610 else: 

2611 toks = line.split() 

2612 t = int(toks[0]) + t0 

2613 nleap = int(toks[1]) - 10 

2614 data.append((t, nleap)) 

2615 

2616 except IOError: 

2617 raise LeapSecondsError('cannot read leap seconds file %s' % fn) 

2618 

2619 if texpires is None or tnow > texpires: 

2620 raise LeapSecondsOutdated('leap seconds list is outdated') 

2621 

2622 return data 

2623 

2624 

2625def read_leap_seconds2(): 

2626 from pyrocko import config 

2627 conf = config.config() 

2628 fn = conf.leapseconds_path 

2629 url = conf.leapseconds_url 

2630 # check for outdated default URL 

2631 if url == 'http://www.ietf.org/timezones/data/leap-seconds.list': 

2632 url = 'https://www.ietf.org/timezones/data/leap-seconds.list' 

2633 logger.info( 

2634 'Leap seconds default URL is now: %s\nUsing new default.' % url) 

2635 

2636 if url == 'https://www.ietf.org/timezones/data/leap-seconds.list': 

2637 url = 'https://hpiers.obspm.fr/iers/bul/bulc/ntp/leap-seconds.list' 

2638 logger.info( 

2639 'Leap seconds default URL is now: %s\nUsing new default.' % url) 

2640 

2641 for i in range(3): 

2642 try: 

2643 return parse_leap_seconds_list(fn) 

2644 

2645 except LeapSecondsOutdated: 

2646 try: 

2647 logger.info('updating leap seconds list...') 

2648 download_file(url, fn) 

2649 

2650 except Exception as e: 

2651 raise LeapSecondsError( 

2652 'cannot download leap seconds list from %s to %s (%s)' 

2653 % (url, fn, e)) 

2654 

2655 raise LeapSecondsError('Could not retrieve/read leap seconds file.') 

2656 

2657 

2658def gps_utc_offset(t_utc): 

2659 ''' 

2660 Time offset t_gps - t_utc for a given t_utc. 

2661 ''' 

2662 ls = read_leap_seconds2() 

2663 i = 0 

2664 if t_utc < ls[0][0]: 

2665 return ls[0][1] - 1 - 9 

2666 

2667 while i < len(ls) - 1: 

2668 if ls[i][0] <= t_utc and t_utc < ls[i+1][0]: 

2669 return ls[i][1] - 9 

2670 i += 1 

2671 

2672 return ls[-1][1] - 9 

2673 

2674 

2675def utc_gps_offset(t_gps): 

2676 ''' 

2677 Time offset t_utc - t_gps for a given t_gps. 

2678 ''' 

2679 ls = read_leap_seconds2() 

2680 

2681 if t_gps < ls[0][0] + ls[0][1] - 9: 

2682 return - (ls[0][1] - 1 - 9) 

2683 

2684 i = 0 

2685 while i < len(ls) - 1: 

2686 if ls[i][0] + ls[i][1] - 9 <= t_gps \ 

2687 and t_gps < ls[i+1][0] + ls[i+1][1] - 9: 

2688 return - (ls[i][1] - 9) 

2689 i += 1 

2690 

2691 return - (ls[-1][1] - 9) 

2692 

2693 

2694def make_iload_family(iload_fh, doc_fmt='FMT', doc_yielded_objects='FMT'): 

2695 import itertools 

2696 import glob 

2697 from pyrocko.io.io_common import FileLoadError 

2698 

2699 def iload_filename(filename, **kwargs): 

2700 try: 

2701 with open(filename, 'rb') as f: 

2702 for cr in iload_fh(f, **kwargs): 

2703 yield cr 

2704 

2705 except FileLoadError as e: 

2706 e.set_context('filename', filename) 

2707 raise 

2708 

2709 def iload_dirname(dirname, **kwargs): 

2710 for entry in os.listdir(dirname): 

2711 fpath = op.join(dirname, entry) 

2712 if op.isfile(fpath): 

2713 for cr in iload_filename(fpath, **kwargs): 

2714 yield cr 

2715 

2716 def iload_glob(pattern, **kwargs): 

2717 

2718 for fn in glob.iglob(pattern): 

2719 for cr in iload_filename(fn, **kwargs): 

2720 yield cr 

2721 

2722 def iload(source, **kwargs): 

2723 if isinstance(source, str): 

2724 if op.isdir(source): 

2725 return iload_dirname(source, **kwargs) 

2726 elif op.isfile(source): 

2727 return iload_filename(source, **kwargs) 

2728 else: 

2729 return iload_glob(source, **kwargs) 

2730 

2731 elif hasattr(source, 'read'): 

2732 return iload_fh(source, **kwargs) 

2733 else: 

2734 return itertools.chain.from_iterable( 

2735 iload(subsource, **kwargs) for subsource in source) 

2736 

2737 iload_filename.__doc__ = ''' 

2738 Read %s information from named file. 

2739 ''' % doc_fmt 

2740 

2741 iload_dirname.__doc__ = ''' 

2742 Read %s information from directory of %s files. 

2743 ''' % (doc_fmt, doc_fmt) 

2744 

2745 iload_glob.__doc__ = ''' 

2746 Read %s information from files matching a glob pattern. 

2747 ''' % doc_fmt 

2748 

2749 iload.__doc__ = ''' 

2750 Load %s information from given source(s) 

2751 

2752 The ``source`` can be specified as the name of a %s file, the name of a 

2753 directory containing %s files, a glob pattern of %s files, an open 

2754 filehandle or an iterator yielding any of the forementioned sources. 

2755 

2756 This function behaves as a generator yielding %s objects. 

2757 ''' % (doc_fmt, doc_fmt, doc_fmt, doc_fmt, doc_yielded_objects) 

2758 

2759 for f in iload_filename, iload_dirname, iload_glob, iload: 

2760 f.__module__ = iload_fh.__module__ 

2761 

2762 return iload_filename, iload_dirname, iload_glob, iload 

2763 

2764 

2765class Inconsistency(Exception): 

2766 pass 

2767 

2768 

2769def consistency_check(list_of_tuples, message='values differ:'): 

2770 ''' 

2771 Check for inconsistencies. 

2772 

2773 Given a list of tuples, check that all tuple elements except for first one 

2774 match. E.g. ``[('STA.N', 55.3, 103.2), ('STA.E', 55.3, 103.2)]`` would be 

2775 valid because the coordinates at the two channels are the same. 

2776 ''' 

2777 

2778 if len(list_of_tuples) >= 2: 

2779 if any(t[1:] != list_of_tuples[0][1:] for t in list_of_tuples[1:]): 

2780 raise Inconsistency('%s\n' % message + '\n'.join( 

2781 ' %s: %s' % (t[0], ', '.join(str(x) for x in t[1:])) 

2782 for t in list_of_tuples)) 

2783 

2784 

2785class defaultzerodict(dict): 

2786 def __missing__(self, k): 

2787 return 0 

2788 

2789 

2790def mostfrequent(x): 

2791 c = defaultzerodict() 

2792 for e in x: 

2793 c[e] += 1 

2794 

2795 return sorted(list(c.keys()), key=lambda k: c[k])[-1] 

2796 

2797 

2798def consistency_merge(list_of_tuples, 

2799 message='values differ:', 

2800 error='raise', 

2801 merge=mostfrequent): 

2802 

2803 assert error in ('raise', 'warn', 'ignore') 

2804 

2805 if len(list_of_tuples) == 0: 

2806 raise Exception('cannot merge empty sequence') 

2807 

2808 try: 

2809 consistency_check(list_of_tuples, message) 

2810 return list_of_tuples[0][1:] 

2811 except Inconsistency as e: 

2812 if error == 'raise': 

2813 raise 

2814 

2815 elif error == 'warn': 

2816 logger.warning(str(e)) 

2817 

2818 return tuple([merge(x) for x in list(zip(*list_of_tuples))[1:]]) 

2819 

2820 

2821def short_to_list(nmax, it): 

2822 import itertools 

2823 

2824 if isinstance(it, list): 

2825 return it 

2826 

2827 li = [] 

2828 for i in range(nmax+1): 

2829 try: 

2830 li.append(next(it)) 

2831 except StopIteration: 

2832 return li 

2833 

2834 return itertools.chain(li, it) 

2835 

2836 

2837def parse_md(f): 

2838 try: 

2839 with open(op.join( 

2840 op.dirname(op.abspath(f)), 

2841 'README.md'), 'r') as readme: 

2842 mdstr = readme.read() 

2843 except IOError as e: 

2844 return 'Failed to get README.md: %s' % e 

2845 

2846 # Remve the title 

2847 mdstr = re.sub(r'^# .*\n?', '', mdstr) 

2848 # Append sphinx reference to `pyrocko.` modules 

2849 mdstr = re.sub(r'`pyrocko\.(.*)`', r':py:mod:`pyrocko.\1`', mdstr) 

2850 # Convert Subsections to toc-less rubrics 

2851 mdstr = re.sub(r'## (.*)\n', r'.. rubric:: \1\n', mdstr) 

2852 return mdstr 

2853 

2854 

2855def mpl_show(plt): 

2856 import matplotlib 

2857 if matplotlib.get_backend().lower() == 'agg': 

2858 logger.warning('Cannot show() when using matplotlib "agg" backend') 

2859 else: 

2860 plt.show() 

2861 

2862 

2863g_re_qsplit = re.compile( 

2864 r'"([^"\\]*(?:\\.[^"\\]*)*)"|\'([^\'\\]*(?:\\.[^\'\\]*)*)\'|(\S+)') 

2865g_re_qsplit_sep = {} 

2866 

2867 

2868def get_re_qsplit(sep): 

2869 if sep is None: 

2870 return g_re_qsplit 

2871 else: 

2872 if sep not in g_re_qsplit_sep: 

2873 assert len(sep) == 1 

2874 assert sep not in '\'"' 

2875 esep = re.escape(sep) 

2876 g_re_qsplit_sep[sep] = re.compile( 

2877 r'"([^"\\]*(?:\\.[^"\\]*)*)"|\'([^\'\\]*(?:\\.[^\'\\]*)*)\'|' 

2878 + r'([^' + esep + r']+|(?<=' + esep + r')(?=' + esep + r')|^(?=' + esep + r')|(?<=' + esep + r')$)') # noqa 

2879 return g_re_qsplit_sep[sep] 

2880 

2881 

2882g_re_trivial = re.compile(r'\A[^\'"\s]+\Z') 

2883g_re_trivial_sep = {} 

2884 

2885 

2886def get_re_trivial(sep): 

2887 if sep is None: 

2888 return g_re_trivial 

2889 else: 

2890 if sep not in g_re_qsplit_sep: 

2891 assert len(sep) == 1 

2892 assert sep not in '\'"' 

2893 esep = re.escape(sep) 

2894 g_re_trivial_sep[sep] = re.compile(r'\A[^\'"' + esep + r']+\Z') 

2895 

2896 return g_re_trivial_sep[sep] 

2897 

2898 

2899g_re_escape_s = re.compile(r'([\\\'])') 

2900g_re_unescape_s = re.compile(r'\\([\\\'])') 

2901g_re_escape_d = re.compile(r'([\\"])') 

2902g_re_unescape_d = re.compile(r'\\([\\"])') 

2903 

2904 

2905def escape_s(s): 

2906 ''' 

2907 Backslash-escape single-quotes and backslashes. 

2908 

2909 Example: ``Jack's`` => ``Jack\\'s`` 

2910 

2911 ''' 

2912 return g_re_escape_s.sub(r'\\\1', s) 

2913 

2914 

2915def unescape_s(s): 

2916 ''' 

2917 Unescape backslash-escaped single-quotes and backslashes. 

2918 

2919 Example: ``Jack\\'s`` => ``Jack's`` 

2920 ''' 

2921 return g_re_unescape_s.sub(r'\1', s) 

2922 

2923 

2924def escape_d(s): 

2925 ''' 

2926 Backslash-escape double-quotes and backslashes. 

2927 

2928 Example: ``"Hello \\O/"`` => ``\\"Hello \\\\O/\\"`` 

2929 ''' 

2930 return g_re_escape_d.sub(r'\\\1', s) 

2931 

2932 

2933def unescape_d(s): 

2934 ''' 

2935 Unescape backslash-escaped double-quotes and backslashes. 

2936 

2937 Example: ``\\"Hello \\\\O/\\"`` => ``"Hello \\O/"`` 

2938 ''' 

2939 return g_re_unescape_d.sub(r'\1', s) 

2940 

2941 

2942def qjoin_s(it, sep=None): 

2943 ''' 

2944 Join sequence of strings into a line, single-quoting non-trivial strings. 

2945 

2946 Example: ``["55", "Sparrow's Island"]`` => ``"55 'Sparrow\\\\'s Island'"`` 

2947 ''' 

2948 re_trivial = get_re_trivial(sep) 

2949 

2950 if sep is None: 

2951 sep = ' ' 

2952 

2953 return sep.join( 

2954 w if re_trivial.search(w) else "'%s'" % escape_s(w) for w in it) 

2955 

2956 

2957def qjoin_d(it, sep=None): 

2958 ''' 

2959 Join sequence of strings into a line, double-quoting non-trivial strings. 

2960 

2961 Example: ``['55', 'Pete "The Robot" Smith']`` => 

2962 ``'55' "Pete \\\\"The Robot\\\\" Smith"'`` 

2963 ''' 

2964 re_trivial = get_re_trivial(sep) 

2965 if sep is None: 

2966 sep = ' ' 

2967 

2968 return sep.join( 

2969 w if re_trivial.search(w) else '"%s"' % escape_d(w) for w in it) 

2970 

2971 

2972def qsplit(s, sep=None): 

2973 ''' 

2974 Split line into list of strings, allowing for quoted strings. 

2975 

2976 Example: ``"55 'Sparrow\\\\'s Island'"`` => 

2977 ``["55", "Sparrow's Island"]``, 

2978 ``'55' "Pete \\\\"The Robot\\\\" Smith"'`` => 

2979 ``['55', 'Pete "The Robot" Smith']`` 

2980 ''' 

2981 re_qsplit = get_re_qsplit(sep) 

2982 return [ 

2983 (unescape_d(x[0]) or unescape_s(x[1]) or x[2]) 

2984 for x in re_qsplit.findall(s)] 

2985 

2986 

2987g_have_warned_threadpoolctl = False 

2988 

2989 

2990class threadpool_limits_dummy(object): 

2991 

2992 def __init__(self, *args, **kwargs): 

2993 pass 

2994 

2995 def __enter__(self): 

2996 global g_have_warned_threadpoolctl 

2997 

2998 if not g_have_warned_threadpoolctl: 

2999 logger.warning( 

3000 'Cannot control number of BLAS threads because ' 

3001 '`threadpoolctl` module is not available. You may want to ' 

3002 'install `threadpoolctl`.') 

3003 

3004 g_have_warned_threadpoolctl = True 

3005 

3006 return self 

3007 

3008 def __exit__(self, type, value, traceback): 

3009 pass 

3010 

3011 

3012def get_threadpool_limits(): 

3013 ''' 

3014 Try to import threadpoolctl.threadpool_limits, provide dummy if not avail. 

3015 ''' 

3016 

3017 try: 

3018 from threadpoolctl import threadpool_limits 

3019 return threadpool_limits 

3020 

3021 except ImportError: 

3022 return threadpool_limits_dummy 

3023 

3024 

3025def fmt_summary(entries, widths): 

3026 return ' | '.join( 

3027 entry.ljust(width) for (entry, width) in zip(entries, widths)) 

3028 

3029 

3030def smart_weakref(obj, callback=None): 

3031 if inspect.ismethod(obj): 

3032 return weakref.WeakMethod(obj, callback) 

3033 else: 

3034 return weakref.ref(obj, callback) 

3035 

3036 

3037DEFAULT_SIGNALS = [] 

3038for signame in ['SIGINT', 'SIGTERM', 'SIGHUP']: 

3039 try: 

3040 DEFAULT_SIGNALS.append(getattr(psignal, signame)) 

3041 except AttributeError: 

3042 pass 

3043 

3044 

3045class SignalQuitable: 

3046 

3047 def __init__(self, signals=DEFAULT_SIGNALS): 

3048 

3049 self.quit_requested = False 

3050 self._original = {} 

3051 self._signals = signals 

3052 

3053 def quit(self, *_): 

3054 self.quit_requested = True 

3055 

3056 def __enter__(self): 

3057 for sig in self._signals: 

3058 self._original[sig] = psignal.signal(sig, self.quit) 

3059 

3060 return self 

3061 

3062 def __exit__(self, *_): 

3063 for sig, handler in self._original.items(): 

3064 psignal.signal(sig, handler) 

3065 

3066 self._original = {} 

3067 

3068 

3069def group_by(key, xs): 

3070 groups = defaultdict(list) 

3071 for x in xs: 

3072 groups[key(x)].append(x) 

3073 

3074 return groups