requake 0.8.3__tar.gz → 0.8.4__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 (91) hide show
  1. {requake-0.8.3 → requake-0.8.4}/CHANGELOG.md +62 -0
  2. {requake-0.8.3 → requake-0.8.4}/PKG-INFO +7 -3
  3. {requake-0.8.3 → requake-0.8.4}/README.md +3 -1
  4. {requake-0.8.3 → requake-0.8.4}/pyproject.toml +5 -0
  5. {requake-0.8.3 → requake-0.8.4}/requake/_version.py +3 -3
  6. {requake-0.8.3 → requake-0.8.4}/requake/catalog/catalog.py +3 -1
  7. {requake-0.8.3 → requake-0.8.4}/requake/catalog/print_catalog.py +12 -4
  8. {requake-0.8.3 → requake-0.8.4}/requake/catalog/read_catalog.py +6 -2
  9. {requake-0.8.3 → requake-0.8.4}/requake/catalog/read_catalog_from_fdsnws.py +15 -4
  10. requake-0.8.4/requake/config/generic_printer.py +208 -0
  11. {requake-0.8.3 → requake-0.8.4}/requake/config/parse_arguments.py +310 -12
  12. {requake-0.8.3 → requake-0.8.4}/requake/config/rq_setup.py +30 -17
  13. {requake-0.8.3 → requake-0.8.4}/requake/config/utils.py +150 -10
  14. {requake-0.8.3 → requake-0.8.4}/requake/database/catalog.py +104 -3
  15. {requake-0.8.3 → requake-0.8.4}/requake/database/db.py +32 -0
  16. {requake-0.8.3 → requake-0.8.4}/requake/database/pairs.py +262 -9
  17. {requake-0.8.3 → requake-0.8.4}/requake/families/__init__.py +1 -0
  18. {requake-0.8.3 → requake-0.8.4}/requake/families/build_families.py +22 -9
  19. requake-0.8.4/requake/families/dedup_events.py +431 -0
  20. {requake-0.8.3 → requake-0.8.4}/requake/families/families.py +4 -1
  21. {requake-0.8.3 → requake-0.8.4}/requake/families/pairs.py +6 -2
  22. {requake-0.8.3 → requake-0.8.4}/requake/families/print_families.py +57 -3
  23. requake-0.8.4/requake/families/print_pairs.py +230 -0
  24. requake-0.8.4/requake/logo.py +15 -0
  25. {requake-0.8.3 → requake-0.8.4}/requake/main.py +9 -0
  26. requake-0.8.4/requake/pager.py +1392 -0
  27. requake-0.8.4/requake/plot/map_families.py +38 -0
  28. requake-0.8.3/requake/plot/map_families.py → requake-0.8.4/requake/plot/map_families_cartopy.py +47 -11
  29. requake-0.8.4/requake/plot/map_families_folium.py +582 -0
  30. requake-0.8.4/requake/plot/map_families_utils.py +291 -0
  31. requake-0.8.4/requake/plot/plot_families.py +996 -0
  32. {requake-0.8.3 → requake-0.8.4}/requake/scan/scan_catalog_pairs.py +16 -8
  33. requake-0.8.4/requake/terminal.py +328 -0
  34. {requake-0.8.3 → requake-0.8.4}/requake/waveforms/arrivals.py +14 -2
  35. {requake-0.8.3 → requake-0.8.4}/requake/waveforms/station_metadata.py +25 -12
  36. {requake-0.8.3 → requake-0.8.4}/requake/waveforms/waveform_pair.py +0 -2
  37. {requake-0.8.3 → requake-0.8.4}/requake/waveforms/waveforms.py +5 -1
  38. {requake-0.8.3 → requake-0.8.4}/requake/wfcache/commands.py +26 -14
  39. {requake-0.8.3 → requake-0.8.4}/requake.egg-info/PKG-INFO +7 -3
  40. {requake-0.8.3 → requake-0.8.4}/requake.egg-info/SOURCES.txt +7 -0
  41. {requake-0.8.3 → requake-0.8.4}/requake.egg-info/requires.txt +3 -0
  42. requake-0.8.3/requake/config/generic_printer.py +0 -65
  43. requake-0.8.3/requake/families/print_pairs.py +0 -83
  44. requake-0.8.3/requake/plot/plot_families.py +0 -252
  45. {requake-0.8.3 → requake-0.8.4}/CONTRIBUTORS.txt +0 -0
  46. {requake-0.8.3 → requake-0.8.4}/LICENSE.txt +0 -0
  47. {requake-0.8.3 → requake-0.8.4}/MANIFEST.in +0 -0
  48. {requake-0.8.3 → requake-0.8.4}/requake/__init__.py +0 -0
  49. {requake-0.8.3 → requake-0.8.4}/requake/catalog/__init__.py +0 -0
  50. {requake-0.8.3 → requake-0.8.4}/requake/catalog/read_catalog_from_csv.py +0 -0
  51. {requake-0.8.3 → requake-0.8.4}/requake/catalog/read_catalog_from_quakeml.py +0 -0
  52. {requake-0.8.3 → requake-0.8.4}/requake/config/__init__.py +0 -0
  53. {requake-0.8.3 → requake-0.8.4}/requake/config/config.py +0 -0
  54. {requake-0.8.3 → requake-0.8.4}/requake/config/configobj/LICENSE +0 -0
  55. {requake-0.8.3 → requake-0.8.4}/requake/config/configobj/__init__.py +0 -0
  56. {requake-0.8.3 → requake-0.8.4}/requake/config/configobj/_version.py +0 -0
  57. {requake-0.8.3 → requake-0.8.4}/requake/config/configobj/validate.py +0 -0
  58. {requake-0.8.3 → requake-0.8.4}/requake/config/configspec.conf +0 -0
  59. {requake-0.8.3 → requake-0.8.4}/requake/database/__init__.py +0 -0
  60. {requake-0.8.3 → requake-0.8.4}/requake/database/families.py +0 -0
  61. {requake-0.8.3 → requake-0.8.4}/requake/database/templates.py +0 -0
  62. {requake-0.8.3 → requake-0.8.4}/requake/database/trace_metadata.py +0 -0
  63. {requake-0.8.3 → requake-0.8.4}/requake/families/build_templates.py +0 -0
  64. {requake-0.8.3 → requake-0.8.4}/requake/families/flag_family.py +0 -0
  65. {requake-0.8.3 → requake-0.8.4}/requake/formulas/__init__.py +0 -0
  66. {requake-0.8.3 → requake-0.8.4}/requake/formulas/conversion.py +0 -0
  67. {requake-0.8.3 → requake-0.8.4}/requake/formulas/moment.py +0 -0
  68. {requake-0.8.3 → requake-0.8.4}/requake/formulas/slip.py +0 -0
  69. {requake-0.8.3 → requake-0.8.4}/requake/plot/__init__.py +0 -0
  70. {requake-0.8.3 → requake-0.8.4}/requake/plot/cached_tiler.py +0 -0
  71. {requake-0.8.3 → requake-0.8.4}/requake/plot/colormaps.py +0 -0
  72. {requake-0.8.3 → requake-0.8.4}/requake/plot/map_tiles.py +0 -0
  73. {requake-0.8.3 → requake-0.8.4}/requake/plot/plot_cumulative.py +0 -0
  74. {requake-0.8.3 → requake-0.8.4}/requake/plot/plot_pair.py +0 -0
  75. {requake-0.8.3 → requake-0.8.4}/requake/plot/plot_timespans.py +0 -0
  76. {requake-0.8.3 → requake-0.8.4}/requake/plot/plot_utils.py +0 -0
  77. {requake-0.8.3 → requake-0.8.4}/requake/scan/__init__.py +0 -0
  78. {requake-0.8.3 → requake-0.8.4}/requake/scan/parallel_diagnostics.py +0 -0
  79. {requake-0.8.3 → requake-0.8.4}/requake/scan/scan_catalog.py +0 -0
  80. {requake-0.8.3 → requake-0.8.4}/requake/scan/scan_catalog_helpers.py +0 -0
  81. {requake-0.8.3 → requake-0.8.4}/requake/scan/scan_catalog_workers.py +0 -0
  82. {requake-0.8.3 → requake-0.8.4}/requake/scan/scan_templates.py +0 -0
  83. {requake-0.8.3 → requake-0.8.4}/requake/scan/slurm_diagnostics.py +0 -0
  84. {requake-0.8.3 → requake-0.8.4}/requake/waveforms/__init__.py +0 -0
  85. {requake-0.8.3 → requake-0.8.4}/requake/wfcache/__init__.py +0 -0
  86. {requake-0.8.3 → requake-0.8.4}/requake/wfcache/storage.py +0 -0
  87. {requake-0.8.3 → requake-0.8.4}/requake.egg-info/dependency_links.txt +0 -0
  88. {requake-0.8.3 → requake-0.8.4}/requake.egg-info/entry_points.txt +0 -0
  89. {requake-0.8.3 → requake-0.8.4}/requake.egg-info/top_level.txt +0 -0
  90. {requake-0.8.3 → requake-0.8.4}/setup.cfg +0 -0
  91. {requake-0.8.3 → requake-0.8.4}/setup.py +0 -0
@@ -2,6 +2,67 @@
2
2
 
3
3
  Copyright (c) 2021-2026 Claudio Satriano <satriano@ipgp.fr>
4
4
 
5
+ ## [0.8.4] - 2026-10-23
6
+
7
+ - ``plot_families``: new ``--mode`` option, to draw the families as a
8
+ heatmap (``--mode heatmap``): one row per event, with the trace
9
+ amplitudes encoded in color and the family average drawn as a wiggle
10
+ below. The default remains the wiggle plot (``--mode wiggle``).
11
+ - ``plot_families``: the family average is now drawn in its own panel
12
+ also in ``--mode wiggle``, and both modes show an extra panel below
13
+ it, where all the traces of the family are drawn stacked on top of
14
+ each other, with the average highlighted on top, so that the waveform
15
+ similarity can be judged at a glance.
16
+ - ``plot_families``: new ``--max-labels`` option (default 15), to set
17
+ the maximum number of traces carrying a label. When a family has more
18
+ traces than that, only a subset of them is labeled (in wiggle mode,
19
+ the labeled traces are also highlighted and the traces are scaled up),
20
+ so that large families remain readable. Use 0 to label every trace.
21
+ - New ``-E``/``--events`` option for ``print_families``, to print one
22
+ row per event (ordered by family number and origin time), instead of
23
+ one row per family; combine with ``-f csv`` to export the list.
24
+ - New ``dedup_events`` command: flags likely duplicate events (event
25
+ pairs with high cross-correlation, close in time and space) with the
26
+ new ``duplicate`` column in the catalog, and marks all the pairs
27
+ involving them as invalid (new ``invalid`` column in ``event_pairs``);
28
+ ``build_families`` ignores the invalid pairs. A compact report is
29
+ printed at the end (``-n``/``--dry-run`` only prints it) and
30
+ ``--reset`` clears the flags.
31
+ - ``print_pairs`` hides the invalid pairs by default; the new
32
+ ``--include-invalid`` option lists them as well.
33
+ - Long operations now show a progress indicator, and long database
34
+ queries can be interrupted with ``Ctrl+C`` (pressed twice): the
35
+ database is closed cleanly (no stray ``-wal``/``-shm`` files) and the
36
+ abort is no longer reported as a bug.
37
+ - Fixed the non-paged table output (``--no-pager``), which dropped all
38
+ the rows but the last batch.
39
+ - Stylized ``◀◀ RƎQuake`` logo, shown in the command line help.
40
+ - Colored terminal output for help and usage messages, log messages,
41
+ errors, table headers and waveform-cache diagnostics. Colors are used
42
+ only on interactive terminals, and are therefore automatically removed
43
+ when the output is piped or redirected. The ``NO_COLOR`` and
44
+ ``FORCE_COLOR`` environment variables can be used to force colors off
45
+ or on.
46
+ - New interactive curses pager for all ``print_`` commands
47
+ (``print_catalog``, ``print_pairs``, ``print_families``).
48
+ Automatically activated when output is a terminal; use ``--no-pager``
49
+ to fall back to plain-text output. Requesting a specific output format
50
+ with ``-f``/``--format`` implies ``--no-pager``.
51
+ - New ``-b/--backend`` option for ``map_families``, to plot families on
52
+ an interactive HTML map using
53
+ [folium](https://python-visualization.github.io/folium/), instead of
54
+ the default static Cartopy map. Folium is an optional dependency (not
55
+ installed by default): the folium backend is only used if folium is
56
+ installed.
57
+ - Family marker size is now scaled with a quantity selected with the new
58
+ ``-s/--sizeby`` option (default: number of events), and a legend on the
59
+ map explains the marker size. This applies to both the Cartopy and the
60
+ Folium map backends.
61
+ - Clickable URLs in terminal output for compatible terminals, e.g. in the
62
+ help messages and in the bug report.
63
+ - New documentation page on event pairs, covering `print_pairs`,
64
+ `plot_pair` and `dedup_events`.
65
+
5
66
  ## [0.8.3] - 2026-06-19
6
67
 
7
68
  Note: you should run `requake update_config` to update your config file
@@ -206,6 +267,7 @@ Mostly a bugfix release with a slightly improved trace plotting.
206
267
 
207
268
  - Initial release, not yet feature complete
208
269
 
270
+ [0.8.4]: https://github.com/SeismicSource/requake/compare/v0.8.3...v0.8.4
209
271
  [0.8.3]: https://github.com/SeismicSource/requake/compare/v0.8.2...v0.8.3
210
272
  [0.8.2]: https://github.com/SeismicSource/requake/compare/v0.8.1...v0.8.2
211
273
  [0.8.1]: https://github.com/SeismicSource/requake/compare/v0.8...v0.8.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: requake
3
- Version: 0.8.3
3
+ Version: 0.8.4
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
@@ -33,11 +33,13 @@ Requires-Dist: argcomplete
33
33
  Requires-Dist: tqdm
34
34
  Requires-Dist: tabulate
35
35
  Requires-Dist: pygments
36
+ Provides-Extra: folium
37
+ Requires-Dist: folium; extra == "folium"
36
38
  Dynamic: description
37
39
  Dynamic: description-content-type
38
40
  Dynamic: license-file
39
41
 
40
- <img src="https://cdn.jsdelivr.net/gh/SeismicSource/requake@407e6c96ec3574f6e3743741763ca5b7c4daf914/imgs/Requake_logo.svg" width="400">
42
+ <img src="https://cdn.jsdelivr.net/gh/SeismicSource/requake@5cd818d88427140b2705841c2046f72b10612d72/imgs/Requake_logo.svg" width="400">
41
43
 
42
44
  # Requake
43
45
 
@@ -167,6 +169,8 @@ Different commands are available:
167
169
  print_catalog print the event catalog to screen
168
170
  scan_catalog scan an existing catalog for earthquake pairs
169
171
  print_pairs print pairs to screen
172
+ dedup_events detect and flag duplicate events in the catalog from
173
+ close event pairs
170
174
  plot_pair plot traces for a given event pair
171
175
  build_families build families of repeating earthquakes from a catalog
172
176
  of pairs
@@ -276,7 +280,7 @@ You can also cite the following abstract presented at the
276
280
  [docs-link]: https://requake.readthedocs.io/en/latest/?badge=latest
277
281
  [changelog-badge]: https://img.shields.io/badge/Changelog-136CB6.svg
278
282
  [changelog-link]: https://github.com/SeismicSource/requake/blob/main/CHANGELOG.md
279
- [DOI-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.10832203.svg
283
+ [DOI-badge]: https://img.shields.io/badge/DOI-10.5281%2Fzenodo.10832203-blue.svg
280
284
  [DOI-link]: https://doi.org/10.5281/zenodo.10832203
281
285
  [Requake GitHub repository]: https://github.com/SeismicSource/requake
282
286
 
@@ -128,6 +128,8 @@ Different commands are available:
128
128
  print_catalog print the event catalog to screen
129
129
  scan_catalog scan an existing catalog for earthquake pairs
130
130
  print_pairs print pairs to screen
131
+ dedup_events detect and flag duplicate events in the catalog from
132
+ close event pairs
131
133
  plot_pair plot traces for a given event pair
132
134
  build_families build families of repeating earthquakes from a catalog
133
135
  of pairs
@@ -237,7 +239,7 @@ You can also cite the following abstract presented at the
237
239
  [docs-link]: https://requake.readthedocs.io/en/latest/?badge=latest
238
240
  [changelog-badge]: https://img.shields.io/badge/Changelog-136CB6.svg
239
241
  [changelog-link]: https://github.com/SeismicSource/requake/blob/main/CHANGELOG.md
240
- [DOI-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.10832203.svg
242
+ [DOI-badge]: https://img.shields.io/badge/DOI-10.5281%2Fzenodo.10832203-blue.svg
241
243
  [DOI-link]: https://doi.org/10.5281/zenodo.10832203
242
244
  [Requake GitHub repository]: https://github.com/SeismicSource/requake
243
245
 
@@ -38,6 +38,11 @@ dependencies = [
38
38
  "pygments",
39
39
  ]
40
40
 
41
+ [project.optional-dependencies]
42
+ folium = [
43
+ "folium",
44
+ ]
45
+
41
46
  [project.urls]
42
47
  Homepage = "https://requake.seismicsource.org"
43
48
  Source = "https://github.com/SeismicSource/requake"
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2026-06-19T10:48:17+0200",
11
+ "date": "2026-10-23T18:15:54+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "407e6c96ec3574f6e3743741763ca5b7c4daf914",
15
- "version": "0.8.3"
14
+ "full-revisionid": "5cd818d88427140b2705841c2046f72b10612d72",
15
+ "version": "0.8.4"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -26,7 +26,8 @@ class RequakeEvent():
26
26
  def __init__(self, evid=None, orig_time=None, lon=None, lat=None,
27
27
  depth=None, mag_type=None, mag=None, author=None,
28
28
  catalog=None, contributor=None, contributor_id=None,
29
- mag_author=None, location_name=None, trace_id=None):
29
+ mag_author=None, location_name=None, trace_id=None,
30
+ duplicate=False):
30
31
  """Initialize an event container."""
31
32
  self.evid = evid
32
33
  self.orig_time = orig_time
@@ -42,6 +43,7 @@ class RequakeEvent():
42
43
  self.mag_author = mag_author
43
44
  self.location_name = location_name
44
45
  self.trace_id = trace_id
46
+ self.duplicate = duplicate
45
47
  self.correlations = {}
46
48
 
47
49
  def __eq__(self, other):
@@ -10,7 +10,8 @@ Print the event catalog to screen.
10
10
  (https://www.gnu.org/licenses/gpl-3.0-standalone.html)
11
11
  """
12
12
  import logging
13
- from ..config import rq_exit, generic_printer
13
+ from ..config import rq_exit
14
+ from ..config.generic_printer import _display_table
14
15
  from .catalog import read_stored_catalog
15
16
  logger = logging.getLogger(__name__.rsplit('.', maxsplit=1)[-1])
16
17
 
@@ -30,7 +31,8 @@ def print_catalog():
30
31
  ('latitude', '.4f'),
31
32
  ('depth\n(km)', '.3f'),
32
33
  ('mag\ntype', None),
33
- ('mag', '.1f')
34
+ ('mag', '.1f'),
35
+ ('dup', None)
34
36
  ]
35
37
  rows = [
36
38
  [
@@ -40,8 +42,14 @@ def print_catalog():
40
42
  ev.lat,
41
43
  ev.depth,
42
44
  ev.mag_type,
43
- ev.mag
45
+ ev.mag,
46
+ ev.duplicate
44
47
  ]
45
48
  for ev in catalog
46
49
  ]
47
- generic_printer(rows, headers_fmt)
50
+ _display_table(
51
+ headers_fmt, rows,
52
+ row_label='Events',
53
+ copy_label='evid',
54
+ detail_title='Event Details',
55
+ )
@@ -13,6 +13,7 @@ import os
13
13
  import logging
14
14
  import contextlib
15
15
  from ..config import config, rq_exit
16
+ from ..config.utils import run_with_spinner
16
17
  from ..database.db import get_db_path
17
18
  from ..database.catalog import write_catalog as write_stored_catalog
18
19
  from .catalog import RequakeEvent, RequakeCatalog
@@ -120,11 +121,14 @@ def read_catalog():
120
121
  f'{nevs_read:n} events read from db file '
121
122
  f'{get_db_path()}'
122
123
  )
123
- logger.info('Reading catalog...')
124
+ logger.debug('Reading catalog...')
124
125
  input_cat_file = config.args.catalog_file
125
126
  if input_cat_file is not None:
126
127
  try:
127
- catalog += _read_catalog_from_file()
128
+ catalog += run_with_spinner(
129
+ 'Reading catalog...',
130
+ _read_catalog_from_file,
131
+ )
128
132
  # Filter catalog based on configuration
129
133
  catalog = _filter_catalog(catalog)
130
134
  except FileNotFoundError:
@@ -15,6 +15,7 @@ import urllib.request
15
15
  from obspy import UTCDateTime
16
16
  from obspy.clients.fdsn.header import URL_MAPPINGS
17
17
  from ..config import config
18
+ from ..config.utils import run_with_spinner
18
19
  from .catalog import RequakeCatalog, RequakeEvent
19
20
  logger = logging.getLogger(__name__.rsplit('.', maxsplit=1)[-1])
20
21
 
@@ -110,7 +111,19 @@ def read_catalog_from_fdsnws():
110
111
  :return: Event catalog.
111
112
  :rtype: requake.catalog.RequakeCatalog
112
113
  """
113
- logger.info('Downloading events from FDSN web services...')
114
+ download_message = 'Downloading events from FDSN web services...'
115
+ logger.debug(download_message)
116
+ event_list = run_with_spinner(
117
+ download_message,
118
+ _download_catalog_events,
119
+ )
120
+ catalog = RequakeCatalog(event_list)
121
+ logger.info(f'{len(catalog)} events downloaded')
122
+ return catalog
123
+
124
+
125
+ def _download_catalog_events():
126
+ """Download events for all the configured FDSN web services."""
114
127
  cat_info = zip(
115
128
  config.catalog_fdsn_event_urls,
116
129
  config.catalog_start_times,
@@ -135,6 +148,4 @@ def read_catalog_from_fdsnws():
135
148
  f'Unable to download events from {url} for period '
136
149
  f'{start_time} - {end_time}. {msg}'
137
150
  )
138
- catalog = RequakeCatalog(event_list)
139
- logger.info(f'{len(catalog)} events downloaded')
140
- return catalog
151
+ return event_list
@@ -0,0 +1,208 @@
1
+ # -*- coding: utf8 -*-
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+ """
4
+ Generic printer functions 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
+ import sys
13
+ import os
14
+ import csv
15
+ import contextlib
16
+ import logging
17
+ from tabulate import tabulate
18
+ from ..terminal import color_enabled, style
19
+ from .config import config
20
+ logger = logging.getLogger(__name__.rsplit('.', maxsplit=1)[-1])
21
+
22
+ # Characters appearing in a ``tabulate`` separator (or divider) line.
23
+ _TABLE_SEPARATOR_CHARS = frozenset('-|=+: ')
24
+
25
+
26
+ def _is_separator_line(line):
27
+ """Return True if ``line`` is a table separator (or divider) line."""
28
+ stripped = line.strip()
29
+ return bool(stripped) and set(stripped) <= _TABLE_SEPARATOR_CHARS
30
+
31
+
32
+ def _style_table(text, has_header=True):
33
+ """
34
+ Colorize a plain-text table produced by :func:`tabulate`.
35
+
36
+ Header lines are highlighted in bold cyan, separator lines are
37
+ dimmed, and data rows are left untouched.
38
+
39
+ :param text: table text
40
+ :type text: str
41
+ :param has_header: whether the table starts with a header
42
+ :type has_header: bool
43
+
44
+ :return: colorized table text
45
+ :rtype: str
46
+ """
47
+ if not color_enabled():
48
+ return text
49
+ lines = text.split('\n')
50
+ separator_index = next(
51
+ (i for i, line in enumerate(lines) if _is_separator_line(line)),
52
+ None,
53
+ )
54
+ if separator_index is None:
55
+ separator_index = 1 if has_header else 0
56
+ styled_lines = []
57
+ for index, line in enumerate(lines):
58
+ if _is_separator_line(line):
59
+ styled_lines.append(style(line, 'dim'))
60
+ elif has_header and index < separator_index:
61
+ styled_lines.append(style(line, 'bold', 'cyan'))
62
+ else:
63
+ styled_lines.append(line)
64
+ return '\n'.join(styled_lines)
65
+
66
+
67
+ def _drop_header_block(text):
68
+ """
69
+ Return ``text`` without the table header and its separator line.
70
+
71
+ Used for continuation batches, which must not repeat the header
72
+ but must keep all their data rows.
73
+
74
+ :param text: table text
75
+ :type text: str
76
+ :return: table text without the header block
77
+ :rtype: str
78
+ """
79
+ lines = text.split('\n')
80
+ separator_index = next(
81
+ (i for i, line in enumerate(lines) if _is_separator_line(line)),
82
+ None,
83
+ )
84
+ if separator_index is None:
85
+ return '\n'.join(lines[1:])
86
+ return '\n'.join(lines[separator_index + 1:])
87
+
88
+
89
+ def generic_printer(rows, headers_fmt, print_headers=True):
90
+ """
91
+ Print rows using the configured output format.
92
+
93
+ :param rows: Rows to print.
94
+ :type rows: list of rows
95
+ :param headers_fmt: Headers and format strings.
96
+ :type headers_fmt: list of tuples of str
97
+ """
98
+ headers = [h[0] for h in headers_fmt]
99
+ # Replace None with a safe default so tabulate never receives
100
+ # None as a format specifier (crashes on float columns).
101
+ floatfmt = [
102
+ h[1] if h[1] is not None else '.6g' for h in headers_fmt
103
+ ]
104
+ tablefmt = config.args.format
105
+ if tablefmt == 'csv':
106
+ writer = csv.writer(sys.stdout)
107
+ if print_headers:
108
+ # replace newlines with spaces in headers
109
+ headers = [h.replace('\n', ' ') for h in headers]
110
+ writer.writerow(headers)
111
+ elif tablefmt == 'markdown':
112
+ headers = [h.replace('\n', '<br>') for h in headers]
113
+ if tablefmt == 'csv':
114
+ try:
115
+ writer.writerows(rows)
116
+ except BrokenPipeError:
117
+ # Redirect remaining output to devnull to avoid another
118
+ # BrokenPipeError at shutdown
119
+ devnull = os.open(os.devnull, os.O_WRONLY)
120
+ os.dup2(devnull, sys.stdout.fileno())
121
+ else:
122
+ format_dict = {
123
+ 'simple': 'simple',
124
+ 'markdown': 'github'
125
+ }
126
+ tablefmt = format_dict[config.args.format]
127
+ with contextlib.suppress(BrokenPipeError):
128
+ kwargs = {
129
+ 'headers': headers,
130
+ 'floatfmt': floatfmt,
131
+ 'tablefmt': tablefmt
132
+ }
133
+ line = tabulate(rows, **kwargs)
134
+ if not print_headers:
135
+ line = _drop_header_block(line)
136
+ if not line:
137
+ return
138
+ print(_style_table(line, has_header=print_headers))
139
+
140
+
141
+ def _display_table(headers_fmt, rows=None, data_source=None,
142
+ row_label='Rows', copy_label=None,
143
+ copy_fn=None, detail_title='Row Details',
144
+ print_headers=True):
145
+ """
146
+ Display a table using the interactive curses pager.
147
+
148
+ When stdout is a TTY (and ``--no-pager`` is not set), the
149
+ interactive pager is used. Otherwise the table is printed
150
+ to stdout via :func:`generic_printer`.
151
+
152
+ :param headers_fmt: Headers and format strings
153
+ (list of (name, fmt) tuples).
154
+ :param rows: In-memory rows (list of lists). Ignored when
155
+ *data_source* is given.
156
+ :param data_source: Optional :class:`~requake.pager.DataSource`
157
+ for database-backed pagination.
158
+ :param row_label: Label for rows in the pager status bar.
159
+ :param copy_label: Label for the copy-to-clipboard message.
160
+ :param copy_fn: Optional callable ``copy_fn(row) -> str`` for
161
+ custom copy behaviour.
162
+ :param detail_title: Title for the detail popup.
163
+ :param print_headers: Whether to include the header row
164
+ (used for iterative printing fallback).
165
+ """
166
+ # Determine whether to use the pager
167
+ use_pager = sys.stdout.isatty()
168
+ if use_pager:
169
+ no_pager = getattr(config.args, 'no_pager', False)
170
+ if no_pager:
171
+ use_pager = False
172
+
173
+ if use_pager:
174
+ from ..pager import (
175
+ display_table_pager,
176
+ PagerException,
177
+ ListDataSource,
178
+ )
179
+ with contextlib.suppress(PagerException):
180
+ if data_source is not None:
181
+ ds = data_source
182
+ else:
183
+ fields = [h[0].replace('\n', ' ') for h in headers_fmt]
184
+ ds = ListDataSource(fields, rows or [])
185
+ display_table_pager(
186
+ ds,
187
+ row_label=row_label,
188
+ copy_label=copy_label,
189
+ copy_fn=copy_fn,
190
+ detail_title=detail_title,
191
+ )
192
+ return
193
+ # Fallback: plain-text output via generic_printer
194
+ if data_source is not None:
195
+ # For DB-backed sources, stream rows via generic_printer
196
+ fields = data_source.fields
197
+ total = data_source.total_count
198
+ batch_size = 1000
199
+ for offset in range(0, total, batch_size):
200
+ batch = data_source.get_rows(offset, batch_size, None, True)
201
+ # Rebuild headers_fmt for generic_printer
202
+ batch_headers_fmt = [(f, None) for f in fields]
203
+ generic_printer(
204
+ batch, batch_headers_fmt,
205
+ print_headers=(offset == 0 and print_headers)
206
+ )
207
+ else:
208
+ generic_printer(rows, headers_fmt, print_headers)