isgri 0.5.1__tar.gz → 0.6.0__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 (38) hide show
  1. {isgri-0.5.1 → isgri-0.6.0}/PKG-INFO +5 -25
  2. {isgri-0.5.1 → isgri-0.6.0}/README.md +4 -24
  3. {isgri-0.5.1 → isgri-0.6.0}/pyproject.toml +2 -2
  4. isgri-0.6.0/src/isgri/__version__.py +1 -0
  5. {isgri-0.5.1 → isgri-0.6.0}/src/isgri/catalog/scwquery.py +42 -0
  6. isgri-0.6.0/src/isgri/cli/__init__.py +1 -0
  7. isgri-0.5.1/src/isgri/cli.py → isgri-0.6.0/src/isgri/cli/main.py +168 -224
  8. isgri-0.6.0/src/isgri/cli/query.py +172 -0
  9. {isgri-0.5.1 → isgri-0.6.0}/tests/catalog/test_scwquery.py +119 -19
  10. isgri-0.5.1/tests/test_cli.py → isgri-0.6.0/tests/cli/test_main.py +71 -90
  11. isgri-0.5.1/src/isgri/__version__.py +0 -1
  12. {isgri-0.5.1 → isgri-0.6.0}/.gitignore +0 -0
  13. {isgri-0.5.1 → isgri-0.6.0}/.python-version +0 -0
  14. {isgri-0.5.1 → isgri-0.6.0}/LICENSE +0 -0
  15. {isgri-0.5.1 → isgri-0.6.0}/demo/data/255900280010_isgri_model.fits.gz +0 -0
  16. {isgri-0.5.1 → isgri-0.6.0}/demo/data/isgri_events.fits.gz +0 -0
  17. {isgri-0.5.1 → isgri-0.6.0}/demo/lightcurve_walkthrough.ipynb +0 -0
  18. {isgri-0.5.1 → isgri-0.6.0}/demo/scwquery_walkthrough.ipynb +0 -0
  19. {isgri-0.5.1 → isgri-0.6.0}/src/isgri/__init__.py +0 -0
  20. {isgri-0.5.1 → isgri-0.6.0}/src/isgri/catalog/__init__.py +0 -0
  21. {isgri-0.5.1 → isgri-0.6.0}/src/isgri/catalog/builder.py +0 -0
  22. {isgri-0.5.1 → isgri-0.6.0}/src/isgri/catalog/wcs.py +0 -0
  23. {isgri-0.5.1 → isgri-0.6.0}/src/isgri/config.py +0 -0
  24. {isgri-0.5.1 → isgri-0.6.0}/src/isgri/utils/__init__.py +0 -0
  25. {isgri-0.5.1 → isgri-0.6.0}/src/isgri/utils/file_loaders.py +0 -0
  26. {isgri-0.5.1 → isgri-0.6.0}/src/isgri/utils/lightcurve.py +0 -0
  27. {isgri-0.5.1 → isgri-0.6.0}/src/isgri/utils/pif.py +0 -0
  28. {isgri-0.5.1 → isgri-0.6.0}/src/isgri/utils/quality.py +0 -0
  29. {isgri-0.5.1 → isgri-0.6.0}/src/isgri/utils/time_conversion.py +0 -0
  30. {isgri-0.5.1 → isgri-0.6.0}/tests/__init__.py +0 -0
  31. {isgri-0.5.1 → isgri-0.6.0}/tests/catalog/test_wcs.py +0 -0
  32. {isgri-0.5.1 → isgri-0.6.0}/tests/test_config.py +0 -0
  33. {isgri-0.5.1 → isgri-0.6.0}/tests/utils/__init__.py +0 -0
  34. {isgri-0.5.1 → isgri-0.6.0}/tests/utils/test_file_loaders.py +0 -0
  35. {isgri-0.5.1 → isgri-0.6.0}/tests/utils/test_lightcurve.py +0 -0
  36. {isgri-0.5.1 → isgri-0.6.0}/tests/utils/test_pif.py +0 -0
  37. {isgri-0.5.1 → isgri-0.6.0}/tests/utils/test_quality.py +0 -0
  38. {isgri-0.5.1 → isgri-0.6.0}/tests/utils/test_time_conversion.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: isgri
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Python package for INTEGRAL IBIS/ISGRI lightcurve analysis
5
5
  Author: Dominik Patryk Pacholski
6
6
  License: MIT
@@ -55,6 +55,9 @@ pip install isgri
55
55
  # Configure default paths (once)
56
56
  isgri config-set --catalog ~/data/scw_catalog.fits
57
57
 
58
+ # Interactive method
59
+ isgri query
60
+
58
61
  # Query by time range
59
62
  isgri query --tstart 2010-01-01 --tstop 2010-12-31
60
63
 
@@ -138,27 +141,4 @@ Local config file `isgri_config.toml` in current directory overrides global conf
138
141
  - **CLI Reference**: Run `isgri --help` or `isgri <command> --help`
139
142
  - **Catalog Tutorial**: [scwquery_walkthrough.ipynb](https://github.com/dominp/isgri/blob/main/demo/scwquery_walkthrough.ipynb)
140
143
  - **Light Curve Tutorial**: [lightcurve_walkthrough.ipynb](https://github.com/dominp/isgri/blob/main/demo/lightcurve_walkthrough.ipynb)
141
- - **API Reference**: Use `help()` in Python or see docstrings
142
-
143
- ## Project Structure
144
-
145
- ```
146
- isgri/
147
- ├── catalog/ # SCW catalog query tools
148
- │ ├── scwquery.py # Main query interface
149
- │ └── wcs.py # Coordinate transformations
150
- ├── utils/ # Light curve analysis utilities
151
- │ ├── lightcurve.py # Light curve class
152
- │ ├── quality.py # Quality metrics
153
- │ ├── pif.py # PIF tools
154
- │ ├── file_loaders.py
155
- │ └── time_conversion.py
156
- ├── config.py # Configuration management
157
- └── cli.py # Command line interface
158
- ```
159
-
160
- ## Requirements
161
-
162
- - Python ≥ 3.10
163
- - astropy
164
- - numpy
144
+ - **API Reference**: Use `help()` in Python or see docstrings
@@ -39,6 +39,9 @@ pip install isgri
39
39
  # Configure default paths (once)
40
40
  isgri config-set --catalog ~/data/scw_catalog.fits
41
41
 
42
+ # Interactive method
43
+ isgri query
44
+
42
45
  # Query by time range
43
46
  isgri query --tstart 2010-01-01 --tstop 2010-12-31
44
47
 
@@ -122,27 +125,4 @@ Local config file `isgri_config.toml` in current directory overrides global conf
122
125
  - **CLI Reference**: Run `isgri --help` or `isgri <command> --help`
123
126
  - **Catalog Tutorial**: [scwquery_walkthrough.ipynb](https://github.com/dominp/isgri/blob/main/demo/scwquery_walkthrough.ipynb)
124
127
  - **Light Curve Tutorial**: [lightcurve_walkthrough.ipynb](https://github.com/dominp/isgri/blob/main/demo/lightcurve_walkthrough.ipynb)
125
- - **API Reference**: Use `help()` in Python or see docstrings
126
-
127
- ## Project Structure
128
-
129
- ```
130
- isgri/
131
- ├── catalog/ # SCW catalog query tools
132
- │ ├── scwquery.py # Main query interface
133
- │ └── wcs.py # Coordinate transformations
134
- ├── utils/ # Light curve analysis utilities
135
- │ ├── lightcurve.py # Light curve class
136
- │ ├── quality.py # Quality metrics
137
- │ ├── pif.py # PIF tools
138
- │ ├── file_loaders.py
139
- │ └── time_conversion.py
140
- ├── config.py # Configuration management
141
- └── cli.py # Command line interface
142
- ```
143
-
144
- ## Requirements
145
-
146
- - Python ≥ 3.10
147
- - astropy
148
- - numpy
128
+ - **API Reference**: Use `help()` in Python or see docstrings
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "isgri"
7
- version = "0.5.1"
7
+ version = "0.6.0"
8
8
  authors = [{name = "Dominik Patryk Pacholski"}]
9
9
  license = {text = "MIT"}
10
10
  description = "Python package for INTEGRAL IBIS/ISGRI lightcurve analysis"
@@ -23,4 +23,4 @@ dependencies = [
23
23
  where = ["src"]
24
24
 
25
25
  [project.scripts]
26
- isgri = "isgri.cli:main"
26
+ isgri = "isgri.cli.main:main"
@@ -0,0 +1 @@
1
+ __version__ = "0.6.0"
@@ -344,6 +344,48 @@ class ScwQuery:
344
344
  combined_mask &= filt.mask
345
345
  return self.catalog[combined_mask]
346
346
 
347
+ def write(
348
+ self, output_path: Union[str, Path], swid_only: Optional[str] = False, overwrite: Optional[bool] = False
349
+ ):
350
+ """Write filtered catalog to the file.
351
+
352
+ Parameters
353
+ ----------
354
+ output_path : str or Path
355
+ Path to output file. Format auto-detected from extension:
356
+ - .txt: SWID list (one per line)
357
+ - .fits: FITS table
358
+ - .csv: CSV table
359
+ swid_only : bool, optional
360
+ If True, write only SWID list regardless of extension
361
+ overwrite : bool, optional
362
+ Whether to overwrite existing file, by default False
363
+
364
+ Raises
365
+ ------
366
+ FileExistsError
367
+ If file exists and overwrite=False
368
+
369
+ Examples
370
+ --------
371
+ >>> query.time(tstart=3000).write("filtered_scws.fits", overwrite=True)
372
+ >>> query.quality(max_chi=2.0).write("good_scws.txt", swid_only=True)
373
+ >>> query.write("scws.csv")
374
+ >>> query.write("output", swid_only=True) # Force SWID list regardless of extension
375
+ """
376
+
377
+ results = self.get()
378
+ if isinstance(output_path, str):
379
+ output_path = Path(output_path)
380
+ if output_path.exists() and not overwrite:
381
+ raise FileExistsError(f"Output file already exists: {output_path}")
382
+ if swid_only or output_path.suffix == ".txt":
383
+ with open(output_path, "w") as f:
384
+ for swid in results["SWID"]:
385
+ f.write(f"{swid}\n")
386
+ else:
387
+ results.write(output_path, overwrite=overwrite)
388
+
347
389
  def count(self) -> int:
348
390
  """
349
391
  Count SCWs matching current filters.
@@ -0,0 +1 @@
1
+ from .main import main
@@ -1,224 +1,168 @@
1
- import click
2
- from pathlib import Path
3
- from .catalog import ScwQuery
4
- from .__version__ import __version__
5
- from .config import Config
6
-
7
-
8
- @click.group()
9
- @click.version_option(version=__version__)
10
- def main():
11
- """ISGRI - INTEGRAL/ISGRI data analysis toolkit."""
12
- pass
13
-
14
-
15
- def parse_time(time_str):
16
- """
17
- Parse time string as IJD float or ISO date string.
18
-
19
- Parameters
20
- ----------
21
- time_str : str or None
22
- Time as "YYYY-MM-DD" or IJD number
23
-
24
- Returns
25
- -------
26
- float or str or None
27
- Parsed time value
28
- """
29
- if time_str is None:
30
- return None
31
-
32
- try:
33
- return float(time_str)
34
- except ValueError:
35
- return time_str
36
-
37
-
38
- @main.command()
39
- @click.option("--catalog", type=click.Path(), help="Path to catalog FITS file. If not provided, uses config value.")
40
- @click.option("--tstart", help="Start time (YYYY-MM-DD or IJD)")
41
- @click.option("--tstop", help="Stop time (YYYY-MM-DD or IJD)")
42
- @click.option("--ra", type=float, help="Right ascension (degrees)")
43
- @click.option("--dec", type=float, help="Declination (degrees)")
44
- @click.option("--fov", type=click.Choice(["full", "any"]), default="any", help="Field of view mode")
45
- @click.option("--max-chi", type=float, help="Maximum chi-squared value")
46
- @click.option("--chi-type", type=click.Choice(["RAW", "CUT", "GTI"]), default="CUT", help="Type of chi-squared value")
47
- @click.option("--revolution", "-r", help="Revolution number")
48
- @click.option("--output", "-o", type=click.Path(), help="Output file (.fits or .csv)")
49
- @click.option("--list-swids", is_flag=True, help="Only output SWID list")
50
- @click.option("--count", is_flag=True, help="Only show count")
51
- def query(catalog, tstart, tstop, ra, dec, fov, max_chi, chi_type, revolution, output, list_swids, count):
52
- """
53
- Query INTEGRAL science window catalog.
54
-
55
- If no catalog path is provided, uses the default from configuration.
56
- Multiple filters can be combined.
57
-
58
- Examples:
59
- Query by time range (IJD):
60
-
61
- isgri query --tstart 3000 --tstop 3100
62
-
63
- Query by time range (ISO date):
64
-
65
- isgri query --tstart 2010-01-01 --tstop 2010-12-31
66
-
67
- Query by sky position:
68
-
69
- isgri query --ra 83.63 --dec 22.01 --fov full
70
-
71
- Query with quality cut:
72
-
73
- isgri query --max-chi 2.0 --chi-type CUT
74
-
75
- Save results to file:
76
-
77
- isgri query --tstart 3000 --tstop 3100 --output results.fits
78
-
79
- Get only SWID list:
80
-
81
- isgri query --tstart 3000 --tstop 3100 --list-swids
82
-
83
- Count matching science windows:
84
-
85
- isgri query --ra 83.63 --dec 22.01 --count
86
- """
87
- try:
88
- # Load catalog
89
- q = ScwQuery(catalog)
90
- initial_count = len(q.catalog)
91
-
92
- # Parse times (handle both IJD and ISO)
93
- tstart = parse_time(tstart)
94
- tstop = parse_time(tstop)
95
-
96
- # Apply filters
97
- if tstart or tstop:
98
- q = q.time(tstart=tstart, tstop=tstop)
99
-
100
- if ra is not None and dec is not None:
101
- q = q.position(ra=ra, dec=dec, fov_mode=fov)
102
-
103
- if max_chi is not None:
104
- q = q.quality(max_chi=max_chi, chi_type=chi_type)
105
-
106
- if revolution:
107
- q = q.revolution(revolution)
108
-
109
- results = q.get()
110
-
111
- if count:
112
- click.echo(len(results))
113
-
114
- elif list_swids:
115
- for swid in results["SWID"]:
116
- click.echo(swid)
117
-
118
- elif output:
119
- if output.endswith(".csv"):
120
- results.write(output, format="ascii.csv", overwrite=True)
121
- else:
122
- results.write(output, format="fits", overwrite=True)
123
- click.echo(f"Saved {len(results)} SCWs to {output}")
124
-
125
- else:
126
- click.echo(f"Found {len(results)}/{initial_count} SCWs")
127
- if len(results) > 0:
128
- display_cols = ["SWID", "TSTART", "TSTOP", "RA_SCX", "DEC_SCX"]
129
- chi_col = f"{chi_type}_CHI" if chi_type != "RAW" else "CHI"
130
- if chi_col in results.colnames:
131
- display_cols.append(chi_col)
132
- click.echo(results[display_cols][:10])
133
- if len(results) > 10:
134
- click.echo(f"... and {len(results) - 10} more")
135
-
136
- except Exception as e:
137
- click.echo(f"Error: {e}", err=True)
138
- raise click.Abort()
139
-
140
-
141
- @main.command()
142
- def config():
143
- """
144
- Show current configuration.
145
-
146
- Displays paths to config file, archive directory, and catalog file,
147
- along with their existence status.
148
- """
149
- cfg = Config()
150
-
151
- click.echo(f"Config file: {cfg.path}")
152
- click.echo(f" Exists: {cfg.path.exists()}")
153
- click.echo()
154
-
155
- archive = cfg.archive_path
156
- click.echo(f"Archive path: {archive if archive else '(not set)'}")
157
- if archive:
158
- click.echo(f" Exists: {archive.exists()}")
159
-
160
- try:
161
- catalog = cfg.catalog_path
162
- click.echo(f"Catalog path: {catalog if catalog else '(not set)'}")
163
- if catalog:
164
- click.echo(f" Exists: {catalog.exists()}")
165
- except FileNotFoundError as e:
166
- click.echo(f"Catalog path: (configured but file not found)")
167
- click.echo(f" Error: {e}")
168
-
169
-
170
- @main.command()
171
- @click.option("--archive", type=click.Path(), help="INTEGRAL archive directory path")
172
- @click.option("--catalog", type=click.Path(), help="Catalog FITS file path")
173
- def config_set(archive, catalog):
174
- """
175
- Set configuration values.
176
-
177
- Set default paths for archive directory and/or catalog file.
178
- Paths are expanded (~ becomes home directory) and resolved to absolute paths.
179
- Warns if path doesn't exist but allows setting anyway.
180
-
181
- Examples:
182
-
183
- Set archive path:
184
-
185
- isgri config-set --archive /anita/archivio/
186
-
187
- Set catalog path:
188
-
189
- isgri config-set --catalog ~/data/scw_catalog.fits
190
-
191
- Set both at once:
192
-
193
- isgri config-set --archive /anita/archivio/ --catalog ~/data/scw_catalog.fits
194
- """
195
- if not archive and not catalog:
196
- click.echo("Error: Specify at least one option (--archive or --catalog)", err=True)
197
- raise click.Abort()
198
-
199
- cfg = Config()
200
-
201
- if archive:
202
- archive_path = Path(archive).expanduser().resolve()
203
- if not archive_path.exists():
204
- click.echo(f"Warning: Archive path does not exist: {archive_path}", err=True)
205
- if not click.confirm("Set anyway?"):
206
- raise click.Abort()
207
- cfg.set(archive_path=archive_path)
208
- click.echo(f"✓ Archive path set to: {archive_path}")
209
-
210
- if catalog:
211
- catalog_path = Path(catalog).expanduser().resolve()
212
- if not catalog_path.exists():
213
- click.echo(f"Warning: Catalog file does not exist: {catalog_path}", err=True)
214
- if not click.confirm("Set anyway?"):
215
- raise click.Abort()
216
- cfg.set(catalog_path=catalog_path)
217
- click.echo(f"✓ Catalog path set to: {catalog_path}")
218
-
219
- click.echo()
220
- click.echo(f"Configuration saved to: {cfg.path}")
221
-
222
-
223
- if __name__ == "__main__":
224
- main()
1
+ import click
2
+ from pathlib import Path
3
+ from ..catalog import ScwQuery
4
+ from ..__version__ import __version__
5
+ from ..config import Config
6
+ from .query import query_direct, query_interactive
7
+
8
+
9
+ @click.group()
10
+ @click.version_option(version=__version__)
11
+ def main():
12
+ """ISGRI - INTEGRAL/ISGRI data analysis toolkit."""
13
+ pass
14
+
15
+
16
+ @main.command()
17
+ @click.option("--catalog", type=click.Path(), help="Path to catalog FITS file. If not provided, uses config value.")
18
+ @click.option("--tstart", help="Start time (YYYY-MM-DD or IJD)")
19
+ @click.option("--tstop", help="Stop time (YYYY-MM-DD or IJD)")
20
+ @click.option("--ra", help="Right ascension (degrees or HH:MM:SS)")
21
+ @click.option("--dec", help="Declination (degrees or DD:MM:SS)")
22
+ @click.option("--radius", type=float, help="Angular separation (degrees)")
23
+ @click.option("--fov", type=click.Choice(["full", "any"]), default="any", help="Field of view mode")
24
+ @click.option("--max-chi", type=float, help="Maximum chi-squared value")
25
+ @click.option("--chi-type", type=click.Choice(["RAW", "CUT", "GTI"]), default="CUT", help="Type of chi-squared value")
26
+ @click.option("--revolution", help="Revolution number")
27
+ @click.option(
28
+ "--output", "-o", type=click.Path(), help="Output file (.fits or .csv or any if --list-swids or --count)"
29
+ )
30
+ @click.option("--list-swids", is_flag=True, help="Only output SWID list")
31
+ @click.option("--count", is_flag=True, help="Only show count")
32
+ def query(catalog, tstart, tstop, ra, dec, radius, fov, max_chi, chi_type, revolution, output, list_swids, count):
33
+ """
34
+ Query INTEGRAL science window catalog.
35
+
36
+ If no catalog path is provided, uses the default from configuration.
37
+ Multiple filters can be combined.
38
+
39
+ Examples:
40
+ Query by time range (IJD):
41
+
42
+ isgri query --tstart 3000 --tstop 3100
43
+
44
+ Query by time range (ISO date):
45
+
46
+ isgri query --tstart 2010-01-01 --tstop 2010-12-31
47
+
48
+ Query by sky position:
49
+
50
+ isgri query --ra 83.63 --dec 22.01 --fov full
51
+ isgri query --ra 83.63 --dec 22.01 --radius 5.0
52
+
53
+ Query with quality cut:
54
+
55
+ isgri query --max-chi 2.0 --chi-type CUT
56
+
57
+ Save results to file:
58
+
59
+ isgri query --tstart 3000 --tstop 3100 --output results.fits
60
+
61
+ Get only SWID list:
62
+
63
+ isgri query --tstart 3000 --tstop 3100 --list-swids
64
+
65
+ Count matching science windows:
66
+
67
+ isgri query --ra 83.63 --dec 22.01 --count
68
+ """
69
+ if catalog is None:
70
+ cfg = Config()
71
+ catalog = cfg.catalog_path
72
+
73
+ if not catalog:
74
+ click.echo("Error: No catalog configured", err=True)
75
+ raise click.Abort()
76
+
77
+ if any(param is not None for param in [tstart, tstop, ra, dec, radius, max_chi, revolution]):
78
+ query_direct(
79
+ catalog, tstart, tstop, ra, dec, radius, fov, max_chi, chi_type, revolution, output, list_swids, count
80
+ )
81
+ else:
82
+ query_interactive(catalog)
83
+
84
+
85
+ @main.command()
86
+ def config():
87
+ """
88
+ Show current configuration.
89
+
90
+ Displays paths to config file, archive directory, and catalog file,
91
+ along with their existence status.
92
+ """
93
+ cfg = Config()
94
+
95
+ click.echo(f"Config file: {cfg.path}")
96
+ click.echo(f" Exists: {cfg.path.exists()}")
97
+ click.echo()
98
+
99
+ archive = cfg.archive_path
100
+ click.echo(f"Archive path: {archive if archive else '(not set)'}")
101
+ if archive:
102
+ click.echo(f" Exists: {archive.exists()}")
103
+
104
+ try:
105
+ catalog = cfg.catalog_path
106
+ click.echo(f"Catalog path: {catalog if catalog else '(not set)'}")
107
+ if catalog:
108
+ click.echo(f" Exists: {catalog.exists()}")
109
+ except FileNotFoundError as e:
110
+ click.echo(f"Catalog path: (configured but file not found)")
111
+ click.echo(f" Error: {e}")
112
+
113
+
114
+ @main.command()
115
+ @click.option("--archive", type=click.Path(), help="INTEGRAL archive directory path")
116
+ @click.option("--catalog", type=click.Path(), help="Catalog FITS file path")
117
+ def config_set(archive, catalog):
118
+ """
119
+ Set configuration values.
120
+
121
+ Set default paths for archive directory and/or catalog file.
122
+ Paths are expanded (~ becomes home directory) and resolved to absolute paths.
123
+ Warns if path doesn't exist but allows setting anyway.
124
+
125
+ Examples:
126
+
127
+ Set archive path:
128
+
129
+ isgri config-set --archive /anita/archivio/
130
+
131
+ Set catalog path:
132
+
133
+ isgri config-set --catalog ~/data/scw_catalog.fits
134
+
135
+ Set both at once:
136
+
137
+ isgri config-set --archive /anita/archivio/ --catalog ~/data/scw_catalog.fits
138
+ """
139
+ if not archive and not catalog:
140
+ click.echo("Error: Specify at least one option (--archive or --catalog)", err=True)
141
+ raise click.Abort()
142
+
143
+ cfg = Config()
144
+
145
+ if archive:
146
+ archive_path = Path(archive).expanduser().resolve()
147
+ if not archive_path.exists():
148
+ click.echo(f"Warning: Archive path does not exist: {archive_path}", err=True)
149
+ if not click.confirm("Set anyway?"):
150
+ raise click.Abort()
151
+ cfg.set(archive_path=archive_path)
152
+ click.echo(f"✓ Archive path set to: {archive_path}")
153
+
154
+ if catalog:
155
+ catalog_path = Path(catalog).expanduser().resolve()
156
+ if not catalog_path.exists():
157
+ click.echo(f"Warning: Catalog file does not exist: {catalog_path}", err=True)
158
+ if not click.confirm("Set anyway?"):
159
+ raise click.Abort()
160
+ cfg.set(catalog_path=catalog_path)
161
+ click.echo(f"✓ Catalog path set to: {catalog_path}")
162
+
163
+ click.echo()
164
+ click.echo(f"Configuration saved to: {cfg.path}")
165
+
166
+
167
+ if __name__ == "__main__":
168
+ main()
@@ -0,0 +1,172 @@
1
+ import click
2
+ from pathlib import Path
3
+ from ..catalog import ScwQuery
4
+ from ..config import Config
5
+ from ..__version__ import __version__
6
+
7
+
8
+ def parse_time(time_str):
9
+ """
10
+ Parse time string as IJD float or ISO date string.
11
+
12
+ Parameters
13
+ ----------
14
+ time_str : str or None
15
+ Time as "YYYY-MM-DD" or IJD number
16
+
17
+ Returns
18
+ -------
19
+ float or str or None
20
+ Parsed time value
21
+ """
22
+ if time_str is None:
23
+ return None
24
+
25
+ try:
26
+ return float(time_str)
27
+ except ValueError:
28
+ return time_str
29
+
30
+
31
+ def parse_coord(coord):
32
+ """
33
+ Parse RA and Dec strings as float degrees or sexagesimal strings.
34
+
35
+ Parameters
36
+ ----------
37
+ coord : str or None
38
+ Coordinate as float degrees or sexagesimal string
39
+ Returns
40
+ -------
41
+ float or str or None
42
+ Parsed coordinate value
43
+ """
44
+ if coord is None:
45
+ return None
46
+
47
+ try:
48
+ return float(coord)
49
+ except ValueError:
50
+ return coord
51
+
52
+
53
+ def query_direct(
54
+ catalog_path, tstart, tstop, ra, dec, radius, fov, max_chi, chi_type, revolution, output, list_swids, count
55
+ ):
56
+ try:
57
+ q = ScwQuery(catalog_path)
58
+ initial_count = len(q.catalog)
59
+ # Parse times (handle both IJD and ISO)
60
+ tstart = parse_time(tstart)
61
+ tstop = parse_time(tstop)
62
+
63
+ # Apply filters
64
+ if tstart or tstop:
65
+ q = q.time(tstart=tstart, tstop=tstop)
66
+
67
+ if ra is not None and dec is not None:
68
+ ra = parse_coord(ra)
69
+ dec = parse_coord(dec)
70
+ if radius is not None:
71
+ q = q.position(ra=ra, dec=dec, radius=radius)
72
+ else:
73
+ q = q.position(ra=ra, dec=dec, fov_mode=fov)
74
+
75
+ if max_chi is not None:
76
+ q = q.quality(max_chi=max_chi, chi_type=chi_type)
77
+
78
+ if revolution:
79
+ q = q.revolution(revolution)
80
+
81
+ results = q.get()
82
+
83
+ if count:
84
+ click.echo(len(results))
85
+
86
+ elif list_swids:
87
+ for swid in results["SWID"]:
88
+ click.echo(swid)
89
+
90
+ elif output:
91
+ if output.endswith(".csv"):
92
+ results.write(output, format="ascii.csv", overwrite=True)
93
+ else:
94
+ results.write(output, format="fits", overwrite=True)
95
+ click.echo(f"Saved {len(results)} SCWs to {output}")
96
+
97
+ else:
98
+ click.echo(f"Found {len(results)}/{initial_count} SCWs")
99
+ if len(results) > 0:
100
+ display_cols = ["SWID", "TSTART", "TSTOP", "RA_SCX", "DEC_SCX"]
101
+ chi_col = f"{chi_type}_CHI" if chi_type != "RAW" else "CHI"
102
+ if chi_col in results.colnames:
103
+ display_cols.append(chi_col)
104
+ click.echo(results[display_cols][:10])
105
+ if len(results) > 10:
106
+ click.echo(f"... and {len(results) - 10} more")
107
+
108
+ except Exception as e:
109
+ click.echo(f"Error: {e}", err=True)
110
+ raise click.Abort()
111
+
112
+
113
+ def query_interactive(catalog_path):
114
+ """Run interactive query session."""
115
+ click.echo("=== Interactive Query Mode ===\n")
116
+
117
+ q = ScwQuery(catalog_path)
118
+ click.echo(f"Loaded {len(q.catalog)} SCWs")
119
+ click.echo("Type 'help' for commands\n")
120
+
121
+ while True:
122
+ try:
123
+ cmd = click.prompt("query>", default="").strip().lower()
124
+
125
+ if cmd in ("exit", "quit", "q"):
126
+ break
127
+ elif cmd == "help":
128
+ click.echo("Commands: time, pos, quality, show, reset, save, exit")
129
+ elif cmd == "time":
130
+ tstart = click.prompt("Start", default="", show_default=False)
131
+ tstop = click.prompt("Stop", default="", show_default=False)
132
+ tstart = parse_time(tstart) if tstart else None
133
+ tstop = parse_time(tstop) if tstop else None
134
+ q = q.time(tstart=tstart or None, tstop=tstop or None)
135
+ click.echo(f"→ {q.count()} SCWs")
136
+ elif cmd == "pos":
137
+ ra = click.prompt("RA")
138
+ dec = click.prompt("Dec")
139
+ mode = click.prompt("Mode", type=click.Choice(["fov", "radius"]), default="fov")
140
+ if mode == "radius":
141
+ radius = click.prompt("Radius (deg)", type=float, default=10.0)
142
+ q = q.position(ra=parse_coord(ra), dec=parse_coord(dec), radius=radius)
143
+ else:
144
+ fov_mode = click.prompt("FOV mode", type=click.Choice(["full", "any"]), default="any")
145
+ q = q.position(ra=parse_coord(ra), dec=parse_coord(dec), fov_mode=fov_mode)
146
+ click.echo(f"→ {q.count()} SCWs")
147
+
148
+ elif cmd == "quality":
149
+ max_chi = click.prompt("Max chi-squared", type=float)
150
+ chi_type = click.prompt("Chi type", type=click.Choice(["RAW", "CUT", "GTI"]), default="CUT")
151
+ q = q.quality(max_chi=max_chi, chi_type=chi_type)
152
+ click.echo(f"→ {q.count()} SCWs")
153
+
154
+ elif cmd == "show":
155
+ results = q.get()
156
+ click.echo(f"\n{len(results)} SCWs:")
157
+ click.echo(results[["SWID", "TSTART", "TSTOP"]])
158
+ elif cmd == "reset":
159
+ q = q.reset()
160
+ click.echo(f"→ {len(q.catalog)} SCWs")
161
+ elif cmd == "save":
162
+ only_scws = click.confirm("Save only SWID list?", default=False)
163
+ path = click.prompt("File")
164
+ q.write(path, overwrite=True, swid_only=only_scws)
165
+ click.echo(f"✓ Saved")
166
+ else:
167
+ click.echo(f"Unknown: {cmd}")
168
+
169
+ except KeyboardInterrupt:
170
+ click.echo("\nUse 'exit' to quit")
171
+ except Exception as e:
172
+ click.echo(f"Error: {e}", err=True)
@@ -377,31 +377,34 @@ def test_scwquery_uses_config(tmp_path, mock_catalog, monkeypatch):
377
377
  """Test ScwQuery uses config when no path provided."""
378
378
  config_path = tmp_path / "config.toml"
379
379
  cfg = Config(config_path)
380
- cfg.set(catalog_path=mock_catalog)
380
+ cfg.set(catalog_path=mock_catalog)
381
381
 
382
382
  monkeypatch.setattr(Config, "DEFAULT_PATH", config_path)
383
383
 
384
384
  query = ScwQuery()
385
385
  assert str(query.catalog_path) == mock_catalog
386
- assert len(query.catalog) == 1000
386
+ assert len(query.catalog) == 1000
387
+
387
388
 
388
389
  def test_scwquery_explicit_path_overrides_config(tmp_path, mock_catalog, monkeypatch):
389
390
  """Test explicit path overrides config."""
390
391
  # Create a second catalog
391
392
  n_scw = 50
392
- other_catalog = Table({
393
- "SWID": [f"{i:012d}" for i in range(n_scw)],
394
- "REVOL": np.random.randint(100, 500, n_scw),
395
- "TSTART": np.linspace(5000, 6000, n_scw),
396
- "TSTOP": np.linspace(5001, 6001, n_scw),
397
- "RA_SCX": np.random.uniform(0, 360, n_scw),
398
- "DEC_SCX": np.random.uniform(-80, 80, n_scw),
399
- "RA_SCZ": np.random.uniform(0, 360, n_scw),
400
- "DEC_SCZ": np.random.uniform(-80, 80, n_scw),
401
- "CHI": np.random.uniform(0.5, 5.0, n_scw),
402
- "CUT_CHI": np.random.uniform(0.5, 5.0, n_scw),
403
- "GTI_CHI": np.random.uniform(0.5, 5.0, n_scw),
404
- })
393
+ other_catalog = Table(
394
+ {
395
+ "SWID": [f"{i:012d}" for i in range(n_scw)],
396
+ "REVOL": np.random.randint(100, 500, n_scw),
397
+ "TSTART": np.linspace(5000, 6000, n_scw),
398
+ "TSTOP": np.linspace(5001, 6001, n_scw),
399
+ "RA_SCX": np.random.uniform(0, 360, n_scw),
400
+ "DEC_SCX": np.random.uniform(-80, 80, n_scw),
401
+ "RA_SCZ": np.random.uniform(0, 360, n_scw),
402
+ "DEC_SCZ": np.random.uniform(-80, 80, n_scw),
403
+ "CHI": np.random.uniform(0.5, 5.0, n_scw),
404
+ "CUT_CHI": np.random.uniform(0.5, 5.0, n_scw),
405
+ "GTI_CHI": np.random.uniform(0.5, 5.0, n_scw),
406
+ }
407
+ )
405
408
  other_path = tmp_path / "other_catalog.fits"
406
409
  other_catalog.write(other_path, overwrite=True)
407
410
 
@@ -412,13 +415,12 @@ def test_scwquery_explicit_path_overrides_config(tmp_path, mock_catalog, monkeyp
412
415
  monkeypatch.setattr(Config, "DEFAULT_PATH", config_path)
413
416
  query = ScwQuery()
414
417
  assert query.catalog_path == Path(other_path)
415
- assert len(query.catalog) == 50
416
-
418
+ assert len(query.catalog) == 50
417
419
 
418
420
  # path should override config
419
421
  query = ScwQuery(mock_catalog)
420
422
  assert query.catalog_path == Path(mock_catalog)
421
- assert len(query.catalog) == 1000
423
+ assert len(query.catalog) == 1000
422
424
 
423
425
 
424
426
  def test_scwquery_no_config_raises(tmp_path, monkeypatch):
@@ -441,6 +443,104 @@ def test_scwquery_config_file_not_exists(tmp_path, monkeypatch):
441
443
 
442
444
  monkeypatch.setattr(Config, "DEFAULT_PATH", config_path)
443
445
 
444
- # Should raise when file doesn't exist (in config.catalog_path property)
445
446
  with pytest.raises(FileNotFoundError, match="Catalog path does not exist"):
446
447
  ScwQuery()
448
+
449
+
450
+ def test_write_fits(tmp_path, mock_catalog):
451
+ """Test writing to FITS file"""
452
+ query = ScwQuery(mock_catalog)
453
+ output = tmp_path / "results.fits"
454
+
455
+ query.time(tstart=3400, tstop=3600).write(output)
456
+
457
+ assert output.exists()
458
+ written = Table.read(output)
459
+ assert len(written) > 0
460
+ assert all(written["TSTOP"] >= 3400)
461
+ assert all(written["TSTART"] <= 3600)
462
+
463
+
464
+ def test_write_csv(tmp_path, mock_catalog):
465
+ """Test writing to CSV file"""
466
+ query = ScwQuery(mock_catalog)
467
+ output = tmp_path / "results.csv"
468
+
469
+ query.quality(max_chi=2.0).write(output)
470
+
471
+ assert output.exists()
472
+ written = Table.read(output, format="ascii.csv")
473
+ assert len(written) > 0
474
+ assert all(written["CHI"] <= 2.0)
475
+
476
+
477
+ def test_write_swid_list_txt(tmp_path, mock_catalog):
478
+ """Test writing SWID list to .txt file"""
479
+ query = ScwQuery(mock_catalog)
480
+ output = tmp_path / "swids.txt"
481
+
482
+ query.time(tstart=3500).write(output)
483
+
484
+ assert output.exists()
485
+ swids = output.read_text().strip().split("\n")
486
+ assert len(swids) > 0
487
+ assert all(len(swid) == 12 for swid in swids)
488
+
489
+
490
+ def test_write_swid_only_flag(tmp_path, mock_catalog):
491
+ """Test swid_only flag forces SWID list"""
492
+ query = ScwQuery(mock_catalog)
493
+ output = tmp_path / "swids.fits" # .fits extension but force SWID
494
+
495
+ query.time(tstart=3500).write(output, swid_only=True)
496
+
497
+ assert output.exists()
498
+ # Should be text file despite .fits extension
499
+ swids = output.read_text().strip().split("\n")
500
+ assert len(swids) > 0
501
+ assert all(len(swid) == 12 for swid in swids)
502
+
503
+
504
+ def test_write_overwrite_false(tmp_path, mock_catalog):
505
+ """Test write raises when file exists and overwrite=False"""
506
+ query = ScwQuery(mock_catalog)
507
+ output = tmp_path / "results.fits"
508
+
509
+ # Write once
510
+ query.write(output)
511
+
512
+ # Try to write again without overwrite
513
+ with pytest.raises(FileExistsError, match="already exists"):
514
+ query.write(output, overwrite=False)
515
+
516
+
517
+ def test_write_overwrite_true(tmp_path, mock_catalog):
518
+ """Test write overwrites when overwrite=True"""
519
+ query = ScwQuery(mock_catalog)
520
+ output = tmp_path / "results.fits"
521
+
522
+ # First write
523
+ query.time(tstart=3400, tstop=3600).write(output)
524
+ first_result = Table.read(output)
525
+
526
+ # Overwrite with different data
527
+ query.time(tstart=3700, tstop=3800).write(output, overwrite=True)
528
+ second_result = Table.read(output)
529
+
530
+ # Check different time ranges (data should be different)
531
+ assert all(first_result["TSTART"] <= 3600)
532
+ assert all(second_result["TSTOP"] >= 3700)
533
+ assert first_result != second_result
534
+
535
+
536
+ def test_write_empty_result(tmp_path, mock_catalog):
537
+ """Test writing empty result set"""
538
+ query = ScwQuery(mock_catalog)
539
+ output = tmp_path / "empty.fits"
540
+
541
+ # Query that returns no results
542
+ query.time(tstart=10000).write(output)
543
+
544
+ assert output.exists()
545
+ result = Table.read(output)
546
+ assert len(result) == 0
@@ -17,98 +17,85 @@ def runner():
17
17
  def mock_catalog(tmp_path):
18
18
  """Create mock SCW catalog."""
19
19
  catalog_path = tmp_path / "test_catalog.fits"
20
-
20
+
21
21
  n_scw = 100
22
22
  data = {
23
- 'SWID': [f"{i:012d}" for i in range(n_scw)],
24
- 'REVOL': np.random.randint(100, 500, n_scw),
25
- 'TSTART': np.linspace(3000, 4000, n_scw),
26
- 'TSTOP': np.linspace(3000.5, 4000.5, n_scw),
27
- 'RA_SCX': np.random.uniform(0, 360, n_scw),
28
- 'DEC_SCX': np.random.uniform(-80, 80, n_scw),
29
- 'RA_SCZ': np.random.uniform(0, 360, n_scw),
30
- 'DEC_SCZ': np.random.uniform(-80, 80, n_scw),
31
- 'CHI': np.random.uniform(0.5, 5.0, n_scw),
32
- 'CUT_CHI': np.random.uniform(0.5, 5.0, n_scw),
33
- 'GTI_CHI': np.random.uniform(0.5, 5.0, n_scw),
23
+ "SWID": [f"{i:012d}" for i in range(n_scw)],
24
+ "REVOL": np.random.randint(100, 500, n_scw),
25
+ "TSTART": np.linspace(3000, 4000, n_scw),
26
+ "TSTOP": np.linspace(3000.5, 4000.5, n_scw),
27
+ "RA_SCX": np.random.uniform(0, 360, n_scw),
28
+ "DEC_SCX": np.random.uniform(-80, 80, n_scw),
29
+ "RA_SCZ": np.random.uniform(0, 360, n_scw),
30
+ "DEC_SCZ": np.random.uniform(-80, 80, n_scw),
31
+ "CHI": np.random.uniform(0.5, 5.0, n_scw),
32
+ "CUT_CHI": np.random.uniform(0.5, 5.0, n_scw),
33
+ "GTI_CHI": np.random.uniform(0.5, 5.0, n_scw),
34
34
  }
35
35
  table = Table(data)
36
- table.write(catalog_path, format='fits', overwrite=True)
37
-
36
+ table.write(catalog_path, format="fits", overwrite=True)
37
+
38
38
  return catalog_path
39
39
 
40
40
 
41
41
  def test_cli_version(runner):
42
42
  """Test --version flag."""
43
- result = runner.invoke(main, ['--version'])
43
+ result = runner.invoke(main, ["--version"])
44
44
  assert result.exit_code == 0
45
- assert 'version' in result.output.lower()
45
+ assert "version" in result.output.lower()
46
46
 
47
47
 
48
48
  def test_cli_help(runner):
49
49
  """Test --help flag."""
50
- result = runner.invoke(main, ['--help'])
50
+ result = runner.invoke(main, ["--help"])
51
51
  assert result.exit_code == 0
52
- assert 'ISGRI' in result.output
52
+ assert "ISGRI" in result.output
53
53
 
54
54
 
55
55
  def test_query_with_catalog(runner, mock_catalog):
56
56
  """Test query command with explicit catalog."""
57
- result = runner.invoke(main, ['query', '--catalog', str(mock_catalog)])
57
+ result = runner.invoke(main, ["query", "--catalog", str(mock_catalog), "--tstart", "3000", "--tstop", "4000"])
58
58
  assert result.exit_code == 0
59
- assert 'Found' in result.output
60
- assert '100' in result.output
59
+ assert "Found" in result.output
60
+ assert "100" in result.output
61
61
 
62
62
 
63
63
  def test_query_count(runner, mock_catalog):
64
64
  """Test query --count flag."""
65
- result = runner.invoke(main, ['query', '--catalog', str(mock_catalog), '--count'])
65
+ result = runner.invoke(main, ["query", "--catalog", str(mock_catalog),"--tstart", "3000", "--count"])
66
66
  assert result.exit_code == 0
67
- assert result.output.strip() == '100'
67
+ assert result.output.strip() == "100"
68
68
 
69
69
 
70
70
  def test_query_list_swids(runner, mock_catalog):
71
71
  """Test query --list-swids flag."""
72
- result = runner.invoke(main, ['query', '--catalog', str(mock_catalog), '--list-swids'])
72
+ result = runner.invoke(main, ["query", "--catalog", str(mock_catalog), "--tstart", "3000","--list-swids"])
73
73
  assert result.exit_code == 0
74
- lines = result.output.strip().split('\n')
74
+ lines = result.output.strip().split("\n")
75
75
  assert len(lines) == 100
76
76
  assert all(len(line) == 12 for line in lines)
77
77
 
78
78
 
79
79
  def test_query_with_filters(runner, mock_catalog):
80
80
  """Test query with time filters."""
81
- result = runner.invoke(main, [
82
- 'query',
83
- '--catalog', str(mock_catalog),
84
- '--tstart', '3000',
85
- '--tstop', '3100'
86
- ])
81
+ result = runner.invoke(main, ["query", "--catalog", str(mock_catalog), "--tstart", "3000", "--tstop", "3100"])
87
82
  assert result.exit_code == 0
88
- assert 'Found' in result.output
83
+ assert "Found" in result.output
89
84
 
90
85
 
91
86
  def test_query_output_fits(runner, mock_catalog, tmp_path):
92
87
  """Test query with FITS output."""
93
88
  output_file = tmp_path / "results.fits"
94
- result = runner.invoke(main, [
95
- 'query',
96
- '--catalog', str(mock_catalog),
97
- '--output', str(output_file)
98
- ])
89
+ result = runner.invoke(main, ["query", "--catalog", str(mock_catalog),"--tstart", "3000", "--output", str(output_file)])
99
90
  assert result.exit_code == 0
100
91
  assert output_file.exists()
101
- assert 'Saved' in result.output
92
+ assert "Saved" in result.output
102
93
 
103
94
 
104
95
  def test_query_output_csv(runner, mock_catalog, tmp_path):
105
96
  """Test query with CSV output."""
106
97
  output_file = tmp_path / "results.csv"
107
- result = runner.invoke(main, [
108
- 'query',
109
- '--catalog', str(mock_catalog),
110
- '--output', str(output_file)
111
- ])
98
+ result = runner.invoke(main, ["query", "--catalog", str(mock_catalog),"--tstart", "3000", "--output", str(output_file)])
112
99
  assert result.exit_code == 0
113
100
  assert output_file.exists()
114
101
 
@@ -117,10 +104,10 @@ def test_query_no_catalog_no_config(runner, tmp_path, monkeypatch):
117
104
  """Test query fails when no catalog and no config."""
118
105
  config_path = tmp_path / "config.toml"
119
106
  monkeypatch.setattr(Config, "DEFAULT_PATH", config_path)
120
-
121
- result = runner.invoke(main, ['query'])
107
+
108
+ result = runner.invoke(main, ["query"])
122
109
  assert result.exit_code != 0
123
- assert 'No catalog_path provided' in result.output
110
+ assert "No catalog configured" in result.output
124
111
 
125
112
 
126
113
  def test_query_uses_config(runner, mock_catalog, tmp_path, monkeypatch):
@@ -128,23 +115,23 @@ def test_query_uses_config(runner, mock_catalog, tmp_path, monkeypatch):
128
115
  config_path = tmp_path / "config.toml"
129
116
  cfg = Config(config_path)
130
117
  cfg.set(catalog_path=mock_catalog)
131
-
118
+
132
119
  monkeypatch.setattr(Config, "DEFAULT_PATH", config_path)
133
-
134
- result = runner.invoke(main, ['query'])
120
+
121
+ result = runner.invoke(main, ["query","--tstart", "3000", ])
135
122
  assert result.exit_code == 0
136
- assert 'Found 100' in result.output
123
+ assert "Found 100" in result.output
137
124
 
138
125
 
139
126
  def test_config_show_empty(runner, tmp_path, monkeypatch):
140
127
  """Test config command with empty config."""
141
128
  config_path = tmp_path / "config.toml"
142
129
  monkeypatch.setattr(Config, "DEFAULT_PATH", config_path)
143
-
144
- result = runner.invoke(main, ['config'])
130
+
131
+ result = runner.invoke(main, ["config"])
145
132
  assert result.exit_code == 0
146
133
  assert str(config_path) in result.output
147
- assert '(not set)' in result.output
134
+ assert "(not set)" in result.output
148
135
 
149
136
 
150
137
  def test_config_show_with_values(runner, tmp_path, mock_catalog, monkeypatch):
@@ -152,10 +139,10 @@ def test_config_show_with_values(runner, tmp_path, mock_catalog, monkeypatch):
152
139
  config_path = tmp_path / "config.toml"
153
140
  cfg = Config(config_path)
154
141
  cfg.set(archive_path=tmp_path, catalog_path=mock_catalog)
155
-
142
+
156
143
  monkeypatch.setattr(Config, "DEFAULT_PATH", config_path)
157
-
158
- result = runner.invoke(main, ['config'])
144
+
145
+ result = runner.invoke(main, ["config"])
159
146
  assert result.exit_code == 0
160
147
  assert str(tmp_path) in result.output
161
148
  assert str(mock_catalog) in result.output
@@ -165,14 +152,14 @@ def test_config_set_archive(runner, tmp_path, monkeypatch):
165
152
  """Test config-set --archive."""
166
153
  config_path = tmp_path / "test_config.toml"
167
154
  monkeypatch.setattr(Config, "DEFAULT_PATH", config_path)
168
-
155
+
169
156
  archive_path = tmp_path / "archive"
170
157
  archive_path.mkdir()
171
-
172
- result = runner.invoke(main, ['config-set', '--archive', str(archive_path)])
158
+
159
+ result = runner.invoke(main, ["config-set", "--archive", str(archive_path)])
173
160
  assert result.exit_code == 0
174
- assert 'Archive path set' in result.output
175
-
161
+ assert "Archive path set" in result.output
162
+
176
163
  cfg = Config(config_path)
177
164
  assert cfg.archive_path == archive_path
178
165
 
@@ -181,11 +168,11 @@ def test_config_set_catalog(runner, tmp_path, mock_catalog, monkeypatch):
181
168
  """Test config-set --catalog."""
182
169
  config_path = tmp_path / "test_config.toml"
183
170
  monkeypatch.setattr(Config, "DEFAULT_PATH", config_path)
184
-
185
- result = runner.invoke(main, ['config-set', '--catalog', str(mock_catalog)])
171
+
172
+ result = runner.invoke(main, ["config-set", "--catalog", str(mock_catalog)])
186
173
  assert result.exit_code == 0
187
- assert 'Catalog path set' in result.output
188
-
174
+ assert "Catalog path set" in result.output
175
+
189
176
  cfg = Config(config_path)
190
177
  assert str(cfg.catalog_path) == str(mock_catalog)
191
178
 
@@ -194,50 +181,44 @@ def test_config_set_both(runner, tmp_path, mock_catalog, monkeypatch):
194
181
  """Test config-set with both options."""
195
182
  config_path = tmp_path / "test_config.toml"
196
183
  monkeypatch.setattr(Config, "DEFAULT_PATH", config_path)
197
-
184
+
198
185
  archive_path = tmp_path / "archive"
199
186
  archive_path.mkdir()
200
-
201
- result = runner.invoke(main, [
202
- 'config-set',
203
- '--archive', str(archive_path),
204
- '--catalog', str(mock_catalog)
205
- ])
187
+
188
+ result = runner.invoke(main, ["config-set", "--archive", str(archive_path), "--catalog", str(mock_catalog)])
206
189
  assert result.exit_code == 0
207
- assert 'Archive path set' in result.output
208
- assert 'Catalog path set' in result.output
190
+ assert "Archive path set" in result.output
191
+ assert "Catalog path set" in result.output
209
192
 
210
193
 
211
194
  def test_config_set_no_options(runner):
212
195
  """Test config-set with no options fails."""
213
- result = runner.invoke(main, ['config-set'])
196
+ result = runner.invoke(main, ["config-set"])
214
197
  assert result.exit_code != 0
215
- assert 'Specify at least one option' in result.output
198
+ assert "Specify at least one option" in result.output
216
199
 
217
200
 
218
201
  def test_config_set_nonexistent_path_abort(runner, tmp_path, monkeypatch):
219
202
  """Test config-set with non-existent path (user aborts)."""
220
203
  config_path = tmp_path / "test_config.toml"
221
204
  monkeypatch.setattr(Config, "DEFAULT_PATH", config_path)
222
-
223
- result = runner.invoke(main, [
224
- 'config-set',
225
- '--archive', '/nonexistent/path'
226
- ], input='n\n') # User says 'no' to confirmation
227
-
205
+
206
+ result = runner.invoke(
207
+ main, ["config-set", "--archive", "/nonexistent/path"], input="n\n"
208
+ ) # User says 'no' to confirmation
209
+
228
210
  assert result.exit_code != 0
229
- assert 'Warning' in result.output
211
+ assert "Warning" in result.output
230
212
 
231
213
 
232
214
  def test_config_set_nonexistent_path_confirm(runner, tmp_path, monkeypatch):
233
215
  """Test config-set with non-existent path (user confirms)."""
234
216
  config_path = tmp_path / "test_config.toml"
235
217
  monkeypatch.setattr(Config, "DEFAULT_PATH", config_path)
236
-
237
- result = runner.invoke(main, [
238
- 'config-set',
239
- '--archive', '/nonexistent/path'
240
- ], input='y\n') # User says 'yes' to confirmation
241
-
218
+
219
+ result = runner.invoke(
220
+ main, ["config-set", "--archive", "/nonexistent/path"], input="y\n"
221
+ ) # User says 'yes' to confirmation
222
+
242
223
  assert result.exit_code == 0
243
- assert 'Archive path set' in result.output
224
+ assert "Archive path set" in result.output
@@ -1 +0,0 @@
1
- __version__ = "0.5.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes