requake 0.7.3__tar.gz → 0.8__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 (75) hide show
  1. {requake-0.7.3 → requake-0.8}/CHANGELOG.md +12 -0
  2. {requake-0.7.3 → requake-0.8}/PKG-INFO +11 -2
  3. {requake-0.7.3 → requake-0.8}/README.md +9 -0
  4. {requake-0.7.3 → requake-0.8}/requake/_version.py +3 -3
  5. {requake-0.7.3 → requake-0.8}/requake/catalog/catalog.py +48 -87
  6. {requake-0.7.3 → requake-0.8}/requake/catalog/print_catalog.py +1 -3
  7. {requake-0.7.3 → requake-0.8}/requake/catalog/read_catalog.py +27 -11
  8. {requake-0.7.3 → requake-0.8}/requake/catalog/read_catalog_from_csv.py +5 -7
  9. {requake-0.7.3 → requake-0.8}/requake/config/__init__.py +5 -1
  10. requake-0.8/requake/config/config.py +125 -0
  11. requake-0.8/requake/config/configobj/_version.py +6 -0
  12. {requake-0.7.3 → requake-0.8}/requake/config/generic_printer.py +1 -1
  13. {requake-0.7.3 → requake-0.8}/requake/config/parse_arguments.py +14 -15
  14. {requake-0.7.3 → requake-0.8}/requake/config/rq_setup.py +9 -23
  15. {requake-0.7.3 → requake-0.8}/requake/config/utils.py +22 -7
  16. requake-0.8/requake/database/__init__.py +12 -0
  17. requake-0.8/requake/database/catalog.py +136 -0
  18. requake-0.8/requake/database/db.py +158 -0
  19. requake-0.8/requake/database/families.py +174 -0
  20. requake-0.8/requake/database/pairs.py +180 -0
  21. requake-0.8/requake/database/templates.py +175 -0
  22. {requake-0.7.3 → requake-0.8}/requake/families/__init__.py +3 -2
  23. {requake-0.7.3 → requake-0.8}/requake/families/build_families.py +19 -26
  24. {requake-0.7.3 → requake-0.8}/requake/families/build_templates.py +1 -3
  25. {requake-0.7.3 → requake-0.8}/requake/families/families.py +21 -71
  26. requake-0.8/requake/families/flag_family.py +31 -0
  27. {requake-0.7.3 → requake-0.8}/requake/families/pairs.py +14 -44
  28. {requake-0.7.3 → requake-0.8}/requake/families/print_families.py +3 -5
  29. {requake-0.7.3 → requake-0.8}/requake/families/print_pairs.py +1 -3
  30. {requake-0.7.3 → requake-0.8}/requake/formulas/conversion.py +2 -4
  31. {requake-0.7.3 → requake-0.8}/requake/formulas/moment.py +2 -2
  32. {requake-0.7.3 → requake-0.8}/requake/formulas/slip.py +6 -6
  33. {requake-0.7.3 → requake-0.8}/requake/main.py +1 -1
  34. {requake-0.7.3 → requake-0.8}/requake/plot/cached_tiler.py +1 -1
  35. {requake-0.7.3 → requake-0.8}/requake/plot/colormaps.py +0 -1
  36. {requake-0.7.3 → requake-0.8}/requake/plot/map_families.py +1 -3
  37. {requake-0.7.3 → requake-0.8}/requake/plot/map_tiles.py +25 -25
  38. {requake-0.7.3 → requake-0.8}/requake/plot/plot_cumulative.py +5 -11
  39. {requake-0.7.3 → requake-0.8}/requake/plot/plot_families.py +23 -20
  40. {requake-0.7.3 → requake-0.8}/requake/plot/plot_pair.py +1 -3
  41. {requake-0.7.3 → requake-0.8}/requake/plot/plot_timespans.py +2 -6
  42. {requake-0.7.3 → requake-0.8}/requake/plot/plot_utils.py +11 -12
  43. {requake-0.7.3 → requake-0.8}/requake/scan/scan_catalog.py +29 -31
  44. {requake-0.7.3 → requake-0.8}/requake/scan/scan_templates.py +36 -47
  45. {requake-0.7.3 → requake-0.8}/requake/waveforms/waveform_pair.py +1 -0
  46. {requake-0.7.3 → requake-0.8}/requake/waveforms/waveforms.py +9 -12
  47. {requake-0.7.3 → requake-0.8}/requake.egg-info/PKG-INFO +11 -2
  48. {requake-0.7.3 → requake-0.8}/requake.egg-info/SOURCES.txt +6 -0
  49. {requake-0.7.3 → requake-0.8}/setup.py +1 -1
  50. requake-0.7.3/requake/config/config.py +0 -31
  51. requake-0.7.3/requake/config/configobj/_version.py +0 -2
  52. requake-0.7.3/requake/families/flag_family.py +0 -46
  53. {requake-0.7.3 → requake-0.8}/CONTRIBUTORS.txt +0 -0
  54. {requake-0.7.3 → requake-0.8}/LICENSE.txt +0 -0
  55. {requake-0.7.3 → requake-0.8}/MANIFEST.in +0 -0
  56. {requake-0.7.3 → requake-0.8}/pyproject.toml +0 -0
  57. {requake-0.7.3 → requake-0.8}/requake/__init__.py +0 -0
  58. {requake-0.7.3 → requake-0.8}/requake/catalog/__init__.py +0 -0
  59. {requake-0.7.3 → requake-0.8}/requake/catalog/read_catalog_from_fdsnws.py +0 -0
  60. {requake-0.7.3 → requake-0.8}/requake/catalog/read_catalog_from_quakeml.py +0 -0
  61. {requake-0.7.3 → requake-0.8}/requake/config/configobj/LICENSE +0 -0
  62. {requake-0.7.3 → requake-0.8}/requake/config/configobj/__init__.py +0 -0
  63. {requake-0.7.3 → requake-0.8}/requake/config/configobj/validate.py +0 -0
  64. {requake-0.7.3 → requake-0.8}/requake/config/configspec.conf +0 -0
  65. {requake-0.7.3 → requake-0.8}/requake/formulas/__init__.py +0 -0
  66. {requake-0.7.3 → requake-0.8}/requake/plot/__init__.py +0 -0
  67. {requake-0.7.3 → requake-0.8}/requake/scan/__init__.py +0 -0
  68. {requake-0.7.3 → requake-0.8}/requake/waveforms/__init__.py +0 -0
  69. {requake-0.7.3 → requake-0.8}/requake/waveforms/arrivals.py +0 -0
  70. {requake-0.7.3 → requake-0.8}/requake/waveforms/station_metadata.py +0 -0
  71. {requake-0.7.3 → requake-0.8}/requake.egg-info/dependency_links.txt +0 -0
  72. {requake-0.7.3 → requake-0.8}/requake.egg-info/entry_points.txt +0 -0
  73. {requake-0.7.3 → requake-0.8}/requake.egg-info/requires.txt +0 -0
  74. {requake-0.7.3 → requake-0.8}/requake.egg-info/top_level.txt +0 -0
  75. {requake-0.7.3 → requake-0.8}/setup.cfg +0 -0
@@ -2,6 +2,17 @@
2
2
 
3
3
  Copyright (c) 2021-2026 Claudio Satriano <satriano@ipgp.fr>
4
4
 
5
+ ## [0.8] - 2026-05-22
6
+
7
+ This release introduces a major storage change.
8
+
9
+ Requake now stores scan-related data (catalog, event pairs, and families) in a
10
+ SQLite database instead of CSV files. This improves storage efficiency and
11
+ data access performance, especially for large catalogs.
12
+
13
+ This change is not backward compatible at the data format level. The
14
+ high-level behavior of the command-line interface remains unchanged.
15
+
5
16
  ## [0.7.3] - 2026-04-14
6
17
 
7
18
  This release is intended to validate the new release workflow.
@@ -140,6 +151,7 @@ Mostly a bugfix release with a slightly improved trace plotting.
140
151
 
141
152
  - Initial release, not yet feature complete
142
153
 
154
+ [0.8]: https://github.com/SeismicSource/requake/compare/v0.7.3...v0.8
143
155
  [0.7.3]: https://github.com/SeismicSource/requake/compare/v0.7.2...v0.7.3
144
156
  [0.7.2]: https://github.com/SeismicSource/requake/compare/v0.7.1...v0.7.2
145
157
  [0.7.1]: https://github.com/SeismicSource/requake/compare/v0.7...v0.7.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: requake
3
- Version: 0.7.3
3
+ Version: 0.8
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@5d538bf0991032045ae581e62eef887b54cdfa16/imgs/Requake_logo.svg" width="400">
40
+ <img src="https://cdn.jsdelivr.net/gh/SeismicSource/requake@e3ac4cce14f782d62a5f22f432a5391a2ccacfe7/imgs/Requake_logo.svg" width="400">
41
41
 
42
42
  # Requake
43
43
 
@@ -140,6 +140,15 @@ You can keep your local Requake repository updated by running `git pull`
140
140
  from times to times. Thanks to `pip`'s "editable mode", you don't need to
141
141
  reinstall Requake after each update.
142
142
 
143
+ If you want the repository style rules to run locally before each commit,
144
+ enable the included git hook once per clone:
145
+
146
+ scripts/setup_local_hooks.sh
147
+
148
+ You can also run the local style checks manually with:
149
+
150
+ scripts/check_style.sh
151
+
143
152
  ## Running
144
153
 
145
154
  ### Command line arguments
@@ -101,6 +101,15 @@ You can keep your local Requake repository updated by running `git pull`
101
101
  from times to times. Thanks to `pip`'s "editable mode", you don't need to
102
102
  reinstall Requake after each update.
103
103
 
104
+ If you want the repository style rules to run locally before each commit,
105
+ enable the included git hook once per clone:
106
+
107
+ scripts/setup_local_hooks.sh
108
+
109
+ You can also run the local style checks manually with:
110
+
111
+ scripts/check_style.sh
112
+
104
113
  ## Running
105
114
 
106
115
  ### Command line arguments
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2026-04-14T10:01:43+0200",
11
+ "date": "2026-05-22T08:53:09+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "5d538bf0991032045ae581e62eef887b54cdfa16",
15
- "version": "0.7.3"
14
+ "full-revisionid": "e3ac4cce14f782d62a5f22f432a5391a2ccacfe7",
15
+ "version": "0.8"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -28,6 +28,7 @@ class RequakeEvent():
28
28
  depth=None, mag_type=None, mag=None, author=None,
29
29
  catalog=None, contributor=None, contributor_id=None,
30
30
  mag_author=None, location_name=None, trace_id=None):
31
+ """Initialize an event container."""
31
32
  self.evid = evid
32
33
  self.orig_time = orig_time
33
34
  self.lon = lon
@@ -45,24 +46,31 @@ class RequakeEvent():
45
46
  self.correlations = {}
46
47
 
47
48
  def __eq__(self, other):
49
+ """Return True when two events have the same identity."""
48
50
  return self.evid == other.evid and self.trace_id == other.trace_id
49
51
 
50
52
  def __gt__(self, other):
53
+ """Compare events by origin time."""
51
54
  return self.orig_time > other.orig_time
52
55
 
53
56
  def __ge__(self, other):
57
+ """Compare events by origin time."""
54
58
  return self.orig_time >= other.orig_time
55
59
 
56
60
  def __lt__(self, other):
61
+ """Compare events by origin time."""
57
62
  return self.orig_time < other.orig_time
58
63
 
59
64
  def __le__(self, other):
65
+ """Compare events by origin time."""
60
66
  return self.orig_time <= other.orig_time
61
67
 
62
68
  def __hash__(self):
69
+ """Return the event hash."""
63
70
  return self.evid.__hash__()
64
71
 
65
72
  def __str__(self):
73
+ """Return a compact string representation of the event."""
66
74
  return (
67
75
  f'{self.evid} {self.orig_time} '
68
76
  f'{self.lon} {self.lat} {self.depth} {self.mag_type} {self.mag}'
@@ -148,46 +156,12 @@ class RequakeCatalog(list):
148
156
  """
149
157
  self[:] = sorted(self, key=lambda ev: ev.orig_time)
150
158
 
151
- def read(self, filename):
152
- """
153
- Read catalog from FDSN text file format.
154
-
155
- Skips events already in the catalog.
156
-
157
- :param filename: input filename
158
- :type filename: str
159
-
160
- :raises FileNotFoundError: if filename does not exist
161
- :raises ValueError: if line is not in FDSN text file format
162
- """
163
- with open(filename, 'r', encoding='utf8') as fp:
164
- for line in fp:
165
- if not line:
166
- continue
167
- if line[0] == '#':
168
- continue
169
- ev = RequakeEvent()
170
- ev.from_fdsn_text(line)
171
- self.append(ev)
172
- self.deduplicate()
173
-
174
- def write(self, filename):
175
- """
176
- Write catalog in FDSN text file format.
177
-
178
- :param filename: output filename
179
- :type filename: str
180
- """
181
- with open(filename, 'w', encoding='utf8') as fp:
182
- for ev in self:
183
- fp.write(ev.fdsn_text() + '\n')
184
-
185
159
  def filter(
186
- self, starttime=None, endtime=None,
187
- minlatitude=None, maxlatitude=None,
188
- minlongitude=None, maxlongitude=None,
189
- mindepth=None, maxdepth=None,
190
- minmagnitude=None, maxmagnitude=None
160
+ self, starttime=None, endtime=None,
161
+ minlatitude=None, maxlatitude=None,
162
+ minlongitude=None, maxlongitude=None,
163
+ mindepth=None, maxdepth=None,
164
+ minmagnitude=None, maxmagnitude=None
191
165
  ):
192
166
  """
193
167
  Filter the catalog, based on the specified criteria.
@@ -220,60 +194,47 @@ class RequakeCatalog(list):
220
194
  """
221
195
  outcat = RequakeCatalog()
222
196
  for ev in self:
223
- if ev.orig_time is not None:
224
- if starttime is not None and ev.orig_time < starttime:
225
- continue
226
- if endtime is not None and ev.orig_time > endtime:
227
- continue
228
- if ev.lat is not None:
229
- if minlatitude is not None and ev.lat < minlatitude:
230
- continue
231
- if maxlatitude is not None and ev.lat > maxlatitude:
232
- continue
233
- if ev.lon is not None:
234
- if minlongitude is not None and ev.lon < minlongitude:
235
- continue
236
- if maxlongitude is not None and ev.lon > maxlongitude:
237
- continue
238
- if ev.depth is not None:
239
- if mindepth is not None and ev.depth < mindepth:
240
- continue
241
- if maxdepth is not None and ev.depth > maxdepth:
242
- continue
243
- if ev.mag is not None:
244
- if minmagnitude is not None and ev.mag < minmagnitude:
245
- continue
246
- if maxmagnitude is not None and ev.mag > maxmagnitude:
247
- continue
197
+ if _out_of_range(ev.orig_time, starttime, endtime):
198
+ continue
199
+ if _out_of_range(ev.lat, minlatitude, maxlatitude):
200
+ continue
201
+ if _out_of_range(ev.lon, minlongitude, maxlongitude):
202
+ continue
203
+ if _out_of_range(ev.depth, mindepth, maxdepth):
204
+ continue
205
+ if _out_of_range(ev.mag, minmagnitude, maxmagnitude):
206
+ continue
248
207
  outcat.append(ev)
249
208
  return outcat
250
209
 
251
210
 
211
+ def _out_of_range(value, minimum, maximum):
212
+ """Return True when a value falls outside the requested bounds."""
213
+ if value is None:
214
+ return False
215
+ return (
216
+ (minimum is not None and value < minimum)
217
+ or (maximum is not None and value > maximum)
218
+ )
219
+
220
+
252
221
  def read_stored_catalog():
253
222
  """
254
- Read the catalog stored in the output directory.
223
+ Read the catalog stored in the output database.
255
224
 
256
225
  :return: Catalog object.
257
226
  :rtype: RequakeCatalog
258
227
 
259
- :raises ValueError: if error reading catalog file
260
- :raises FileNotFoundError: if catalog file not found
228
+ :raises ValueError: if the stored catalog is empty
229
+ :raises FileNotFoundError: if the output database is not found
261
230
  """
262
- try:
263
- cat = RequakeCatalog()
264
- cat.read(config.scan_catalog_file)
265
- cat.sort()
266
- if not cat:
267
- raise ValueError('Empty catalog')
268
- return cat
269
- except ValueError as err:
270
- raise ValueError(
271
- f'Error reading catalog file {config.scan_catalog_file}: {err}'
272
- ) from err
273
- except (FileNotFoundError, NotADirectoryError) as err:
274
- raise FileNotFoundError(
275
- f'Catalog file {config.scan_catalog_file} not found'
276
- ) from err
231
+ from ..database.catalog import read_catalog as read_catalog_from_db
232
+
233
+ cat = read_catalog_from_db(config)
234
+ cat.sort()
235
+ if not cat:
236
+ raise ValueError('Empty catalog')
237
+ return cat
277
238
 
278
239
 
279
240
  def fix_non_locatable_events(catalog):
@@ -308,9 +269,9 @@ def _base26(val):
308
269
  :rtype: str
309
270
  """
310
271
  chars = [
311
- 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
312
- 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
313
- 'u', 'v', 'w', 'x', 'y', 'z'
272
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
273
+ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
274
+ 'u', 'v', 'w', 'x', 'y', 'z'
314
275
  ]
315
276
  base = len(chars)
316
277
  ret = ''
@@ -337,7 +298,7 @@ def generate_evid(orig_time):
337
298
  orig_year = UTCDateTime(year=year, month=1, day=1)
338
299
  val = int(orig_time - orig_year)
339
300
  # normalize val between 0 (aaaaaa) and 26**6-1 (zzzzzz)
340
- maxval = 366*24*3600 # max number of seconds in leap year
341
- normval = int(val/maxval * (26**6-1))
301
+ maxval = 366 * 24 * 3600 # max number of seconds in leap year
302
+ normval = int(val / maxval * (26 ** 6 - 1))
342
303
  ret = _base26(normval)
343
304
  return f'{prefix}{year}{ret}'
@@ -16,9 +16,7 @@ logger = logging.getLogger(__name__.rsplit('.', maxsplit=1)[-1])
16
16
 
17
17
 
18
18
  def print_catalog():
19
- """
20
- Print the event catalog to screen.
21
- """
19
+ """Print the event catalog to screen."""
22
20
  try:
23
21
  catalog = read_stored_catalog()
24
22
  except (ValueError, FileNotFoundError) as msg:
@@ -13,7 +13,9 @@ import os
13
13
  import logging
14
14
  import contextlib
15
15
  from ..config import config, rq_exit
16
- from .catalog import RequakeCatalog
16
+ from ..database.db import get_db_path
17
+ from ..database.catalog import write_catalog as write_stored_catalog
18
+ from .catalog import RequakeEvent, RequakeCatalog
17
19
  from .read_catalog_from_fdsnws import read_catalog_from_fdsnws
18
20
  from .read_catalog_from_quakeml import read_catalog_from_quakeml
19
21
  from .read_catalog_from_csv import read_catalog_from_csv
@@ -45,7 +47,16 @@ def _read_catalog_from_file():
45
47
  # try to read the catalog as a FDSN text file
46
48
  with contextlib.suppress(ValueError):
47
49
  cat = RequakeCatalog()
48
- cat.read(catalog_file)
50
+ with open(catalog_file, 'r', encoding='utf8') as fp:
51
+ for line in fp:
52
+ if not line:
53
+ continue
54
+ if line[0] == '#':
55
+ continue
56
+ ev = RequakeEvent()
57
+ ev.from_fdsn_text(line)
58
+ cat.append(ev)
59
+ cat.deduplicate()
49
60
  return cat
50
61
  # try to read the catalog as a CSV file
51
62
  # raises ValueError in case of failure
@@ -54,8 +65,7 @@ def _read_catalog_from_file():
54
65
 
55
66
  def _filter_catalog(catalog):
56
67
  """
57
- Filter an event catalog, based on the criteria specified
58
- in the configuration.
68
+ Filter an event catalog using the configured selection criteria.
59
69
 
60
70
  :param catalog: Event catalog.
61
71
  :type catalog: requake.catalog.RequakeCatalog
@@ -99,14 +109,17 @@ def read_catalog():
99
109
  :rtype: requake.catalog.RequakeCatalog
100
110
  """
101
111
  catalog = RequakeCatalog()
102
- output_cat_file = config.scan_catalog_file
103
112
  nevs_read = 0
104
113
  if config.args.append:
105
114
  with contextlib.suppress(FileNotFoundError):
106
- catalog = RequakeCatalog()
107
- catalog.read(output_cat_file)
115
+ from .catalog import read_stored_catalog
116
+
117
+ catalog = read_stored_catalog()
108
118
  nevs_read = len(catalog)
109
- logger.info(f'{nevs_read} events read from "{output_cat_file}"')
119
+ logger.info(
120
+ f'{nevs_read} events read from db file '
121
+ f'{get_db_path(config)}'
122
+ )
110
123
  logger.info('Reading catalog...')
111
124
  input_cat_file = config.args.catalog_file
112
125
  if input_cat_file is not None:
@@ -136,7 +149,10 @@ def read_catalog():
136
149
  logger.info(f'{nevs_dedup} duplicate events removed')
137
150
  # Sort catalog in increasing time order
138
151
  catalog.sort()
139
- # Write catalog to output file
140
- catalog.write(output_cat_file)
152
+ # Write catalog to the output database
153
+ write_stored_catalog(catalog, config)
141
154
  nevs_written = len(catalog) - nevs_read
142
- logger.info(f'{nevs_written} events written to "{output_cat_file}"')
155
+ logger.info(
156
+ f'{nevs_written} events written to db file '
157
+ f'{get_db_path(config)}'
158
+ )
@@ -19,8 +19,7 @@ logger = logging.getLogger(__name__.rsplit('.', maxsplit=1)[-1])
19
19
 
20
20
  def _guess_field_names(input_fields):
21
21
  """
22
- Guess the field names corresponding to origin time, latitude, longitude,
23
- depth, magnitude and magnitude type.
22
+ Guess field names for origin time, location, depth, and magnitude.
24
23
 
25
24
  :param input_fields: list of field names
26
25
  :type input_fields: list of str
@@ -80,9 +79,8 @@ def _guess_field_names(input_fields):
80
79
  # make a list of duplicated fields, which have been matched more than once
81
80
  duplicated_fields = [
82
81
  (key, value, output_field_scores[key])
83
- for key, value in output_fields.items() if
84
- value is not None and
85
- list(output_fields.values()).count(value) > 1
82
+ for key, value in output_fields.items()
83
+ if value is not None and list(output_fields.values()).count(value) > 1
86
84
  ]
87
85
  # if there are duplicated fields, keep the one with the highest score
88
86
  for _key, value, score in duplicated_fields:
@@ -167,7 +165,7 @@ def read_catalog_from_csv(filename):
167
165
  nrows -= 1 # first row is the header
168
166
  cat = RequakeCatalog()
169
167
  for n, row in enumerate(reader):
170
- print(f'reading row {n+1}/{nrows}\r', end='')
168
+ print(f'reading row {n + 1}/{nrows}\r', end='')
171
169
  if fields['orig_time'] is None:
172
170
  # try build a date-time field from year, month, day, hour,
173
171
  # minute and seconds fields
@@ -195,7 +193,7 @@ def read_catalog_from_csv(filename):
195
193
  )
196
194
  except ValueError:
197
195
  logger.error(
198
- f'Unable to parse origin time at row {n+2}: '
196
+ f'Unable to parse origin time at row {n + 2}: '
199
197
  f'"{orig_time_str}"')
200
198
  continue
201
199
  row[None] = None
@@ -14,4 +14,8 @@ 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()
16
16
  # function in rq_setup.py
17
- from .config import config # noqa
17
+ from .config import ( # noqa
18
+ config,
19
+ to_picklable_config_dict,
20
+ from_picklable_config_dict,
21
+ )
@@ -0,0 +1,125 @@
1
+ # -*- coding: utf8 -*-
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+ """
4
+ Config class for Requake.
5
+
6
+ :copyright:
7
+ 2021-2026 Claudio Satriano <satriano@ipgp.fr>
8
+ :license:
9
+ GNU General Public License v3.0 or later
10
+ (https://www.gnu.org/licenses/gpl-3.0-standalone.html)
11
+ """
12
+ from argparse import Namespace
13
+
14
+
15
+ # Runtime objects that are useful in-process but should not be sent to
16
+ # worker processes via ProcessPoolExecutor.
17
+ RUNTIME_ONLY_KEYS = frozenset({
18
+ 'station_client',
19
+ 'dataselect_client',
20
+ 'catalog_fdsn_event_clients',
21
+ })
22
+
23
+
24
+ class Config(dict):
25
+ """A class to access config values with dot notation."""
26
+
27
+ def __setitem__(self, key, value):
28
+ """Make Config keys accessible as attributes."""
29
+ super().__setattr__(key, value)
30
+ super().__setitem__(key, value)
31
+
32
+ def __getattr__(self, key):
33
+ """Make Config keys accessible as attributes."""
34
+ try:
35
+ return self.__getitem__(key)
36
+ except KeyError as err:
37
+ raise AttributeError(err) from err
38
+
39
+ __setattr__ = __setitem__
40
+
41
+
42
+ def to_picklable_config_dict(
43
+ cfg,
44
+ drop_inventory=False,
45
+ extra_excluded_keys=None,
46
+ ):
47
+ """
48
+ Build a pickle-safe config snapshot for multiprocessing.
49
+
50
+ This helper strips known runtime-only objects (for example ObsPy clients
51
+ containing SSL contexts) and converts ``args`` from ``argparse.Namespace``
52
+ to a plain ``dict``.
53
+
54
+ Typical usage with ``concurrent.futures.ProcessPoolExecutor``::
55
+
56
+ from concurrent.futures import ProcessPoolExecutor
57
+ from requake.config import (
58
+ config,
59
+ to_picklable_config_dict,
60
+ from_picklable_config_dict,
61
+ )
62
+
63
+ cfg_dict = to_picklable_config_dict(config)
64
+
65
+ def worker(cfg_dict, event_id):
66
+ cfg = from_picklable_config_dict(cfg_dict)
67
+ # Rebuild only what is needed in each process.
68
+ # Re-create network clients here, not in the parent process.
69
+ return cfg['cc_min'], event_id
70
+
71
+ def _worker_call(args):
72
+ cfg, evid = args
73
+ return worker(cfg, evid)
74
+
75
+ with ProcessPoolExecutor() as pool:
76
+ worker_args = ((cfg_dict, evid) for evid in event_ids)
77
+ list(pool.map(_worker_call, worker_args))
78
+
79
+ :param cfg: Requake config-like mapping.
80
+ :type cfg: Mapping
81
+ :param drop_inventory: Exclude ``inventory`` from the snapshot.
82
+ :type drop_inventory: bool
83
+ :param extra_excluded_keys: Additional keys to remove from the snapshot.
84
+ :type extra_excluded_keys: iterable or None
85
+ :return: A pickle-safe shallow copy of the configuration.
86
+ :rtype: dict
87
+ """
88
+ cfg_dict = dict(cfg)
89
+ excluded_keys = set(RUNTIME_ONLY_KEYS)
90
+ if drop_inventory:
91
+ excluded_keys.add('inventory')
92
+ if extra_excluded_keys is not None:
93
+ excluded_keys.update(extra_excluded_keys)
94
+ for key in excluded_keys:
95
+ cfg_dict.pop(key, None)
96
+
97
+ args = cfg_dict.get('args')
98
+ if isinstance(args, Namespace):
99
+ cfg_dict['args'] = vars(args).copy()
100
+
101
+ return cfg_dict
102
+
103
+
104
+ def from_picklable_config_dict(cfg_dict):
105
+ """
106
+ Rebuild a ``Config`` instance from a pickle-safe snapshot.
107
+
108
+ This helper is the counterpart of ``to_picklable_config_dict()`` and is
109
+ intended for worker processes receiving a serialized config payload.
110
+
111
+ If ``args`` is a dict, it is converted back to ``argparse.Namespace``.
112
+
113
+ :param cfg_dict: Pickle-safe config snapshot.
114
+ :type cfg_dict: Mapping
115
+ :return: Reconstructed config object.
116
+ :rtype: Config
117
+ """
118
+ cfg = Config()
119
+ cfg.update(dict(cfg_dict))
120
+ args = cfg.get('args')
121
+ if isinstance(args, dict):
122
+ cfg['args'] = Namespace(**args)
123
+ return cfg
124
+
125
+ config = Config() # noqa
@@ -0,0 +1,6 @@
1
+ """Project version.
2
+
3
+ Copyright (C) 2005-2014 ConfigObj contributors.
4
+ Licensed under BSD-3-Clause.
5
+ """
6
+ __version__ = '5.1.0'
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__.rsplit('.', maxsplit=1)[-1])
21
21
 
22
22
  def generic_printer(rows, headers_fmt, print_headers=True):
23
23
  """
24
- A generic printer function for Requake.
24
+ Print rows using the configured output format.
25
25
 
26
26
  :param rows: Rows to print.
27
27
  :type rows: list of rows
@@ -18,9 +18,8 @@ from .._version import get_versions
18
18
 
19
19
 
20
20
  class NewlineHelpFormatter(argparse.HelpFormatter):
21
- """
22
- Custom help formatter that preserves newlines in help messages.
23
- """
21
+ """Custom help formatter that preserves newlines in help messages."""
22
+
24
23
  def _split_lines(self, text, width):
25
24
  lines = []
26
25
  for line in text.splitlines(): # Split the text by newlines first
@@ -36,11 +35,11 @@ class NewlineHelpFormatter(argparse.HelpFormatter):
36
35
 
37
36
  class SubcommandHelpFormatter(argparse.RawDescriptionHelpFormatter):
38
37
  """
39
- Custom help formatter that removes the list of subcommands from the help
40
- message.
38
+ Custom help formatter that removes the subcommand list from help.
41
39
 
42
40
  See: https://stackoverflow.com/a/13429281/2021880
43
41
  """
42
+
44
43
  def _format_action(self, action):
45
44
  parts = super(
46
45
  argparse.RawDescriptionHelpFormatter, self
@@ -130,7 +129,7 @@ def parse_arguments(progname='requake'):
130
129
  '- CSV\n'
131
130
  '- Space-separated text files.\n\n'
132
131
  'For CSV or space-separated formats, the file must include '
133
- 'at least one column for the event\'s origin time '
132
+ "at least one column for the event's origin time "
134
133
  'and may contain additional columns for event ID, longitude, '
135
134
  'latitude, depth, and magnitude. Column names '
136
135
  'must be specified in the first row.\n\n'
@@ -472,8 +471,8 @@ def parse_arguments(progname='requake'):
472
471
  pass
473
472
  except ValueError:
474
473
  sys.stderr.write(
475
- f"{progname} {args.action}: "
476
- "error: argument -l/--longerthan: "
474
+ f'{progname} {args.action}: '
475
+ 'error: argument -l/--longerthan: '
477
476
  f"invalid value: '{args.longerthan}'\n"
478
477
  )
479
478
  sys.exit(2)
@@ -483,8 +482,8 @@ def parse_arguments(progname='requake'):
483
482
  pass
484
483
  except ValueError:
485
484
  sys.stderr.write(
486
- f"{progname} {args.action}: "
487
- "error: argument -S/--shorterthan: "
485
+ f'{progname} {args.action}: '
486
+ 'error: argument -S/--shorterthan: '
488
487
  f"invalid value: '{args.shorterthan}'\n"
489
488
  )
490
489
  sys.exit(2)
@@ -513,13 +512,13 @@ def _timespec_to_sec(timespec):
513
512
  elif suffix == 'm':
514
513
  time_in_seconds *= 60
515
514
  elif suffix == 'h':
516
- time_in_seconds *= 24*60
515
+ time_in_seconds *= 24 * 60
517
516
  elif suffix == 'd':
518
- time_in_seconds *= 24*60*60
517
+ time_in_seconds *= 24 * 60 * 60
519
518
  elif suffix == 'M':
520
- time_in_seconds *= 30*24*60*60
519
+ time_in_seconds *= 30 * 24 * 60 * 60
521
520
  elif suffix == 'y':
522
- time_in_seconds *= 365*24*60*60
521
+ time_in_seconds *= 365 * 24 * 60 * 60
523
522
  else:
524
- raise ValueError(f"Invalid time specification: {timespec}")
523
+ raise ValueError(f'Invalid time specification: {timespec}')
525
524
  return time_in_seconds