esgpull 0.6.3__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 (80) hide show
  1. esgpull/__init__.py +12 -0
  2. esgpull/auth.py +181 -0
  3. esgpull/cli/__init__.py +73 -0
  4. esgpull/cli/add.py +103 -0
  5. esgpull/cli/autoremove.py +38 -0
  6. esgpull/cli/config.py +116 -0
  7. esgpull/cli/convert.py +285 -0
  8. esgpull/cli/decorators.py +342 -0
  9. esgpull/cli/download.py +74 -0
  10. esgpull/cli/facet.py +23 -0
  11. esgpull/cli/get.py +28 -0
  12. esgpull/cli/install.py +85 -0
  13. esgpull/cli/link.py +105 -0
  14. esgpull/cli/login.py +56 -0
  15. esgpull/cli/remove.py +73 -0
  16. esgpull/cli/retry.py +43 -0
  17. esgpull/cli/search.py +201 -0
  18. esgpull/cli/self.py +238 -0
  19. esgpull/cli/show.py +66 -0
  20. esgpull/cli/status.py +67 -0
  21. esgpull/cli/track.py +87 -0
  22. esgpull/cli/update.py +184 -0
  23. esgpull/cli/utils.py +247 -0
  24. esgpull/config.py +410 -0
  25. esgpull/constants.py +56 -0
  26. esgpull/context.py +724 -0
  27. esgpull/database.py +161 -0
  28. esgpull/download.py +162 -0
  29. esgpull/esgpull.py +447 -0
  30. esgpull/exceptions.py +167 -0
  31. esgpull/fs.py +253 -0
  32. esgpull/graph.py +460 -0
  33. esgpull/install_config.py +185 -0
  34. esgpull/migrations/README +1 -0
  35. esgpull/migrations/env.py +82 -0
  36. esgpull/migrations/script.py.mako +24 -0
  37. esgpull/migrations/versions/0.3.0_update_tables.py +170 -0
  38. esgpull/migrations/versions/0.3.1_update_tables.py +25 -0
  39. esgpull/migrations/versions/0.3.2_update_tables.py +26 -0
  40. esgpull/migrations/versions/0.3.3_update_tables.py +25 -0
  41. esgpull/migrations/versions/0.3.4_update_tables.py +25 -0
  42. esgpull/migrations/versions/0.3.5_update_tables.py +25 -0
  43. esgpull/migrations/versions/0.3.6_update_tables.py +26 -0
  44. esgpull/migrations/versions/0.3.7_update_tables.py +26 -0
  45. esgpull/migrations/versions/0.3.8_update_tables.py +26 -0
  46. esgpull/migrations/versions/0.4.0_update_tables.py +25 -0
  47. esgpull/migrations/versions/0.5.0_update_tables.py +26 -0
  48. esgpull/migrations/versions/0.5.1_update_tables.py +26 -0
  49. esgpull/migrations/versions/0.5.2_update_tables.py +25 -0
  50. esgpull/migrations/versions/0.5.3_update_tables.py +26 -0
  51. esgpull/migrations/versions/0.5.4_update_tables.py +25 -0
  52. esgpull/migrations/versions/0.5.5_update_tables.py +25 -0
  53. esgpull/migrations/versions/0.6.0_update_tables.py +25 -0
  54. esgpull/migrations/versions/0.6.1_update_tables.py +25 -0
  55. esgpull/migrations/versions/0.6.2_update_tables.py +25 -0
  56. esgpull/migrations/versions/0.6.3_update_tables.py +25 -0
  57. esgpull/models/__init__.py +31 -0
  58. esgpull/models/base.py +50 -0
  59. esgpull/models/dataset.py +34 -0
  60. esgpull/models/facet.py +18 -0
  61. esgpull/models/file.py +65 -0
  62. esgpull/models/options.py +164 -0
  63. esgpull/models/query.py +481 -0
  64. esgpull/models/selection.py +201 -0
  65. esgpull/models/sql.py +258 -0
  66. esgpull/models/synda_file.py +85 -0
  67. esgpull/models/tag.py +19 -0
  68. esgpull/models/utils.py +54 -0
  69. esgpull/presets.py +13 -0
  70. esgpull/processor.py +172 -0
  71. esgpull/py.typed +0 -0
  72. esgpull/result.py +53 -0
  73. esgpull/tui.py +346 -0
  74. esgpull/utils.py +54 -0
  75. esgpull/version.py +1 -0
  76. esgpull-0.6.3.dist-info/METADATA +110 -0
  77. esgpull-0.6.3.dist-info/RECORD +80 -0
  78. esgpull-0.6.3.dist-info/WHEEL +4 -0
  79. esgpull-0.6.3.dist-info/entry_points.txt +3 -0
  80. esgpull-0.6.3.dist-info/licenses/LICENSE +28 -0
esgpull/cli/convert.py ADDED
@@ -0,0 +1,285 @@
1
+ from collections import Counter
2
+ from collections.abc import MutableMapping
3
+ from pathlib import Path
4
+
5
+ import click
6
+ import pyparsing as pp
7
+ import yaml
8
+ from click.exceptions import Abort, Exit
9
+ from rich.box import MINIMAL_DOUBLE_HEAD
10
+ from rich.prompt import Confirm
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+
14
+ from esgpull.cli.decorators import opts
15
+ from esgpull.cli.utils import init_esgpull
16
+ from esgpull.graph import Graph
17
+ from esgpull.models import Options, Query, Tag
18
+ from esgpull.models.selection import FacetValues, Selection
19
+ from esgpull.tui import Verbosity, logger
20
+
21
+ SKIP = {"priority", "protocol"}
22
+ options_names = Options()._names
23
+ pp.ParserElement.set_default_whitespace_chars(" \t,") # remove newline
24
+
25
+
26
+ def line(expr: pp.ParserElement) -> pp.ParserElement:
27
+ return pp.LineStart().suppress() + expr + pp.LineEnd().suppress()
28
+
29
+
30
+ def sqbr(expr: pp.ParserElement) -> pp.ParserElement:
31
+ return pp.Suppress("[") + expr + pp.Suppress("]")
32
+
33
+
34
+ anything = pp.Word(pp.printables)
35
+ word = pp.Word(pp.alphanums + "-_")
36
+ eq = pp.Suppress("=")
37
+
38
+ comment_start = pp.Char("#")[1, ...].suppress()
39
+ name_comment = pp.Group(line(comment_start + anything[1])[0, 1])
40
+ comment = line(comment_start + anything[...]).suppress()
41
+ facet_name = word("name")
42
+ facet_value = word
43
+ facet_values = pp.Group(word[1, ...])
44
+ facet = pp.Group(line(facet_name + eq + facet_values("vals")))("facet")
45
+ variable_cmip5 = pp.Group(
46
+ line(
47
+ "variable"
48
+ + sqbr(facet_value)("realm")
49
+ + sqbr(facet_value)("time_frequency")
50
+ + eq
51
+ + facet_values("variable")
52
+ )
53
+ )("variable_cmip5")
54
+ variable_cmip6 = pp.Group(
55
+ line(
56
+ "variable"
57
+ + sqbr(pp.Opt("table_id=") + facet_value("table_id"))
58
+ + eq
59
+ + facet_values("variable_id")
60
+ )
61
+ )("variable_cmip6")
62
+ variable_no_sqbr = pp.Group(line("variable" + eq + facet_values("variable")))(
63
+ "variable_no_sqbr"
64
+ )
65
+ otherwise = line(anything[...])("otherwise")
66
+ rest = pp.Group(
67
+ comment
68
+ | facet
69
+ | variable_no_sqbr
70
+ | variable_cmip5
71
+ | variable_cmip6
72
+ | otherwise
73
+ )[...]
74
+ selection_file = name_comment("name") + rest("rest")
75
+
76
+
77
+ def remove_duplicates(
78
+ selection: MutableMapping[str, FacetValues],
79
+ ) -> MutableMapping[str, FacetValues]:
80
+ result: dict[str, FacetValues] = {}
81
+ duplicates: dict[str, list[str]] = {}
82
+ for name, values in selection.items():
83
+ if isinstance(values, str):
84
+ result[name] = values
85
+ continue
86
+ counter = Counter(values)
87
+ nb_dup = sum(c > 1 for c in counter.values())
88
+ if nb_dup == 0:
89
+ result[name] = values
90
+ continue
91
+ duplicates[name] = list(dict(counter.most_common(nb_dup)))
92
+ if duplicates:
93
+ logger.warning(f"Duplicate values {duplicates}'")
94
+ return result
95
+
96
+
97
+ def is_CMIP6(q: Query) -> bool:
98
+ project = set(q.selection["project"] + q.selection["mip_era"])
99
+ return "CMIP6" in project
100
+
101
+
102
+ def isnot_CMIP6(q: Query) -> bool:
103
+ project = set(q.selection["project"] + q.selection["mip_era"])
104
+ return any({"CMIP5", "CORDEX"} & project)
105
+
106
+
107
+ def fix_CMIP5(q: Query) -> Query:
108
+ qd = q.asdict()
109
+ if q.selection["frequency"]:
110
+ qd["selection"]["time_frequency"] = qd["selection"].pop("frequency")
111
+ return Query(**qd)
112
+
113
+
114
+ def convert_file(path: Path) -> Graph:
115
+ logger.info(path)
116
+ query = Query()
117
+ kids: list[Query] = []
118
+ result = selection_file.parse_file(path)
119
+ if result.name:
120
+ # name = result.name[0].split("@", 1)[0]
121
+ query.tags.append(Tag(name=result.name[0]))
122
+ for line in result.rest:
123
+ if line.facet:
124
+ name, values = line.facet.as_list()
125
+ if name in SKIP:
126
+ continue
127
+ elif name in options_names:
128
+ if len(values) > 1:
129
+ raise ValueError({name: values})
130
+ d = {name: values[0]}
131
+ logger.debug(f"OPTION {d}")
132
+ setattr(query.options, name, values[0])
133
+ elif name in Selection._facet_names:
134
+ d = {name: values}
135
+ logger.debug(f"FACET {d}")
136
+ query.selection[name] = list(set(values))
137
+ else:
138
+ raise ValueError(f"{name!r} undefined\n{path.read_text()}")
139
+ elif line.variable_cmip5:
140
+ selection = line.variable_cmip5.as_dict()
141
+ logger.debug(f"SUBQUERY {selection}")
142
+ kid = Query(selection=remove_duplicates(selection), tracked=True)
143
+ kids.append(kid)
144
+ elif line.variable_cmip6:
145
+ selection = line.variable_cmip6.as_dict()
146
+ project = query.selection["project"]
147
+ if project and project[0] in ["CMIP5", "CORDEX"]:
148
+ if "variable_id" in selection:
149
+ selection["variable"] = selection.pop("variable_id")
150
+ if "table_id" in selection:
151
+ selection["time_frequency"] = selection.pop("table_id")
152
+ logger.debug(f"SUBQUERY {selection}")
153
+ kid = Query(selection=remove_duplicates(selection))
154
+ kids.append(kid)
155
+ elif line.variable_no_sqbr:
156
+ logger.error(line.as_dict())
157
+ elif line.as_list():
158
+ logger.error(line.as_list())
159
+ if len(kids) > 1:
160
+ query.untrack()
161
+ elif len(kids) == 1:
162
+ query = query << kids.pop()
163
+ else:
164
+ query.track(query.options)
165
+ if isnot_CMIP6(query):
166
+ query = fix_CMIP5(query)
167
+ query.compute_sha()
168
+ graph = Graph(None)
169
+ graph.add(query)
170
+ for kid in kids:
171
+ if isnot_CMIP6(query) or isnot_CMIP6(kid):
172
+ kid = fix_CMIP5(kid)
173
+ kid.require = query.sha
174
+ kid.compute_sha()
175
+ expanded = query << kid
176
+ kid.track(expanded.options)
177
+ graph.add(kid)
178
+ return graph
179
+
180
+
181
+ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
182
+
183
+
184
+ @click.command(context_settings=CONTEXT_SETTINGS)
185
+ @click.argument(
186
+ "paths",
187
+ required=True,
188
+ type=click.Path(exists=True, path_type=Path),
189
+ nargs=-1,
190
+ )
191
+ @click.option(
192
+ "--out",
193
+ "-o",
194
+ type=click.Path(exists=False, path_type=Path),
195
+ default=None,
196
+ )
197
+ @click.option("print_table", "--table", is_flag=True, default=False)
198
+ @click.option("print_graph", "--graph", is_flag=True, default=False)
199
+ @opts.record
200
+ @opts.verbosity
201
+ def convert(
202
+ paths: list[Path],
203
+ out: Path | None,
204
+ print_table: bool,
205
+ print_graph: bool,
206
+ record: bool,
207
+ verbosity: Verbosity,
208
+ ):
209
+ """
210
+ Convert synda selection files to esgpull queries
211
+
212
+ The flags `--table/--graph` are used to select the console output design.
213
+
214
+ Use `--out <path/to/output.yaml>` to generate a query file containing the converted synda
215
+ selection files. This file can be used as an input to the `add --query-file` command.
216
+
217
+ Note that `convert` takes any number of input paths, and produces a single output.
218
+ Using `**/*` as the input path is a good way to convert a whole tree of selection files.
219
+
220
+ As a an arbitrary convention, the generated queries will be tagged with the content of the first line of a synda selection file, if that line starts with `#` and has a single word (+symbols) without whitespaces in it.
221
+ """
222
+ esg = init_esgpull(verbosity, safe=False, record=record)
223
+ with esg.ui.logging("convert", onraise=Abort):
224
+ if len(paths) == 0:
225
+ esg.ui.print("No file provided")
226
+ raise click.exceptions.Exit(0)
227
+ if out is not None:
228
+ out_str = f"[yellow]{out.resolve()}[/]"
229
+ if out.is_file():
230
+ esg.ui.print(f"Overwriting existing file {out_str}")
231
+ allow = Confirm.ask("Continue?", default=False)
232
+ if not allow:
233
+ raise click.exceptions.Exit(0)
234
+ if out is None and not print_table and not print_graph:
235
+ esg.ui.print(
236
+ "[red]Error[/]: At least one of "
237
+ "[yellow]--out/--table/--graph[/] is required."
238
+ )
239
+ raise click.exceptions.Exit(1)
240
+ table = Table(
241
+ box=MINIMAL_DOUBLE_HEAD,
242
+ show_edge=False,
243
+ show_lines=True,
244
+ )
245
+ table.add_column(Text("path", justify="left"))
246
+ table.add_column(Text("file", justify="center"))
247
+ table.add_column(Text("query", justify="center"))
248
+ full_graph = Graph(None)
249
+ empty_query = Query()
250
+ empty_query.options.apply_defaults(empty_query.options)
251
+ empty_query.compute_sha()
252
+ for path in paths:
253
+ if path.is_file():
254
+ try:
255
+ graph = convert_file(path)
256
+ except Exception as e:
257
+ msg = f"Cannot convert {path.resolve()}"
258
+ logger.exception(e)
259
+ logger.error(msg)
260
+ esg.ui.print(f"[red]Error[/]: {msg}")
261
+ continue
262
+ queries = list(graph.queries.values())
263
+ if len(queries) == 1 and queries[0].sha == empty_query.sha:
264
+ esg.ui.print(
265
+ f"Skipping empty query produced from {path.resolve()}"
266
+ )
267
+ continue
268
+ path_text = Text(str(path).replace("/", "/\n"), style="yellow")
269
+ file_text = Text(path.read_text())
270
+ table.add_row(path_text, file_text, graph)
271
+ full_graph.add(*queries, force=True)
272
+ if print_table:
273
+ esg.ui.print(table)
274
+ if print_graph:
275
+ esg.ui.print(full_graph)
276
+ if out is not None:
277
+ graph_as_yaml = yaml.dump(full_graph.dump())
278
+ try:
279
+ with out.open("w") as f:
280
+ f.write(graph_as_yaml)
281
+ esg.ui.print(f":+1: [green]Graph was written to[/] {out_str}")
282
+ except Exception:
283
+ out.unlink()
284
+ raise
285
+ esg.ui.raise_maybe_record(Exit(0))
@@ -0,0 +1,342 @@
1
+ from collections.abc import Callable
2
+ from pathlib import Path
3
+ from typing import Any, TypeAlias, TypeVar
4
+
5
+ import click
6
+ from click_params import StringListParamType
7
+
8
+ from esgpull.cli.utils import EnumParam
9
+ from esgpull.models import FileStatus, Option
10
+ from esgpull.tui import Verbosity
11
+
12
+ F = TypeVar("F", bound=Callable[..., Any])
13
+ Dec: TypeAlias = Callable[[F], F]
14
+
15
+
16
+ def compose(*decs: Dec) -> Dec:
17
+ def deco(fn: Callable):
18
+ for dec in reversed(decs):
19
+ fn = dec(fn)
20
+ return fn
21
+
22
+ return deco
23
+
24
+
25
+ class args:
26
+ facets: Dec = click.argument(
27
+ "facets",
28
+ type=str,
29
+ nargs=-1,
30
+ )
31
+ key: Dec = click.argument(
32
+ "key",
33
+ type=str,
34
+ nargs=1,
35
+ required=False,
36
+ default=None,
37
+ )
38
+ value: Dec = click.argument(
39
+ "value",
40
+ type=str,
41
+ nargs=1,
42
+ required=False,
43
+ default=None,
44
+ )
45
+ path: Dec = click.argument(
46
+ "path",
47
+ type=click.Path(exists=False, path_type=Path),
48
+ required=False,
49
+ default=None,
50
+ )
51
+ status: Dec = click.argument(
52
+ "status",
53
+ type=EnumParam(FileStatus),
54
+ nargs=-1,
55
+ )
56
+ query_id: Dec = click.argument(
57
+ "query_id",
58
+ type=str,
59
+ nargs=1,
60
+ required=False,
61
+ )
62
+ query_ids: Dec = click.argument(
63
+ "query_ids",
64
+ type=str,
65
+ nargs=-1,
66
+ required=True,
67
+ )
68
+
69
+
70
+ class opts:
71
+ all: Dec = click.option(
72
+ "all_",
73
+ "--all",
74
+ "-a",
75
+ is_flag=True,
76
+ default=False,
77
+ )
78
+ children: Dec = click.option(
79
+ "--children",
80
+ "-c",
81
+ is_flag=True,
82
+ default=False,
83
+ )
84
+ default: Dec = click.option(
85
+ "--default",
86
+ "-d",
87
+ is_flag=True,
88
+ default=False,
89
+ )
90
+ detail: Dec = click.option(
91
+ "--detail",
92
+ type=int,
93
+ default=None,
94
+ )
95
+ disable_ssl: Dec = click.option(
96
+ "--disable-ssl",
97
+ is_flag=True,
98
+ default=False,
99
+ )
100
+ dry_run: Dec = click.option(
101
+ "--dry-run",
102
+ "-z",
103
+ is_flag=True,
104
+ default=False,
105
+ )
106
+ facets_hints: Dec = click.option(
107
+ "facets_hints",
108
+ "--facets",
109
+ "-F",
110
+ is_flag=True,
111
+ default=False,
112
+ )
113
+ file: Dec = click.option(
114
+ "--file",
115
+ "-f",
116
+ is_flag=True,
117
+ default=False,
118
+ )
119
+ files: Dec = click.option(
120
+ "--files",
121
+ is_flag=True,
122
+ default=False,
123
+ )
124
+ force: Dec = click.option(
125
+ "--force",
126
+ is_flag=True,
127
+ default=False,
128
+ )
129
+ generate: Dec = click.option(
130
+ "--generate",
131
+ is_flag=True,
132
+ default=False,
133
+ )
134
+ hints: Dec = click.option(
135
+ "--hints",
136
+ "-H",
137
+ type=StringListParamType(","),
138
+ default=None,
139
+ )
140
+ name: Dec = click.option(
141
+ "--name",
142
+ "-n",
143
+ type=str,
144
+ default=None,
145
+ )
146
+ query_file: Dec = click.option(
147
+ "--query-file",
148
+ "-q",
149
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
150
+ default=None,
151
+ )
152
+ quiet: Dec = click.option(
153
+ "--quiet",
154
+ is_flag=True,
155
+ default=False,
156
+ )
157
+ record: Dec = click.option(
158
+ "--record",
159
+ is_flag=True,
160
+ default=False,
161
+ )
162
+ reset: Dec = click.option(
163
+ "--reset",
164
+ is_flag=True,
165
+ default=False,
166
+ )
167
+ shas: Dec = click.option(
168
+ "--shas",
169
+ "-s",
170
+ is_flag=True,
171
+ default=False,
172
+ )
173
+ show: Dec = click.option(
174
+ "--show",
175
+ is_flag=True,
176
+ default=False,
177
+ )
178
+ simple: Dec = click.option(
179
+ "--simple",
180
+ is_flag=True,
181
+ default=False,
182
+ )
183
+ since: Dec = click.option(
184
+ "--since",
185
+ type=str,
186
+ default=None,
187
+ )
188
+ status: Dec = click.option(
189
+ "--status",
190
+ type=EnumParam(FileStatus),
191
+ default=None,
192
+ multiple=True,
193
+ )
194
+ tag: Dec = click.option(
195
+ "--tag",
196
+ "-t",
197
+ type=str,
198
+ default=None,
199
+ )
200
+ track: Dec = click.option(
201
+ "--track",
202
+ is_flag=True,
203
+ default=False,
204
+ )
205
+ yes: Dec = click.option(
206
+ "--yes",
207
+ "-y",
208
+ is_flag=True,
209
+ default=False,
210
+ )
211
+ verbosity: Dec = click.option(
212
+ "verbosity",
213
+ "-v",
214
+ count=True,
215
+ type=EnumParam(Verbosity),
216
+ )
217
+
218
+
219
+ # Display group
220
+ _page: Dec = click.option(
221
+ "--page",
222
+ "-p",
223
+ type=int,
224
+ default=0, # 1?
225
+ )
226
+ _zero: Dec = click.option(
227
+ "--zero",
228
+ "-0",
229
+ is_flag=True,
230
+ default=False,
231
+ )
232
+ _all: Dec = click.option(
233
+ "_all",
234
+ "--all",
235
+ "-a",
236
+ is_flag=True,
237
+ default=False,
238
+ )
239
+
240
+ # Json/Yaml exclusive group
241
+ _json: Dec = click.option(
242
+ "--json",
243
+ is_flag=True,
244
+ default=False,
245
+ )
246
+ _yaml: Dec = click.option(
247
+ "--yaml",
248
+ is_flag=True,
249
+ default=False,
250
+ )
251
+
252
+ # Query definition group
253
+ OptionChoice = click.Choice([opt.name for opt in list(Option)[:-1]])
254
+ _tag: Dec = click.option("tags", "--tag", "-t", multiple=True)
255
+ _require: Dec = click.option(
256
+ "--require",
257
+ "-r",
258
+ type=str,
259
+ nargs=1,
260
+ required=False,
261
+ default=None,
262
+ )
263
+ _distrib: Dec = click.option(
264
+ "--distrib",
265
+ "-d",
266
+ type=OptionChoice,
267
+ )
268
+ _latest: Dec = click.option(
269
+ "--latest",
270
+ type=OptionChoice,
271
+ )
272
+ _replica: Dec = click.option(
273
+ "--replica",
274
+ type=OptionChoice,
275
+ )
276
+ _retracted: Dec = click.option(
277
+ "--retracted",
278
+ type=OptionChoice,
279
+ )
280
+
281
+ # Query dates group
282
+ datetime_type = click.DateTime(["%Y-%m-%d"])
283
+ _from: Dec = click.option(
284
+ "date_from",
285
+ "--from",
286
+ type=datetime_type,
287
+ default=None,
288
+ )
289
+ _to: Dec = click.option(
290
+ "date_to",
291
+ "--to",
292
+ type=datetime_type,
293
+ default=None,
294
+ )
295
+
296
+ _children: Dec = click.option(
297
+ "--children",
298
+ "-c",
299
+ is_flag=True,
300
+ default=False,
301
+ )
302
+ _parents: Dec = click.option(
303
+ "--parents",
304
+ "-p",
305
+ is_flag=True,
306
+ default=False,
307
+ )
308
+ _expand: Dec = click.option(
309
+ "--expand",
310
+ "-e",
311
+ is_flag=True,
312
+ default=False,
313
+ )
314
+
315
+
316
+ class groups:
317
+ display: Dec = compose(
318
+ _page,
319
+ _zero,
320
+ _all,
321
+ )
322
+ json_yaml: Dec = compose(
323
+ _json,
324
+ _yaml,
325
+ )
326
+ query_def: Dec = compose(
327
+ _tag,
328
+ _require,
329
+ _distrib,
330
+ _latest,
331
+ _replica,
332
+ _retracted,
333
+ )
334
+ query_date: Dec = compose(
335
+ _from,
336
+ _to,
337
+ )
338
+ show: Dec = compose(
339
+ _children,
340
+ _parents,
341
+ _expand,
342
+ )
@@ -0,0 +1,74 @@
1
+ import asyncio
2
+ import sys
3
+
4
+ import click
5
+ import rich
6
+ from click.exceptions import Abort, Exit
7
+
8
+ if sys.version_info < (3, 11):
9
+ from exceptiongroup import BaseExceptionGroup
10
+
11
+ from esgpull.cli.decorators import args, opts
12
+ from esgpull.cli.utils import get_queries, init_esgpull, valid_name_tag
13
+ from esgpull.models import File, FileStatus
14
+ from esgpull.tui import Verbosity, logger
15
+ from esgpull.utils import format_size
16
+
17
+
18
+ @click.command()
19
+ @args.query_id
20
+ @opts.tag
21
+ @opts.disable_ssl
22
+ @opts.quiet
23
+ @opts.record
24
+ @opts.verbosity
25
+ def download(
26
+ query_id: str | None,
27
+ tag: str | None,
28
+ disable_ssl: bool,
29
+ quiet: bool,
30
+ record: bool,
31
+ verbosity: Verbosity,
32
+ ):
33
+ """
34
+ Asynchronously download files linked to queries
35
+ """
36
+ esg = init_esgpull(verbosity, record=record)
37
+ if disable_ssl:
38
+ esg.config.download.disable_ssl = True
39
+ with esg.ui.logging("download", onraise=Abort):
40
+ if not valid_name_tag(esg.graph, esg.ui, query_id, tag):
41
+ esg.ui.raise_maybe_record(Exit(1))
42
+ if query_id is None and tag is None:
43
+ esg.graph.load_db()
44
+ graph = esg.graph
45
+ else:
46
+ queries = get_queries(esg.graph, query_id, tag)
47
+ graph = esg.graph.subgraph(
48
+ *queries,
49
+ children=True,
50
+ parents=True,
51
+ )
52
+ esg.ui.print(graph)
53
+ shas: set[str] = set()
54
+ queue: list[File] = []
55
+ for query in graph.queries.values():
56
+ for file in query.files:
57
+ if file.status == FileStatus.Queued and file.sha not in shas:
58
+ shas.add(file.sha)
59
+ queue.append(file)
60
+ if not queue:
61
+ rich.print("Download queue is empty.")
62
+ esg.ui.raise_maybe_record(Exit(0))
63
+ coro = esg.download(queue, show_progress=not quiet)
64
+ files, errors = asyncio.run(coro)
65
+ if files:
66
+ size = format_size(sum(file.size for file in files))
67
+ esg.ui.print(
68
+ f"Downloaded {len(files)} new files for a total size of {size}"
69
+ )
70
+ if errors:
71
+ logger.error(f"{len(errors)} files could not be installed.")
72
+ exc_group = BaseExceptionGroup("Download", [e.err for e in errors])
73
+ esg.ui.raise_maybe_record(exc_group)
74
+ esg.ui.raise_maybe_record(Exit(0))