gwsim 0.1.0__py3-none-any.whl

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 (103) hide show
  1. gwsim/__init__.py +11 -0
  2. gwsim/__main__.py +8 -0
  3. gwsim/cli/__init__.py +0 -0
  4. gwsim/cli/config.py +88 -0
  5. gwsim/cli/default_config.py +56 -0
  6. gwsim/cli/main.py +101 -0
  7. gwsim/cli/merge.py +150 -0
  8. gwsim/cli/repository/__init__.py +0 -0
  9. gwsim/cli/repository/create.py +91 -0
  10. gwsim/cli/repository/delete.py +51 -0
  11. gwsim/cli/repository/download.py +54 -0
  12. gwsim/cli/repository/list_depositions.py +63 -0
  13. gwsim/cli/repository/main.py +38 -0
  14. gwsim/cli/repository/metadata/__init__.py +0 -0
  15. gwsim/cli/repository/metadata/main.py +24 -0
  16. gwsim/cli/repository/metadata/update.py +58 -0
  17. gwsim/cli/repository/publish.py +52 -0
  18. gwsim/cli/repository/upload.py +74 -0
  19. gwsim/cli/repository/utils.py +47 -0
  20. gwsim/cli/repository/verify.py +61 -0
  21. gwsim/cli/simulate.py +220 -0
  22. gwsim/cli/simulate_utils.py +596 -0
  23. gwsim/cli/utils/__init__.py +85 -0
  24. gwsim/cli/utils/checkpoint.py +178 -0
  25. gwsim/cli/utils/config.py +347 -0
  26. gwsim/cli/utils/hash.py +23 -0
  27. gwsim/cli/utils/retry.py +62 -0
  28. gwsim/cli/utils/simulation_plan.py +439 -0
  29. gwsim/cli/utils/template.py +56 -0
  30. gwsim/cli/utils/utils.py +149 -0
  31. gwsim/cli/validate.py +255 -0
  32. gwsim/data/__init__.py +8 -0
  33. gwsim/data/serialize/__init__.py +9 -0
  34. gwsim/data/serialize/decoder.py +59 -0
  35. gwsim/data/serialize/encoder.py +44 -0
  36. gwsim/data/serialize/serializable.py +33 -0
  37. gwsim/data/time_series/__init__.py +3 -0
  38. gwsim/data/time_series/inject.py +104 -0
  39. gwsim/data/time_series/time_series.py +355 -0
  40. gwsim/data/time_series/time_series_list.py +182 -0
  41. gwsim/detector/__init__.py +8 -0
  42. gwsim/detector/base.py +156 -0
  43. gwsim/detector/detectors/E1_2L_Aligned_Sardinia.interferometer +22 -0
  44. gwsim/detector/detectors/E1_2L_Misaligned_Sardinia.interferometer +22 -0
  45. gwsim/detector/detectors/E1_Triangle_EMR.interferometer +19 -0
  46. gwsim/detector/detectors/E1_Triangle_Sardinia.interferometer +19 -0
  47. gwsim/detector/detectors/E2_2L_Aligned_EMR.interferometer +22 -0
  48. gwsim/detector/detectors/E2_2L_Misaligned_EMR.interferometer +22 -0
  49. gwsim/detector/detectors/E2_Triangle_EMR.interferometer +19 -0
  50. gwsim/detector/detectors/E2_Triangle_Sardinia.interferometer +19 -0
  51. gwsim/detector/detectors/E3_Triangle_EMR.interferometer +19 -0
  52. gwsim/detector/detectors/E3_Triangle_Sardinia.interferometer +19 -0
  53. gwsim/detector/noise_curves/ET_10_HF_psd.txt +3000 -0
  54. gwsim/detector/noise_curves/ET_10_full_cryo_psd.txt +3000 -0
  55. gwsim/detector/noise_curves/ET_15_HF_psd.txt +3000 -0
  56. gwsim/detector/noise_curves/ET_15_full_cryo_psd.txt +3000 -0
  57. gwsim/detector/noise_curves/ET_20_HF_psd.txt +3000 -0
  58. gwsim/detector/noise_curves/ET_20_full_cryo_psd.txt +3000 -0
  59. gwsim/detector/noise_curves/ET_D_psd.txt +3000 -0
  60. gwsim/detector/utils.py +90 -0
  61. gwsim/glitch/__init__.py +7 -0
  62. gwsim/glitch/base.py +69 -0
  63. gwsim/mixin/__init__.py +8 -0
  64. gwsim/mixin/detector.py +203 -0
  65. gwsim/mixin/gwf.py +192 -0
  66. gwsim/mixin/population_reader.py +175 -0
  67. gwsim/mixin/randomness.py +107 -0
  68. gwsim/mixin/time_series.py +295 -0
  69. gwsim/mixin/waveform.py +47 -0
  70. gwsim/noise/__init__.py +19 -0
  71. gwsim/noise/base.py +134 -0
  72. gwsim/noise/bilby_stationary_gaussian.py +117 -0
  73. gwsim/noise/colored_noise.py +275 -0
  74. gwsim/noise/correlated_noise.py +257 -0
  75. gwsim/noise/pycbc_stationary_gaussian.py +112 -0
  76. gwsim/noise/stationary_gaussian.py +44 -0
  77. gwsim/noise/white_noise.py +51 -0
  78. gwsim/repository/__init__.py +0 -0
  79. gwsim/repository/zenodo.py +269 -0
  80. gwsim/signal/__init__.py +11 -0
  81. gwsim/signal/base.py +137 -0
  82. gwsim/signal/cbc.py +61 -0
  83. gwsim/simulator/__init__.py +7 -0
  84. gwsim/simulator/base.py +315 -0
  85. gwsim/simulator/state.py +85 -0
  86. gwsim/utils/__init__.py +11 -0
  87. gwsim/utils/datetime_parser.py +44 -0
  88. gwsim/utils/et_2l_geometry.py +165 -0
  89. gwsim/utils/io.py +167 -0
  90. gwsim/utils/log.py +145 -0
  91. gwsim/utils/population.py +48 -0
  92. gwsim/utils/random.py +69 -0
  93. gwsim/utils/retry.py +75 -0
  94. gwsim/utils/triangular_et_geometry.py +164 -0
  95. gwsim/version.py +7 -0
  96. gwsim/waveform/__init__.py +7 -0
  97. gwsim/waveform/factory.py +83 -0
  98. gwsim/waveform/pycbc_wrapper.py +37 -0
  99. gwsim-0.1.0.dist-info/METADATA +157 -0
  100. gwsim-0.1.0.dist-info/RECORD +103 -0
  101. gwsim-0.1.0.dist-info/WHEEL +4 -0
  102. gwsim-0.1.0.dist-info/entry_points.txt +2 -0
  103. gwsim-0.1.0.dist-info/licenses/LICENSE +21 -0
gwsim/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """A package to simulate a population of gravitational waves."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from . import utils
6
+ from .utils.log import setup_logger
7
+ from .version import __version__
8
+
9
+ setup_logger()
10
+
11
+ __all__ = ["__version__", "utils"]
gwsim/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Main entry point for gwsim package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ if __name__ == "__main__":
6
+ from gwsim.utils.log import setup_logger
7
+
8
+ setup_logger(print_version=True)
gwsim/cli/__init__.py ADDED
File without changes
gwsim/cli/config.py ADDED
@@ -0,0 +1,88 @@
1
+ """
2
+ Utility functions to load and save configuration files.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import yaml
12
+
13
+ from ..utils.io import check_file_exist, check_file_overwrite
14
+
15
+ logger = logging.getLogger("gwsim")
16
+
17
+
18
+ @check_file_exist()
19
+ def load_config(file_name: Path, encoding: str = "utf-8") -> dict:
20
+ """Load configuration file.
21
+
22
+ Args:
23
+ file_name (Path): File name.
24
+ encoding (str, optional): File encoding. Defaults to "utf-8".
25
+
26
+ Returns:
27
+ dict: A dictionary of the configuration.
28
+ """
29
+ with open(file_name, encoding=encoding) as f:
30
+ config = yaml.safe_load(f)
31
+ return config
32
+
33
+
34
+ @check_file_overwrite()
35
+ def save_config(
36
+ file_name: Path, config: dict, overwrite: bool = False, encoding: str = "utf-8", backup: bool = True
37
+ ) -> None:
38
+ """Save configuration file safely with optional backup.
39
+
40
+ Args:
41
+ file_name (Path): File name.
42
+ config (dict): A dictionary of configuration.
43
+ overwrite (bool, optional): If True, overwrite the existing file, or otherwise raise an error.
44
+ Defaults to False.
45
+ encoding (str, optional): File encoding. Defaults to "utf-8".
46
+ backup (bool, optional): If True and overwriting, create a backup of the existing file.
47
+ Defaults to True.
48
+
49
+ Raises:
50
+ FileExistsError: If file_name exists and overwrite is False, raise an error.
51
+ """
52
+ # Create backup if file exists and we're overwriting
53
+ if file_name.exists() and overwrite and backup:
54
+ backup_path = file_name.with_suffix(f"{file_name.suffix}.backup")
55
+ logger.info("Creating backup: %s", backup_path)
56
+ backup_path.write_text(file_name.read_text(encoding=encoding), encoding=encoding)
57
+
58
+ # Atomic write using temporary file
59
+ temp_file = file_name.with_suffix(f"{file_name.suffix}.tmp")
60
+ try:
61
+ with open(temp_file, "w", encoding=encoding) as f:
62
+ yaml.safe_dump(config, f, default_flow_style=False, sort_keys=False)
63
+
64
+ # Atomic move (rename) - this is atomic on most filesystems
65
+ temp_file.replace(file_name)
66
+ logger.info("Configuration saved to: %s", file_name)
67
+
68
+ except Exception:
69
+ # Clean up temp file if something went wrong
70
+ if temp_file.exists():
71
+ temp_file.unlink()
72
+ raise
73
+
74
+
75
+ def get_config_value(config: dict, key: str, default_value: Any | None = None) -> Any:
76
+ """Get the argument
77
+
78
+ Args:
79
+ config (dict): A dictionary of configuration.
80
+ key (str): Key of the entry.
81
+ default_value (Any | None, optional): Default value if key is not present. Defaults to None.
82
+
83
+ Returns:
84
+ Any: Value of the corresponding key in config.
85
+ """
86
+ if key in config:
87
+ return config[key]
88
+ return default_value
@@ -0,0 +1,56 @@
1
+ """
2
+ A tool to generate default configuration file.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ _DEFAULT_CONFIG = {
13
+ "globals": {
14
+ "working-directory": ".",
15
+ "sampling-frequency": 16384,
16
+ "duration": 4,
17
+ "output-directory": "output",
18
+ "metadata-directory": "metadata",
19
+ },
20
+ "simulators": {
21
+ "example": {
22
+ "class": "WhiteNoise", # Resolves to gwsim.noise.WhiteNoise
23
+ "arguments": {
24
+ "batch_size": 1,
25
+ "max_samples": 10,
26
+ "loc": 0.0,
27
+ "scale": 1.0,
28
+ "seed": 0,
29
+ },
30
+ "output": {
31
+ "file_name": "example-{{ start-time }}-{{ duration }}.gwf",
32
+ "arguments": {
33
+ "channel": "STRAIN",
34
+ },
35
+ },
36
+ },
37
+ },
38
+ }
39
+
40
+
41
+ def default_config_command(
42
+ output: Annotated[str, typer.Option("--output", help="File name of the output", prompt=True)] = "config.yaml",
43
+ overwrite: Annotated[bool, typer.Option("--overwrite", help="Overwrite the existing file")] = False,
44
+ ) -> None:
45
+ """Write the default configuration file to disk.
46
+
47
+ Args:
48
+ output (str): Name of the output file.
49
+ overwrite (bool): If True, overwrite the existing file, otherwise raise an error if output already exists.
50
+
51
+ Raises:
52
+ FileExistsError: If file_name exists and overwrite is False, raise an error.
53
+ """
54
+ from gwsim.cli.utils.config import save_config # pylint: disable=import-outside-toplevel
55
+
56
+ save_config(file_name=Path(output), config=_DEFAULT_CONFIG, overwrite=overwrite)
gwsim/cli/main.py ADDED
@@ -0,0 +1,101 @@
1
+ """
2
+ Main command line tool to generate mock data.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import enum
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+
13
+ class LoggingLevel(str, enum.Enum):
14
+ """Logging levels for the CLI."""
15
+
16
+ NOTSET = "NOTSET"
17
+ DEBUG = "DEBUG"
18
+ INFO = "INFO"
19
+ WARNING = "WARNING"
20
+ ERROR = "ERROR"
21
+ CRITICAL = "CRITICAL"
22
+
23
+
24
+ # Create the main Typer app
25
+ app = typer.Typer(
26
+ name="gwsim",
27
+ help="Gravitational Wave Simulation Data Simulator",
28
+ rich_markup_mode="rich",
29
+ )
30
+
31
+
32
+ def setup_logging(level: LoggingLevel = LoggingLevel.INFO) -> None:
33
+ """Set up logging with Rich handler."""
34
+ import logging # pylint: disable=import-outside-toplevel
35
+
36
+ from rich.console import Console # pylint: disable=import-outside-toplevel
37
+ from rich.logging import RichHandler # pylint: disable=import-outside-toplevel
38
+
39
+ logger = logging.getLogger("gwsim")
40
+
41
+ logger.setLevel(level.value)
42
+
43
+ console = Console()
44
+
45
+ # Remove any existing handlers to ensure RichHandler is used
46
+ for h in logger.handlers[:]: # Use slice copy to avoid modification during iteration
47
+ logger.removeHandler(h)
48
+ # Add the RichHandler
49
+ if not logger.handlers:
50
+ handler = RichHandler(
51
+ console=console,
52
+ rich_tracebacks=True,
53
+ show_time=True,
54
+ show_level=True, # Keep level (e.g., DEBUG, INFO) for clarity
55
+ markup=True, # Enable Rich markup in messages for styling
56
+ level=level.value, # Ensure handler respects the level
57
+ omit_repeated_times=False,
58
+ log_time_format="%H:%M",
59
+ )
60
+ handler.setLevel(level.value)
61
+ logger.addHandler(handler)
62
+
63
+ # Prevent propagation to root logger to avoid duplicate output
64
+ logger.propagate = False
65
+
66
+
67
+ @app.callback()
68
+ def main(
69
+ verbose: Annotated[
70
+ LoggingLevel,
71
+ typer.Option("--verbose", "-v", help="Set verbosity level"),
72
+ ] = LoggingLevel.INFO,
73
+ ) -> None:
74
+ """Gravitational Wave Simulation Data Simulator.
75
+
76
+ This command-line tool provides functionality for generating
77
+ gravitational wave detector simulation data.
78
+ """
79
+ setup_logging(verbose)
80
+
81
+
82
+ # Import and register commands after app is created
83
+ def register_commands() -> None:
84
+ """Register all CLI commands."""
85
+
86
+ # Fast imports
87
+ from gwsim.cli.default_config import default_config_command # pylint: disable=import-outside-toplevel
88
+ from gwsim.cli.merge import merge_command # pylint: disable=import-outside-toplevel
89
+ from gwsim.cli.repository.main import repository_app # pylint: disable=import-outside-toplevel
90
+ from gwsim.cli.simulate import simulate_command # pylint: disable=import-outside-toplevel
91
+ from gwsim.cli.validate import validate_command # pylint: disable=import-outside-toplevel
92
+
93
+ app.command("simulate")(simulate_command)
94
+ app.command("merge")(merge_command)
95
+ app.command("default-config")(default_config_command)
96
+ app.command("validate")(validate_command)
97
+
98
+ app.add_typer(repository_app, name="repository", help="Manage Zenodo repositories")
99
+
100
+
101
+ register_commands()
gwsim/cli/merge.py ADDED
@@ -0,0 +1,150 @@
1
+ """CLI to merge the frame files generated by gwsim simulations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+
11
+ def merge_command( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
12
+ file_names: Annotated[list[Path], typer.Argument(..., help="List of frame files to merge")],
13
+ channel: Annotated[str, typer.Option("--channel", help="Channel name to merge")] = "STRAIN",
14
+ output: Annotated[str, typer.Option("--output", help="Output merged frame file name")] = "merged.gwf",
15
+ output_channel: (
16
+ Annotated[str, typer.Option("--output-channel", help="Channel name for the output file")] | None
17
+ ) = None,
18
+ metadata: Annotated[list[str], typer.Option("--metadata", help="Metadata file to use for merging")] | None = None,
19
+ author: Annotated[str, typer.Option("--author", help="Author of the merged file")] | None = None,
20
+ email: Annotated[str, typer.Option("--email", help="Email of the author")] | None = None,
21
+ force: Annotated[bool, typer.Option("--force", help="Bypass the requirements of providing metadata files")] = False,
22
+ ):
23
+ """Merge multiple frame files into a single file.
24
+
25
+ Args:
26
+ file_names (list[str]): List of frame files to merge.
27
+ channel (str): Channel name to merge.
28
+ output (str): Output merged frame file name.
29
+ output_channel (str | None): Channel name for the output file. If None, use
30
+ metadata (list[str] | None): List of metadata files corresponding to the frame files.
31
+ author (str | None): Author of the merged file.
32
+ email (str | None): Email of the author.
33
+ force (bool): If True, bypass the requirement of providing metadata files.
34
+
35
+ Raises:
36
+ ValueError: If metadata files are not provided and force is False.
37
+ """
38
+ import datetime # pylint: disable=import-outside-toplevel
39
+ import getpass # pylint: disable=import-outside-toplevel
40
+ from typing import cast # pylint: disable=import-outside-toplevel
41
+
42
+ import yaml # pylint: disable=import-outside-toplevel
43
+ from gwpy.timeseries import TimeSeries # pylint: disable=import-outside-toplevel
44
+
45
+ from gwsim.cli.utils.hash import compute_file_hash # pylint: disable=import-outside-toplevel
46
+ from gwsim.utils.log import get_dependency_versions # pylint: disable=import-outside-toplevel
47
+
48
+ if file_names is None:
49
+ file_names = []
50
+ if not isinstance(metadata, list) and not force:
51
+ raise ValueError("Metadata files must be provided unless --force is used.")
52
+
53
+ if isinstance(metadata, list) and not force:
54
+ typer.echo("Validating files...")
55
+
56
+ if len(file_names) != len(metadata or []):
57
+ raise ValueError("The number of metadata files must match the number of frame files.")
58
+
59
+ # Validate the files against the metadata
60
+ for i, file_name in enumerate(file_names):
61
+ metadata_file = Path(metadata[i])
62
+
63
+ # Find the corresponding metadata entry for the file_name
64
+ with metadata_file.open("r", encoding="utf-8") as f:
65
+ file_metadata = yaml.safe_load(f)
66
+
67
+ file_hashes: dict = file_metadata.get("file_hashes", {})
68
+ expected_hash = file_hashes.get(file_name.name)
69
+
70
+ if expected_hash is None:
71
+ raise ValueError(f"No hash found in metadata for file {file_name.name}")
72
+
73
+ actual_hash = compute_file_hash(file_name)
74
+
75
+ if actual_hash != expected_hash:
76
+ raise ValueError(
77
+ f"Hash mismatch for file {file_name.name}: expected {expected_hash}, got {actual_hash}"
78
+ )
79
+
80
+ # Placeholder for actual merging logic
81
+ typer.echo(f"Merging files: {file_names}")
82
+
83
+ # Read the first file
84
+ frame_data = cast(TimeSeries, TimeSeries.read(file_names[0], channel))
85
+
86
+ # Get the start time, duration and sampling frequency
87
+ start_time = frame_data.epoch
88
+ duration = frame_data.duration
89
+ sampling_frequency = frame_data.sample_rate
90
+
91
+ for i in range(1, len(file_names)):
92
+ next_frame_data = cast(TimeSeries, TimeSeries.read(file_names[i], channel))
93
+
94
+ if next_frame_data.epoch != start_time:
95
+ raise ValueError(f"Start time mismatch: {next_frame_data.epoch} != {start_time}")
96
+ if next_frame_data.duration != duration:
97
+ raise ValueError(f"Duration mismatch: {next_frame_data.duration} != {duration}")
98
+ if next_frame_data.sample_rate != sampling_frequency:
99
+ raise ValueError(f"Sampling frequency mismatch: {next_frame_data.sample_rate} != {sampling_frequency}")
100
+
101
+ frame_data = frame_data.inject(next_frame_data)
102
+
103
+ # Write the merged data to a new file
104
+ # Atomic write to avoid partial writes
105
+ # The write function is suffix sensitive, so we prepend a .tmp to the original suffix
106
+ temp_output = Path(output).with_suffix(".tmp" + Path(output).suffix)
107
+ if output_channel is not None:
108
+ frame_data.channel = output_channel
109
+ frame_data.write(temp_output)
110
+ temp_output.rename(output)
111
+
112
+ if metadata:
113
+ # If metadata is provided, create a new metadata file for the merged file
114
+
115
+ typer.echo("Creating merged metadata file...")
116
+
117
+ merged_metadata = {"type": "merged", "source_files": {}}
118
+
119
+ for i, file_name in enumerate(file_names):
120
+ metadata_file = Path(metadata[i])
121
+
122
+ with metadata_file.open("r", encoding="utf-8") as f:
123
+ file_metadata = yaml.safe_load(f)
124
+
125
+ merged_metadata["source_files"][file_name] = file_metadata
126
+
127
+ if author is None:
128
+ author = getpass.getuser()
129
+
130
+ timestamp = datetime.datetime.now(datetime.timezone.utc)
131
+
132
+ merged_metadata["output_files"] = [output]
133
+ merged_metadata["file_hashes"] = {output: compute_file_hash(output)}
134
+ merged_metadata["author"] = author
135
+ merged_metadata["email"] = email
136
+ merged_metadata["timestamp"] = timestamp.isoformat()
137
+ merged_metadata["versions"] = get_dependency_versions()
138
+
139
+ # Atomic write of metadata file
140
+ merged_metadata_file = Path(output).with_suffix(".metadata.yaml")
141
+ temp_metadata_file = merged_metadata_file.with_suffix(".tmp")
142
+ with temp_metadata_file.open("w", encoding="utf-8") as f:
143
+ yaml.safe_dump(merged_metadata, f)
144
+ temp_metadata_file.rename(merged_metadata_file)
145
+
146
+ typer.echo(f"Merged metadata file created at {merged_metadata_file}")
147
+
148
+ else:
149
+ typer.echo("No metadata provided for the source files.")
150
+ typer.echo("No metadata file will be created for the merged file.")
File without changes
@@ -0,0 +1,91 @@
1
+ """CLI for creating Zenodo repository depositions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+
11
+ def create_command( # pylint: disable=too-many-locals
12
+ title: Annotated[str | None, typer.Option("--title", help="Deposition title")] = None,
13
+ description: Annotated[str | None, typer.Option("--description", help="Deposition description")] = None,
14
+ metadata_file: Annotated[
15
+ Path | None, typer.Option("--metadata-file", help="YAML file with additional metadata")
16
+ ] = None,
17
+ sandbox: Annotated[bool, typer.Option("--sandbox", help="Use sandbox environment for testing")] = False,
18
+ token: Annotated[
19
+ str | None,
20
+ typer.Option(
21
+ "--token",
22
+ help=(
23
+ "Zenodo access token (default: ZENODO_API_TOKEN env or"
24
+ " ZENODO_SANDBOX_API_TOKEN env for Zenodo Sandbox)"
25
+ ),
26
+ ),
27
+ ] = None,
28
+ ) -> None:
29
+ """Create a new deposition on Zenodo.
30
+
31
+ Interactive mode: Leave options blank to be prompted.
32
+
33
+ Examples:
34
+ # Interactive mode
35
+ gwsim repository create
36
+
37
+ # With all options
38
+ gwsim repository create --title "GW Simulation Data" --description "MDC v1"
39
+
40
+ # Using metadata file
41
+ gwsim repository create --metadata-file metadata.yaml
42
+ """
43
+ import logging # pylint: disable=import-outside-toplevel
44
+
45
+ import yaml # pylint: disable=import-outside-toplevel
46
+ from rich.console import Console # pylint: disable=import-outside-toplevel
47
+
48
+ from gwsim.cli.repository.utils import get_zenodo_client # pylint: disable=import-outside-toplevel
49
+
50
+ logger = logging.getLogger("gwsim")
51
+ console = Console()
52
+
53
+ client = get_zenodo_client(sandbox=sandbox, token=token)
54
+
55
+ if title is None:
56
+ title = typer.prompt("Deposition Title")
57
+
58
+ if description is None:
59
+ description = typer.prompt("Deposition Description", default="")
60
+
61
+ metadata_dict = {"title": title}
62
+ metadata_dict["description"] = description
63
+
64
+ if metadata_file:
65
+ if not metadata_file.exists():
66
+ console.print(f"[red]Error:[/red] Metadata file not found: {metadata_file}")
67
+ raise typer.Exit(1)
68
+ with metadata_file.open("r") as f:
69
+ extra = yaml.safe_load(f)
70
+ if extra:
71
+ metadata_dict.update(extra)
72
+
73
+ console.print("[bold blue]Creating deposition...[/bold blue]")
74
+ try:
75
+ result = client.create_deposition(metadata=metadata_dict)
76
+ deposition_id = result.get("id")
77
+ if sandbox:
78
+ console.print("[yellow]Note:[/yellow] Created in Zenodo Sandbox environment.")
79
+ console.print("[green]✓ Deposition created successfully![/green]")
80
+ console.print(f" [cyan]ID:[/cyan] {deposition_id}")
81
+ if sandbox:
82
+ console.print(
83
+ "[yellow]Note:[/yellow] This is a sandbox deposition. Use [bold]--sandbox[/bold] to access it later."
84
+ )
85
+ console.print(f" [cyan]Next:[/cyan] gwsim repository upload {deposition_id} --file <path> --sandbox")
86
+ else:
87
+ console.print(f" [cyan]Next:[/cyan] gwsim repository upload {deposition_id} --file <path>")
88
+ except Exception as e:
89
+ console.print(f"[red]✗ Failed to create deposition: {e}[/red]")
90
+ logger.error("Create deposition failed: %s", e)
91
+ raise typer.Exit(1) from e
@@ -0,0 +1,51 @@
1
+ """CLI for deleting Zenodo repository depositions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+
10
+ def delete_command(
11
+ deposition_id: Annotated[str, typer.Argument(help="Deposition ID")],
12
+ sandbox: Annotated[bool, typer.Option("--sandbox", help="Use sandbox environment")] = False,
13
+ token: Annotated[str | None, typer.Option("--token", help="Zenodo access token")] = None,
14
+ force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation prompt")] = False,
15
+ ) -> None:
16
+ """Delete an unpublished deposition.
17
+
18
+ Warning: Only unpublished (draft) depositions can be deleted.
19
+
20
+ Examples:
21
+ gwsim repository delete 123456
22
+ gwsim repository delete 123456 --force
23
+ """
24
+ import logging # pylint: disable=import-outside-toplevel
25
+
26
+ from rich.console import Console # pylint: disable=import-outside-toplevel
27
+ from rich.prompt import Confirm # pylint: disable=import-outside-toplevel
28
+
29
+ from gwsim.cli.repository.utils import get_zenodo_client # pylint: disable=import-outside-toplevel
30
+
31
+ logger = logging.getLogger("gwsim")
32
+ console = Console()
33
+
34
+ if not force and not Confirm.ask(
35
+ f"[red bold]Delete deposition {deposition_id}?[/red bold] [dim]This cannot be undone.[/dim]",
36
+ console=console,
37
+ default=False,
38
+ ):
39
+ console.print("[yellow]Cancelled.[/yellow]")
40
+ raise typer.Exit(0)
41
+
42
+ client = get_zenodo_client(sandbox=sandbox, token=token)
43
+
44
+ console.print(f"[bold blue]Deleting deposition {deposition_id}...[/bold blue]")
45
+ try:
46
+ client.delete_deposition(deposition_id)
47
+ console.print("[green]✓ Deposition deleted[/green]")
48
+ except Exception as e:
49
+ console.print(f"[red]✗ Failed to delete: {e}[/red]")
50
+ logger.error("Delete failed: %s", e)
51
+ raise typer.Exit(1) from e
@@ -0,0 +1,54 @@
1
+ """CLI for downloading Zenodo repository files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+
11
+ def download_command(
12
+ deposition_id: Annotated[str | None, typer.Argument(help="Deposition ID")] = None,
13
+ filename: Annotated[str | None, typer.Option("--file", help="Filename to download")] = None,
14
+ output: Annotated[Path | None, typer.Option("--output", help="Output file path")] = None,
15
+ sandbox: Annotated[bool, typer.Option("--sandbox", help="Use sandbox environment")] = False,
16
+ file_size_mb: Annotated[int | None, typer.Option("--file-size-mb", help="File size in MB (for timeout)")] = None,
17
+ ) -> None:
18
+ """Download a file from a published Zenodo record.
19
+
20
+ Examples:
21
+ gwsim repository download 10.5281/zenodo.123456 --file data.gwf --output ./data.gwf
22
+ gwsim repository download 10.5281/zenodo.123456 --file metadata.yaml
23
+ """
24
+ import logging # pylint: disable=import-outside-toplevel
25
+
26
+ from rich.console import Console # pylint: disable=import-outside-toplevel
27
+
28
+ from gwsim.cli.repository.utils import get_zenodo_client # pylint: disable=import-outside-toplevel
29
+
30
+ logger = logging.getLogger("gwsim")
31
+ console = Console()
32
+
33
+ if not deposition_id:
34
+ deposition_id = typer.prompt("Deposition ID (e.g., 123456)")
35
+ if not filename:
36
+ filename = typer.prompt("Filename to download")
37
+ if not output and filename:
38
+ output = Path(filename)
39
+ else:
40
+ console.print("[red]Error:[/red] Output path must be specified.")
41
+ raise typer.Exit(1)
42
+
43
+ client = get_zenodo_client(sandbox=sandbox, token=None) # Downloads don't require auth
44
+
45
+ console.print(f"[bold blue]Downloading {filename} from {deposition_id}...[/bold blue]")
46
+
47
+ try:
48
+ client.download_file(str(deposition_id), filename, output, file_size_in_mb=file_size_mb)
49
+ console.print("[green]✓ Downloaded successfully[/green]")
50
+ console.print(f" [cyan]Saved to:[/cyan] {output.resolve()}")
51
+ except Exception as e:
52
+ console.print(f"[red]✗ Download failed: {e}[/red]")
53
+ logger.error("Download failed: %s", e)
54
+ raise typer.Exit(1) from e
@@ -0,0 +1,63 @@
1
+ """CLI for managing Zenodo repositories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+
10
+ def list_depositions_command( # pylint: disable=import-outside-toplevel,too-many-locals
11
+ status: Annotated[
12
+ str, typer.Option("--status", help="Filter by status (draft, published, unsubmitted)")
13
+ ] = "published",
14
+ sandbox: Annotated[bool, typer.Option("--sandbox", help="Use sandbox environment")] = False,
15
+ token: Annotated[str | None, typer.Option("--token", help="Zenodo access token")] = None,
16
+ ) -> None:
17
+ """List depositions for the authenticated user.
18
+
19
+ Examples:
20
+ gwsim repository list
21
+ gwsim repository list --status draft
22
+ gwsim repository list --status published --sandbox
23
+ """
24
+ import logging
25
+
26
+ from rich.console import Console
27
+ from rich.table import Table
28
+
29
+ from gwsim.cli.repository.utils import get_zenodo_client
30
+
31
+ logger = logging.getLogger("gwsim")
32
+ console = Console()
33
+
34
+ client = get_zenodo_client(sandbox=sandbox, token=token)
35
+
36
+ console.print(f"[bold blue]Listing {status} depositions...[/bold blue]")
37
+ try:
38
+ depositions = client.list_depositions(status=status)
39
+
40
+ if not depositions:
41
+ console.print(f"[yellow]No {status} depositions found.[/yellow]")
42
+ return
43
+
44
+ table = Table(title=f"{status.capitalize()} Depositions")
45
+ table.add_column("ID", style="cyan", width=12)
46
+ table.add_column("Title", style="green", width=40)
47
+ table.add_column("DOI", style="blue", width=20)
48
+ table.add_column("Created", style="magenta", width=12)
49
+
50
+ for dep in depositions:
51
+ dep_id = str(dep.get("id", "N/A"))
52
+ title = dep.get("metadata", {}).get("title", "N/A")
53
+ if len(title) > 38:
54
+ title = title[:35] + "..."
55
+ doi = dep.get("doi", "N/A")
56
+ created = dep.get("created", "N/A")[:10]
57
+ table.add_row(dep_id, title, doi, created)
58
+
59
+ console.print(table)
60
+ except Exception as e:
61
+ console.print(f"[red]✗ Failed to list depositions: {e}[/red]")
62
+ logger.error("List depositions failed: %s", e)
63
+ raise typer.Exit(1) from e