requake 0.8__tar.gz → 0.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {requake-0.8 → requake-0.8.2}/CHANGELOG.md +38 -0
  2. {requake-0.8 → requake-0.8.2}/PKG-INFO +10 -7
  3. {requake-0.8 → requake-0.8.2}/README.md +8 -5
  4. {requake-0.8 → requake-0.8.2}/requake/__init__.py +9 -0
  5. {requake-0.8 → requake-0.8.2}/requake/_version.py +3 -3
  6. {requake-0.8 → requake-0.8.2}/requake/catalog/catalog.py +2 -3
  7. {requake-0.8 → requake-0.8.2}/requake/catalog/read_catalog.py +5 -5
  8. {requake-0.8 → requake-0.8.2}/requake/config/__init__.py +1 -1
  9. {requake-0.8 → requake-0.8.2}/requake/config/configspec.conf +14 -0
  10. {requake-0.8 → requake-0.8.2}/requake/config/parse_arguments.py +42 -3
  11. {requake-0.8 → requake-0.8.2}/requake/config/rq_setup.py +47 -4
  12. {requake-0.8 → requake-0.8.2}/requake/database/catalog.py +5 -5
  13. {requake-0.8 → requake-0.8.2}/requake/database/db.py +12 -7
  14. {requake-0.8 → requake-0.8.2}/requake/database/families.py +8 -8
  15. requake-0.8.2/requake/database/pairs.py +481 -0
  16. {requake-0.8 → requake-0.8.2}/requake/database/templates.py +9 -9
  17. requake-0.8.2/requake/database/trace_metadata.py +343 -0
  18. {requake-0.8 → requake-0.8.2}/requake/families/build_families.py +26 -15
  19. {requake-0.8 → requake-0.8.2}/requake/families/families.py +2 -2
  20. {requake-0.8 → requake-0.8.2}/requake/families/flag_family.py +1 -1
  21. {requake-0.8 → requake-0.8.2}/requake/families/pairs.py +8 -17
  22. requake-0.8.2/requake/families/print_families.py +247 -0
  23. {requake-0.8 → requake-0.8.2}/requake/families/print_pairs.py +17 -7
  24. requake-0.8.2/requake/scan/scan_catalog.py +220 -0
  25. requake-0.8.2/requake/scan/scan_catalog_helpers.py +317 -0
  26. requake-0.8.2/requake/scan/scan_catalog_pairs.py +182 -0
  27. requake-0.8.2/requake/scan/scan_catalog_workers.py +561 -0
  28. {requake-0.8 → requake-0.8.2}/requake/scan/scan_templates.py +3 -3
  29. {requake-0.8 → requake-0.8.2}/requake/waveforms/waveform_pair.py +127 -37
  30. {requake-0.8 → requake-0.8.2}/requake/waveforms/waveforms.py +141 -12
  31. {requake-0.8 → requake-0.8.2}/requake.egg-info/PKG-INFO +10 -7
  32. {requake-0.8 → requake-0.8.2}/requake.egg-info/SOURCES.txt +4 -0
  33. requake-0.8/requake/database/pairs.py +0 -180
  34. requake-0.8/requake/families/print_families.py +0 -118
  35. requake-0.8/requake/scan/scan_catalog.py +0 -132
  36. {requake-0.8 → requake-0.8.2}/CONTRIBUTORS.txt +0 -0
  37. {requake-0.8 → requake-0.8.2}/LICENSE.txt +0 -0
  38. {requake-0.8 → requake-0.8.2}/MANIFEST.in +0 -0
  39. {requake-0.8 → requake-0.8.2}/pyproject.toml +0 -0
  40. {requake-0.8 → requake-0.8.2}/requake/catalog/__init__.py +0 -0
  41. {requake-0.8 → requake-0.8.2}/requake/catalog/print_catalog.py +0 -0
  42. {requake-0.8 → requake-0.8.2}/requake/catalog/read_catalog_from_csv.py +0 -0
  43. {requake-0.8 → requake-0.8.2}/requake/catalog/read_catalog_from_fdsnws.py +0 -0
  44. {requake-0.8 → requake-0.8.2}/requake/catalog/read_catalog_from_quakeml.py +0 -0
  45. {requake-0.8 → requake-0.8.2}/requake/config/config.py +0 -0
  46. {requake-0.8 → requake-0.8.2}/requake/config/configobj/LICENSE +0 -0
  47. {requake-0.8 → requake-0.8.2}/requake/config/configobj/__init__.py +0 -0
  48. {requake-0.8 → requake-0.8.2}/requake/config/configobj/_version.py +0 -0
  49. {requake-0.8 → requake-0.8.2}/requake/config/configobj/validate.py +0 -0
  50. {requake-0.8 → requake-0.8.2}/requake/config/generic_printer.py +0 -0
  51. {requake-0.8 → requake-0.8.2}/requake/config/utils.py +0 -0
  52. {requake-0.8 → requake-0.8.2}/requake/database/__init__.py +0 -0
  53. {requake-0.8 → requake-0.8.2}/requake/families/__init__.py +0 -0
  54. {requake-0.8 → requake-0.8.2}/requake/families/build_templates.py +0 -0
  55. {requake-0.8 → requake-0.8.2}/requake/formulas/__init__.py +0 -0
  56. {requake-0.8 → requake-0.8.2}/requake/formulas/conversion.py +0 -0
  57. {requake-0.8 → requake-0.8.2}/requake/formulas/moment.py +0 -0
  58. {requake-0.8 → requake-0.8.2}/requake/formulas/slip.py +0 -0
  59. {requake-0.8 → requake-0.8.2}/requake/main.py +0 -0
  60. {requake-0.8 → requake-0.8.2}/requake/plot/__init__.py +0 -0
  61. {requake-0.8 → requake-0.8.2}/requake/plot/cached_tiler.py +0 -0
  62. {requake-0.8 → requake-0.8.2}/requake/plot/colormaps.py +0 -0
  63. {requake-0.8 → requake-0.8.2}/requake/plot/map_families.py +0 -0
  64. {requake-0.8 → requake-0.8.2}/requake/plot/map_tiles.py +0 -0
  65. {requake-0.8 → requake-0.8.2}/requake/plot/plot_cumulative.py +0 -0
  66. {requake-0.8 → requake-0.8.2}/requake/plot/plot_families.py +0 -0
  67. {requake-0.8 → requake-0.8.2}/requake/plot/plot_pair.py +0 -0
  68. {requake-0.8 → requake-0.8.2}/requake/plot/plot_timespans.py +0 -0
  69. {requake-0.8 → requake-0.8.2}/requake/plot/plot_utils.py +0 -0
  70. {requake-0.8 → requake-0.8.2}/requake/scan/__init__.py +0 -0
  71. {requake-0.8 → requake-0.8.2}/requake/waveforms/__init__.py +0 -0
  72. {requake-0.8 → requake-0.8.2}/requake/waveforms/arrivals.py +0 -0
  73. {requake-0.8 → requake-0.8.2}/requake/waveforms/station_metadata.py +0 -0
  74. {requake-0.8 → requake-0.8.2}/requake.egg-info/dependency_links.txt +0 -0
  75. {requake-0.8 → requake-0.8.2}/requake.egg-info/entry_points.txt +0 -0
  76. {requake-0.8 → requake-0.8.2}/requake.egg-info/requires.txt +0 -0
  77. {requake-0.8 → requake-0.8.2}/requake.egg-info/top_level.txt +0 -0
  78. {requake-0.8 → requake-0.8.2}/setup.cfg +0 -0
  79. {requake-0.8 → requake-0.8.2}/setup.py +0 -0
@@ -2,6 +2,39 @@
2
2
 
3
3
  Copyright (c) 2021-2026 Claudio Satriano <satriano@ipgp.fr>
4
4
 
5
+ ## [0.8.2] - 2026-06-03
6
+
7
+ Note: you might want to run `requake update_config` to update your config file
8
+ to the latest version.
9
+
10
+ - Parallel processing of earthquake pairs in `scan_catalog`. On by default
11
+ with automatic detection of the number of worker processes. This can be
12
+ configured with the `catalog_scan_nprocs` config option.
13
+
14
+ ## [0.8.1] - 2026-05-31
15
+
16
+ - Slimmed-down DB size by optimizing `event_pairs` storage. This allows storing
17
+ also invalid pairs, so that resuming catalog scan is faster.
18
+ - Dramatic speedup of event pairs building by using a KD-tree
19
+ spatial index and approximate geodetic distance calculations.
20
+ - Dramatic speedup of waveform fetching by improving the caching mechanism.
21
+ A new config option `catalog_waveform_cache_size` allows to set the maximum
22
+ number of waveforms to cache.
23
+ - Dramatically faster reading of event pairs with cross-correlation
24
+ filters in large catalogs, thanks to the new SQLite-backed storage.
25
+ - Add an optional disk cache, through the `catalog_waveform_disk_cache_enabled`
26
+ config option.
27
+ - `scan_catalog`: when running as a batch job, log a progress
28
+ report every minute, instead of showing the progress bar.
29
+ - `scan_catalog`: if event pairs already exist in the database,
30
+ ask whether to overwrite or continue from an interrupted run.
31
+ Add `--force` to restart from scratch and `--force-continue`
32
+ (short `-c`) to resume without prompting.
33
+ - `print_families`: add horizontal and vertical distance min/max values
34
+ to both summary and detailed (`-d`) outputs.
35
+ - `print_families`: add `-f stats` output format for summary family
36
+ statistics.
37
+
5
38
  ## [0.8] - 2026-05-22
6
39
 
7
40
  This release introduces a major storage change.
@@ -13,6 +46,9 @@ data access performance, especially for large catalogs.
13
46
  This change is not backward compatible at the data format level. The
14
47
  high-level behavior of the command-line interface remains unchanged.
15
48
 
49
+ Note: further improvements based on this new storage system will be introduced
50
+ throughout the 0.8.x release series.
51
+
16
52
  ## [0.7.3] - 2026-04-14
17
53
 
18
54
  This release is intended to validate the new release workflow.
@@ -151,6 +187,8 @@ Mostly a bugfix release with a slightly improved trace plotting.
151
187
 
152
188
  - Initial release, not yet feature complete
153
189
 
190
+ [0.8.2]: https://github.com/SeismicSource/requake/compare/v0.8.1...v0.8.2
191
+ [0.8.1]: https://github.com/SeismicSource/requake/compare/v0.8...v0.8.1
154
192
  [0.8]: https://github.com/SeismicSource/requake/compare/v0.7.3...v0.8
155
193
  [0.7.3]: https://github.com/SeismicSource/requake/compare/v0.7.2...v0.7.3
156
194
  [0.7.2]: https://github.com/SeismicSource/requake/compare/v0.7.1...v0.7.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: requake
3
- Version: 0.8
3
+ Version: 0.8.2
4
4
  Summary: Repeating earthquakes search and analysis
5
5
  Author-email: Claudio Satriano <satriano@ipgp.fr>
6
6
  License-Expression: GPL-3.0-or-later
@@ -37,7 +37,7 @@ Dynamic: description
37
37
  Dynamic: description-content-type
38
38
  Dynamic: license-file
39
39
 
40
- <img src="https://cdn.jsdelivr.net/gh/SeismicSource/requake@e3ac4cce14f782d62a5f22f432a5391a2ccacfe7/imgs/Requake_logo.svg" width="400">
40
+ <img src="https://cdn.jsdelivr.net/gh/SeismicSource/requake@9071fd7343802f3ef642a16f6bb02ff5ce1d88f5/imgs/Requake_logo.svg" width="400">
41
41
 
42
42
  # Requake
43
43
 
@@ -223,11 +223,14 @@ repeating earthquake families:
223
223
 
224
224
  ## Performances
225
225
 
226
- - `requake scan_catalog` took 53 minutes on my 2.7 GHz i7 MacBook Pro to
227
- process 14,100,705 earthquake pairs.
228
- Dowloaded traces are cached in memory to speed up execution. Processing is not
229
- yet parallel: some improvements might come in future versions, when
230
- parallelization will be implemented.
226
+ - `requake scan_catalog` is designed to fully utilize available CPU cores by
227
+ processing earthquake pairs in parallel. On an M2 MacBook Air, scanning
228
+ 95,034 earthquake pairs downloaded via FDSNWS took approximately 5 minutes
229
+ using 7 worker processes, yielding ~19,000 pairs per minute. When repeating
230
+ the same scan with a fully cached set of waveforms, runtime dropped to 84
231
+ seconds (~68,000 pairs per minute). These results indicate that overall
232
+ performance is typically dominated by waveform download latency rather than
233
+ computation.
231
234
 
232
235
  - `requake build_families` is fast™.
233
236
 
@@ -184,11 +184,14 @@ repeating earthquake families:
184
184
 
185
185
  ## Performances
186
186
 
187
- - `requake scan_catalog` took 53 minutes on my 2.7 GHz i7 MacBook Pro to
188
- process 14,100,705 earthquake pairs.
189
- Dowloaded traces are cached in memory to speed up execution. Processing is not
190
- yet parallel: some improvements might come in future versions, when
191
- parallelization will be implemented.
187
+ - `requake scan_catalog` is designed to fully utilize available CPU cores by
188
+ processing earthquake pairs in parallel. On an M2 MacBook Air, scanning
189
+ 95,034 earthquake pairs downloaded via FDSNWS took approximately 5 minutes
190
+ using 7 worker processes, yielding ~19,000 pairs per minute. When repeating
191
+ the same scan with a fully cached set of waveforms, runtime dropped to 84
192
+ seconds (~68,000 pairs per minute). These results indicate that overall
193
+ performance is typically dominated by waveform download latency rather than
194
+ computation.
192
195
 
193
196
  - `requake build_families` is fast™.
194
197
 
@@ -9,5 +9,14 @@ Initialize requake package.
9
9
  GNU General Public License v3.0 or later
10
10
  (https://www.gnu.org/licenses/gpl-3.0-standalone.html)
11
11
  """
12
+ import multiprocessing
13
+ import signal
14
+
15
+ # Child processes spawned for parallel scans may import requake before worker
16
+ # initializers run. Ignore SIGINT early in children so Ctrl+C is managed by
17
+ # the parent process only.
18
+ if multiprocessing.current_process().name != 'MainProcess':
19
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
20
+
12
21
  from . import _version
13
22
  __version__ = _version.get_versions()['version']
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2026-05-22T08:53:09+0200",
11
+ "date": "2026-06-03T14:39:27+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "e3ac4cce14f782d62a5f22f432a5391a2ccacfe7",
15
- "version": "0.8"
14
+ "full-revisionid": "9071fd7343802f3ef642a16f6bb02ff5ce1d88f5",
15
+ "version": "0.8.2"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -11,7 +11,6 @@ Classes and functions for downloading, reading and writing catalogs.
11
11
  """
12
12
  import numpy as np
13
13
  from obspy import UTCDateTime
14
- from ..config import config
15
14
  from ..formulas import float_or_none
16
15
  from ..waveforms import get_traceid_coords
17
16
 
@@ -230,7 +229,7 @@ def read_stored_catalog():
230
229
  """
231
230
  from ..database.catalog import read_catalog as read_catalog_from_db
232
231
 
233
- cat = read_catalog_from_db(config)
232
+ cat = read_catalog_from_db()
234
233
  cat.sort()
235
234
  if not cat:
236
235
  raise ValueError('Empty catalog')
@@ -246,7 +245,7 @@ def fix_non_locatable_events(catalog):
246
245
  """
247
246
  if not any(ev.lat is None or ev.lon is None for ev in catalog):
248
247
  return
249
- traceid_coords = get_traceid_coords(config)
248
+ traceid_coords = get_traceid_coords()
250
249
  mean_lat = np.mean([
251
250
  coords['latitude'] for coords in traceid_coords.values()])
252
251
  mean_lon = np.mean([
@@ -117,8 +117,8 @@ def read_catalog():
117
117
  catalog = read_stored_catalog()
118
118
  nevs_read = len(catalog)
119
119
  logger.info(
120
- f'{nevs_read} events read from db file '
121
- f'{get_db_path(config)}'
120
+ f'{nevs_read:n} events read from db file '
121
+ f'{get_db_path()}'
122
122
  )
123
123
  logger.info('Reading catalog...')
124
124
  input_cat_file = config.args.catalog_file
@@ -150,9 +150,9 @@ def read_catalog():
150
150
  # Sort catalog in increasing time order
151
151
  catalog.sort()
152
152
  # Write catalog to the output database
153
- write_stored_catalog(catalog, config)
153
+ write_stored_catalog(catalog)
154
154
  nevs_written = len(catalog) - nevs_read
155
155
  logger.info(
156
- f'{nevs_written} events written to db file '
157
- f'{get_db_path(config)}'
156
+ f'{nevs_written:n} events written to db file '
157
+ f'{get_db_path()}'
158
158
  )
@@ -9,7 +9,7 @@ Configuration and initialization of the Requake package.
9
9
  GNU General Public License v3.0 or later
10
10
  (https://www.gnu.org/licenses/gpl-3.0-standalone.html)
11
11
  """
12
- from .rq_setup import rq_exit # noqa
12
+ from .rq_setup import rq_exit, wait_for_sigint_pause # noqa
13
13
  from .generic_printer import generic_printer # noqa
14
14
  # The config object is created in config.py and needs to be populated
15
15
  # when using requake from command line, this is done by the configure()
@@ -68,6 +68,20 @@ catalog_search_range = float(default=30)
68
68
  ## If more than one trace_id is specified (separated by commas), the closest
69
69
  ## station will be used for a given event pair.
70
70
  catalog_trace_id = force_list(default=NET.STA.LOC.CHAN)
71
+ ## Maximum number of event waveforms kept in memory cache during
72
+ ## catalog-based scan. Higher values increase memory usage and can reduce
73
+ ## repeated waveform downloads.
74
+ catalog_waveform_cache_size = integer(min=2, default=5000)
75
+ ## Per-worker waveform cache size for parallel catalog scan.
76
+ ## Set to 0 to derive it automatically from catalog_waveform_cache_size.
77
+ catalog_waveform_cache_size_parallel = integer(min=0, default=0)
78
+ ## Keep a persistent copy of fetched event waveforms on disk to speed up
79
+ ## reruns. This on-disk cache lives in OUTDIR/waveform_cache and is not
80
+ ## limited by catalog_waveform_cache_size.
81
+ catalog_waveform_disk_cache_enabled = boolean(default=true)
82
+ ## Number of worker processes for catalog scan.
83
+ ## 0 means automatic selection; 1 disables parallelism.
84
+ catalog_scan_nprocs = integer(min=0, default=0)
71
85
 
72
86
  #### Template-based scan
73
87
  ### The following parameters are for template-based scan:
@@ -49,6 +49,21 @@ class SubcommandHelpFormatter(argparse.RawDescriptionHelpFormatter):
49
49
  return parts
50
50
 
51
51
 
52
+ def _nonnegative_int(value):
53
+ """Argparse type for integers >= 0."""
54
+ try:
55
+ int_value = int(value)
56
+ except ValueError as err:
57
+ raise argparse.ArgumentTypeError(
58
+ f'invalid non-negative integer value: {value}'
59
+ ) from err
60
+ if int_value < 0:
61
+ raise argparse.ArgumentTypeError(
62
+ f'invalid non-negative integer value: {value}'
63
+ )
64
+ return int_value
65
+
66
+
52
67
  def parse_arguments(progname='requake'):
53
68
  """
54
69
  Parse command line arguments.
@@ -160,11 +175,30 @@ def parse_arguments(progname='requake'):
160
175
  )
161
176
  # ---
162
177
  # --- scan_catalog
163
- subparser.add_parser(
178
+ scan_catalog = subparser.add_parser(
164
179
  'scan_catalog',
165
- parents=[force],
166
180
  help='scan an existing catalog for earthquake pairs'
167
181
  )
182
+ scan_catalog_mode = scan_catalog.add_mutually_exclusive_group()
183
+ scan_catalog_mode.add_argument(
184
+ '-f',
185
+ '--force',
186
+ action='store_true',
187
+ help='delete existing event pairs and restart the scan'
188
+ )
189
+ scan_catalog_mode.add_argument(
190
+ '-c',
191
+ '--force-continue',
192
+ action='store_true',
193
+ help='continue an interrupted scan and keep existing event pairs'
194
+ )
195
+ scan_catalog.add_argument(
196
+ '--nprocs',
197
+ type=_nonnegative_int,
198
+ default=None,
199
+ help='number of worker processes for scan_catalog '
200
+ '(0: auto, 1: disable parallelism)'
201
+ )
168
202
  # ---
169
203
  # --- print_pairs
170
204
  print_pairs = subparser.add_parser(
@@ -324,10 +358,15 @@ def parse_arguments(progname='requake'):
324
358
  print_families = subparser.add_parser(
325
359
  'print_families',
326
360
  parents=[
327
- longerthan, shorterthan, minevents, family_numbers, print_format
361
+ longerthan, shorterthan, minevents, family_numbers
328
362
  ],
329
363
  help='print families to screen'
330
364
  )
365
+ print_families.add_argument(
366
+ '-f', '--format', type=str, default='simple',
367
+ choices=['simple', 'markdown', 'csv', 'stats'],
368
+ help='format for the output (default: %(default)s)'
369
+ )
331
370
  print_families.add_argument(
332
371
  '-d', '--detailed', action='store_true',
333
372
  help='print detailed information for each family, including a list '
@@ -10,11 +10,13 @@ Setup functions for Requake.
10
10
  (https://www.gnu.org/licenses/gpl-3.0-standalone.html)
11
11
  """
12
12
  import contextlib
13
+ import multiprocessing
13
14
  import sys
14
15
  import os
15
16
  import shutil
16
17
  import logging
17
18
  import signal
19
+ import time
18
20
  import tqdm
19
21
  from .._version import get_versions
20
22
  from ..database.db import get_db_path
@@ -31,6 +33,10 @@ PYTHON_VERSION_STR = None
31
33
  NUMPY_VERSION_STR = None
32
34
  SCIPY_VERSION_STR = None
33
35
  OBSPY_VERSION_STR = None
36
+ SIGINT_CONFIRM_WINDOW_S = 2.0
37
+ SIGINT_PAUSE_POLL_S = 0.05
38
+ _LAST_SIGINT_TS = None
39
+ _SIGINT_PAUSE_UNTIL_TS = None
34
40
 
35
41
 
36
42
  def _check_library_versions():
@@ -273,7 +279,7 @@ def configure(args):
273
279
  should_write_catalog = (
274
280
  args.action == 'read_catalog'
275
281
  and not args.append
276
- and not write_ok(get_db_path(config), args.force)
282
+ and not write_ok(get_db_path(), args.force)
277
283
  )
278
284
  if should_write_catalog:
279
285
  print('Exiting now.')
@@ -330,8 +336,45 @@ def rq_exit(retval=0, abort=False, progname='requake'):
330
336
 
331
337
 
332
338
  def sigint_handler(_sig, _frame):
333
- """Abort gracefully."""
334
- rq_exit(1, abort=True)
339
+ """Ask for confirmation on first Ctrl+C and abort on second."""
340
+ global _LAST_SIGINT_TS # pylint: disable=global-statement
341
+ global _SIGINT_PAUSE_UNTIL_TS # pylint: disable=global-statement
335
342
 
343
+ if multiprocessing.current_process().name != 'MainProcess':
344
+ signal.default_int_handler(_sig, _frame)
345
+ return
346
+
347
+ now = time.monotonic()
348
+ if (
349
+ _LAST_SIGINT_TS is not None
350
+ and now - _LAST_SIGINT_TS <= SIGINT_CONFIRM_WINDOW_S
351
+ ):
352
+ rq_exit(1, abort=True)
353
+ _LAST_SIGINT_TS = now
354
+ _SIGINT_PAUSE_UNTIL_TS = now + SIGINT_CONFIRM_WINDOW_S
355
+ print(
356
+ '\nCtrl+C pressed. '
357
+ f'Press Ctrl+C again within {SIGINT_CONFIRM_WINDOW_S:g}s '
358
+ 'to abort.'
359
+ )
336
360
 
337
- signal.signal(signal.SIGINT, sigint_handler)
361
+
362
+ def wait_for_sigint_pause():
363
+ """Block while the first Ctrl+C confirmation window is active."""
364
+ global _SIGINT_PAUSE_UNTIL_TS # pylint: disable=global-statement
365
+
366
+ if multiprocessing.current_process().name != 'MainProcess':
367
+ return
368
+ while _SIGINT_PAUSE_UNTIL_TS is not None:
369
+ if time.monotonic() >= _SIGINT_PAUSE_UNTIL_TS:
370
+ _SIGINT_PAUSE_UNTIL_TS = None
371
+ break
372
+ time.sleep(SIGINT_PAUSE_POLL_S)
373
+
374
+
375
+ if multiprocessing.current_process().name == 'MainProcess':
376
+ signal.signal(signal.SIGINT, sigint_handler)
377
+ else:
378
+ # Workers must ignore SIGINT from startup to avoid pool breakage during
379
+ # spawn, before the worker initializer runs.
380
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
@@ -86,9 +86,9 @@ def _event_from_row(row):
86
86
  )
87
87
 
88
88
 
89
- def write_catalog(catalog, config, db_path=None):
89
+ def write_catalog(catalog, db_path=None):
90
90
  """Write the stored catalog into SQLite."""
91
- conn = get_db_connection(config, initdb=True, db_path=db_path)
91
+ conn = get_db_connection(initdb=True, db_path=db_path)
92
92
  try:
93
93
  cursor = conn.cursor()
94
94
  _ensure_catalog_table(cursor)
@@ -112,11 +112,11 @@ def write_catalog(catalog, config, db_path=None):
112
112
  conn.close()
113
113
 
114
114
 
115
- def read_catalog(config, db_path=None):
115
+ def read_catalog(db_path=None):
116
116
  """Read the stored catalog from SQLite."""
117
117
  from ..catalog.catalog import RequakeCatalog
118
118
 
119
- conn = get_db_connection(config, initdb=False, db_path=db_path)
119
+ conn = get_db_connection(initdb=False, db_path=db_path)
120
120
  try:
121
121
  rows = conn.execute(
122
122
  f'SELECT * FROM {CATALOG_TABLE} ORDER BY orig_time'
@@ -125,7 +125,7 @@ def read_catalog(config, db_path=None):
125
125
  if MISSING_CATALOG_TABLE in str(err):
126
126
  raise FileNotFoundError(
127
127
  'Catalog not found in db file '
128
- f'{get_db_path(config, db_path=db_path)}'
128
+ f'{get_db_path(db_path=db_path)}'
129
129
  ) from err
130
130
  raise
131
131
  finally:
@@ -14,6 +14,7 @@ import contextlib
14
14
  import random
15
15
  import sqlite3
16
16
  import time
17
+ from ..config.config import config
17
18
 
18
19
  DB_VERSION = 1
19
20
  DB_FILENAME = 'requake.sqlite'
@@ -29,7 +30,11 @@ class DatabaseBusyError(RuntimeError):
29
30
  """Raised when retries for transient SQLite lock errors are exhausted."""
30
31
 
31
32
 
32
- def get_db_path(config, db_path=None):
33
+ class DatabaseCorruptError(RuntimeError):
34
+ """Raised when the SQLite database file is corrupt or malformed."""
35
+
36
+
37
+ def get_db_path(db_path=None):
33
38
  """Return the SQLite path."""
34
39
  if db_path is not None:
35
40
  return db_path
@@ -61,9 +66,9 @@ def get_db_path(config, db_path=None):
61
66
  return os.path.join(outdir, DB_FILENAME)
62
67
 
63
68
 
64
- def check_db_exists(config, initdb=False, db_path=None):
69
+ def check_db_exists(initdb=False, db_path=None):
65
70
  """Validate the configured database path for read or init mode."""
66
- db_path = get_db_path(config, db_path=db_path)
71
+ db_path = get_db_path(db_path=db_path)
67
72
  if initdb:
68
73
  _ensure_parent_dir(db_path)
69
74
  return db_path
@@ -138,15 +143,15 @@ def execute_with_retry(operation, operation_name):
138
143
  attempt += 1
139
144
 
140
145
 
141
- def initialize_database_if_needed(config, db_path=None):
146
+ def initialize_database_if_needed(db_path=None):
142
147
  """Initialize database schema version and pragmas if missing."""
143
- conn = get_db_connection(config, initdb=True, db_path=db_path)
148
+ conn = get_db_connection(initdb=True, db_path=db_path)
144
149
  conn.close()
145
150
 
146
151
 
147
- def get_db_connection(config, initdb=False, db_path=None):
152
+ def get_db_connection(initdb=False, db_path=None):
148
153
  """Open a SQLite connection for the configured database file."""
149
- db_path = check_db_exists(config, initdb=initdb, db_path=db_path)
154
+ db_path = check_db_exists(initdb=initdb, db_path=db_path)
150
155
  conn = sqlite3.connect(db_path)
151
156
  _configure_connection(conn)
152
157
  conn.row_factory = sqlite3.Row
@@ -64,9 +64,9 @@ def _family_row(family, event):
64
64
  )
65
65
 
66
66
 
67
- def write_families(families, config):
67
+ def write_families(families):
68
68
  """Write catalog-scan families into SQLite."""
69
- conn = get_db_connection(config, initdb=True)
69
+ conn = get_db_connection(initdb=True)
70
70
  try:
71
71
  cursor = conn.cursor()
72
72
  _ensure_families_table(cursor)
@@ -95,12 +95,12 @@ def write_families(families, config):
95
95
  conn.close()
96
96
 
97
97
 
98
- def read_families(config):
98
+ def read_families():
99
99
  """Read catalog-scan families from SQLite."""
100
100
  from ..catalog import RequakeEvent
101
101
  from ..families.families import Family
102
102
 
103
- conn = get_db_connection(config, initdb=False)
103
+ conn = get_db_connection(initdb=False)
104
104
  try:
105
105
  cursor = conn.cursor()
106
106
  try:
@@ -113,7 +113,7 @@ def read_families(config):
113
113
  except sqlite3.OperationalError as err:
114
114
  if MISSING_FAMILIES_TABLE in str(err):
115
115
  raise FileNotFoundError(
116
- f'Families not found in db file {get_db_path(config)}'
116
+ f'Families not found in db file {get_db_path()}'
117
117
  ) from err
118
118
  raise
119
119
  finally:
@@ -147,9 +147,9 @@ def read_families(config):
147
147
  return families
148
148
 
149
149
 
150
- def update_family_valid(config, family_number, is_valid):
150
+ def update_family_valid(family_number, is_valid):
151
151
  """Update the validity flag for all events in one family."""
152
- conn = get_db_connection(config, initdb=False)
152
+ conn = get_db_connection(initdb=False)
153
153
  try:
154
154
  cursor = conn.cursor()
155
155
  try:
@@ -166,7 +166,7 @@ def update_family_valid(config, family_number, is_valid):
166
166
  except sqlite3.OperationalError as err:
167
167
  if MISSING_FAMILIES_TABLE in str(err):
168
168
  raise FileNotFoundError(
169
- f'Families not found in db file {get_db_path(config)}'
169
+ f'Families not found in db file {get_db_path()}'
170
170
  ) from err
171
171
  raise
172
172
  conn.commit()