-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathcli.py
More file actions
224 lines (184 loc) · 6.94 KB
/
Copy pathcli.py
File metadata and controls
224 lines (184 loc) · 6.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
from pathlib import Path
from typing import List, Optional
import typer
import yaml
from pydantic import BaseModel
from dvuploader import DVUploader, File
from dvuploader.utils import add_directory
class CliInput(BaseModel):
"""
Model for CLI input parameters.
Attributes:
api_token (str): API token for authentication with Dataverse
dataverse_url (str): URL of the Dataverse instance
persistent_id (str): Persistent identifier of the dataset
files (List[File]): List of files to upload
n_jobs (int): Number of parallel upload jobs to run (default: 1)
"""
api_token: str
dataverse_url: str
persistent_id: str
files: List[File]
n_jobs: int = 1
app = typer.Typer()
def _enumerate_filepaths(filepaths: List[str], recurse: bool) -> List[File]:
"""
Take a list of filepaths and transform it into a list of File objects, optionally recursing into each of them.
Args:
filepaths (List[str]): a list of files or paths for upload
recurse (bool): whether to recurse into each given filepath
Returns:
List[File]: A list of File objects representing the files extracted from all filepaths.
Raises:
FileNotFoundError: If a filepath does not exist.
IsADirectoryError: If recurse is False and a filepath points to a directory instead of a file.
"""
if not recurse:
return [File(filepath=filepath) for filepath in filepaths]
files = []
for fp in filepaths:
files.extend(add_directory(fp) if Path(fp).is_dir() else [File(filepath=fp)])
return files
def _parse_yaml_config(path: str) -> CliInput:
"""
Parse a YAML/JSON configuration file into a CliInput object.
Args:
path (str): Path to a YAML/JSON configuration file containing upload specifications
Returns:
CliInput: Object containing upload configuration parameters
Raises:
yaml.YAMLError: If the YAML/JSON file is malformed
ValidationError: If the configuration data does not match the CliInput model
"""
return CliInput(**yaml.safe_load(open(path))) # type: ignore
def _validate_inputs(
filepaths: List[str],
pid: str,
dataverse_url: str,
api_token: str,
recurse: bool,
config_path: Optional[str],
) -> None:
"""
Validate CLI input parameters.
Checks for valid combinations of configuration file and command line parameters.
Args:
filepaths (List[str]): List of files to upload
pid (str): Persistent identifier of the dataset
dataverse_url (str): URL of the Dataverse instance
api_token (str): API token for authentication
recurse (bool): Whether to recurse into filepaths
config_path (Optional[str]): Path to configuration file
Raises:
typer.BadParameter: If both config file and filepaths are specified
typer.BadParameter: If both config file and recurse are specified
typer.BadParameter: If neither config file nor required parameters are provided
"""
if config_path is not None:
if len(filepaths) > 0:
raise typer.BadParameter(
"Cannot specify both a JSON/YAML file and a list of filepaths."
)
if recurse:
raise typer.BadParameter(
"Cannot specify both a JSON/YAML file and recurse into filepaths."
)
_has_meta_params = all(arg is not None for arg in [pid, dataverse_url, api_token])
_has_config_file = config_path is not None
if _has_meta_params and _has_config_file:
print(
"\n⚠️ Warning\n"
"├── You have specified both a configuration file and metadata parameters via the command line.\n"
"╰── Will use metadata parameters specified in the config file."
)
elif not _has_meta_params and not _has_config_file:
raise typer.BadParameter(
"You must specify either a JSON/YAML file or metadata parameters (dv_url, api_token, pid, files) via the command line."
)
@app.command()
def main(
filepaths: Optional[List[str]] = typer.Argument(
default=None,
help="A list of filepaths to upload.",
),
recurse: Optional[bool] = typer.Option(
default=False,
help="Enable recursion into filepaths.",
),
pid: str = typer.Option(
default=None,
help="The persistent identifier of the Dataverse dataset.",
),
api_token: str = typer.Option(
default=None,
help="The API token for the Dataverse repository.",
),
dataverse_url: str = typer.Option(
default=None,
help="The URL of the Dataverse repository.",
),
config_path: Optional[str] = typer.Option(
default=None,
help="Path to a JSON/YAML file containing specifications for the files to upload.",
),
n_jobs: int = typer.Option(
default=1,
help="Number of parallel upload jobs to run.",
),
):
"""
Upload files to a Dataverse repository.
Files can be specified either directly via command line arguments or through a
configuration file. The configuration file can be either YAML or JSON format.
If using command line arguments, you must specify:
- One or more filepaths to upload
- (Optional) whether to recurse into the filepaths
- The dataset's persistent identifier
- A valid API token
- The Dataverse repository URL
If using a configuration file, it should contain:
- api_token: API token for authentication
- dataverse_url: URL of the Dataverse instance
- persistent_id: Dataset persistent identifier
- files: List of file specifications
- n_jobs: (optional) Number of parallel upload jobs
Examples:
Upload files via command line:
$ dvuploader file1.txt file2.txt --pid doi:10.5072/FK2/123456 --api-token abc123 --dataverse-url https://demo.dataverse.org
Upload files via config file:
$ dvuploader --config-path upload_config.yaml
"""
if filepaths is None:
filepaths = []
if recurse is None:
recurse = False
_validate_inputs(
filepaths=filepaths,
pid=pid,
dataverse_url=dataverse_url,
api_token=api_token,
recurse=recurse,
config_path=config_path,
)
if config_path:
# PyYAML is a superset of JSON, so we can use the same function to parse both
cli_input = _parse_yaml_config(config_path)
else:
cli_input = CliInput(
api_token=api_token,
dataverse_url=dataverse_url,
persistent_id=pid,
files=_enumerate_filepaths(
filepaths=filepaths,
recurse=recurse,
),
)
uploader = DVUploader(files=cli_input.files)
uploader.upload(
persistent_id=cli_input.persistent_id,
dataverse_url=cli_input.dataverse_url,
api_token=cli_input.api_token,
n_parallel_uploads=n_jobs,
)
if __name__ == "__main__":
app()