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/self.py ADDED
@@ -0,0 +1,238 @@
1
+ import os
2
+ from configparser import ConfigParser
3
+ from pathlib import Path
4
+
5
+ import click
6
+ from click.exceptions import Abort, Exit
7
+ from rich.table import Table
8
+
9
+ from esgpull import Esgpull
10
+ from esgpull.cli.decorators import args, opts
11
+ from esgpull.cli.utils import init_esgpull
12
+ from esgpull.config import Config
13
+ from esgpull.exceptions import (
14
+ InvalidInstallPath,
15
+ UnknownInstallName,
16
+ UnregisteredInstallPath,
17
+ )
18
+ from esgpull.install_config import InstallConfig
19
+ from esgpull.tui import TempUI, Verbosity
20
+
21
+
22
+ def get_synda_db_path(sdt_home: str | None = None) -> Path | None:
23
+ if sdt_home is None:
24
+ sdt_home = os.getenv("SDT_HOME")
25
+ if sdt_home is not None:
26
+ sdt_path = Path(sdt_home).expanduser().resolve()
27
+ conf_path = sdt_path / "conf" / "sdt.conf"
28
+ conf = ConfigParser()
29
+ conf.read(conf_path)
30
+ path = Path(conf["core"]["db_path"]) / "sdt.db"
31
+ if path.is_file():
32
+ db_path = path
33
+ else:
34
+ db_path = None
35
+ else:
36
+ db_path = None
37
+ return db_path
38
+
39
+
40
+ @click.group()
41
+ def self():
42
+ """
43
+ Manage esgpull installations / import synda database
44
+ """
45
+ ...
46
+
47
+
48
+ @self.command()
49
+ @args.path
50
+ @opts.name
51
+ @opts.verbosity
52
+ def install(
53
+ path: Path | None,
54
+ name: str | None,
55
+ verbosity: Verbosity,
56
+ ):
57
+ with TempUI.logging():
58
+ TempUI.rule("[b green]esgpull[/] installation")
59
+ if path is None:
60
+ default = str(InstallConfig.default)
61
+ path = Path(TempUI.prompt("Install location", default=default))
62
+ idx = InstallConfig.index(path=path)
63
+ if idx > -1:
64
+ TempUI.print(
65
+ (
66
+ f"\n:stop_sign: {path} is already installed:\n"
67
+ f" {InstallConfig.installs[idx]}\n\n"
68
+ f"{InstallConfig.activate_msg(idx)}"
69
+ ),
70
+ err=True,
71
+ )
72
+ raise Exit(1)
73
+ if name is None:
74
+ name = TempUI.prompt("Name (optional)") or None
75
+ idx = InstallConfig.add(path, name)
76
+ InstallConfig.choose(idx=idx)
77
+ path = InstallConfig.installs[idx].path
78
+ if path.is_dir():
79
+ TempUI.print(f"Using existing install at {path}")
80
+ else:
81
+ TempUI.print(f"Creating install directory and files at {path}")
82
+ InstallConfig.write()
83
+ TempUI.print(f"Install config added to {InstallConfig.path}")
84
+ esg = Esgpull(verbosity=verbosity, install=True)
85
+ with esg.ui.logging("init", onraise=Abort):
86
+ # with esg.ui.spinner("Fetching facets"):
87
+ # if esg.fetch_facets(update=False):
88
+ # esg.ui.print(":+1: Facets are initialised.")
89
+ # else:
90
+ # esg.ui.print(":+1: Facets were already initialised.")
91
+ sdt_home = os.getenv("SDT_HOME")
92
+ if sdt_home is not None:
93
+ db_name = get_synda_db_path(sdt_home) or ""
94
+ msg = (
95
+ f"Found existing synda installation at {sdt_home}\n"
96
+ "You can import its database by running:\n"
97
+ f"$ esgpull self import_synda {db_name}"
98
+ )
99
+ esg.ui.print(msg)
100
+
101
+
102
+ @self.command()
103
+ @args.path
104
+ @opts.name
105
+ def activate(
106
+ path: Path | None,
107
+ name: str | None,
108
+ ):
109
+ with TempUI.logging():
110
+ idx = InstallConfig.index(path=path, name=name)
111
+ if idx < 0:
112
+ if path is not None and InstallConfig.index(name=path.name) > -1:
113
+ raise ValueError(
114
+ f"{InstallConfig.fullpath(path)} is not installed, "
115
+ "did you mean this?\n\n"
116
+ f"$ esgpull self activate --name {path}"
117
+ )
118
+ raise InvalidInstallPath(path=path)
119
+ TempUI.print(InstallConfig.activate_needs_eval(idx))
120
+
121
+
122
+ @self.command()
123
+ @args.path
124
+ @opts.name
125
+ def choose(
126
+ path: Path | None,
127
+ name: str | None,
128
+ ):
129
+ with TempUI.logging():
130
+ if path is None and name is None:
131
+ table = Table(
132
+ title="Install locations",
133
+ title_style="bold",
134
+ title_justify="left",
135
+ show_header=False,
136
+ show_footer=False,
137
+ show_edge=False,
138
+ box=None,
139
+ padding=(0, 1),
140
+ )
141
+ table.add_column(style="b yellow")
142
+ table.add_column(style="magenta")
143
+ table.add_column(style="b green")
144
+ for i, inst in enumerate(InstallConfig.installs):
145
+ table.add_row(
146
+ "*" if i == InstallConfig.current_idx else "",
147
+ str(inst.path),
148
+ inst.name or "",
149
+ )
150
+ TempUI.print(table)
151
+ raise Exit(0)
152
+ InstallConfig.choose(path=path, name=name)
153
+ if InstallConfig.current is None:
154
+ if name is not None:
155
+ raise UnknownInstallName(name)
156
+ elif path is not None:
157
+ raise UnregisteredInstallPath(path)
158
+ else:
159
+ InstallConfig.write()
160
+
161
+
162
+ @self.command()
163
+ def reset():
164
+ with TempUI.logging():
165
+ if InstallConfig.current is not None:
166
+ idx = InstallConfig.current_idx
167
+ path = InstallConfig.current.path
168
+ InstallConfig.choose()
169
+ InstallConfig.write()
170
+ TempUI.print(
171
+ f"Install location is not set anymore as {path}\n"
172
+ # "To set it back to its previous location, run:\n"
173
+ # f"$ esgpull self choose {name}"
174
+ )
175
+ TempUI.print(InstallConfig.activate_msg(idx))
176
+ raise Exit(0)
177
+ else:
178
+ TempUI.print(":stop_sign: No install found.")
179
+ TempUI.print("To install esgpull, run:\n")
180
+ TempUI.print("$ esgpull self install")
181
+ raise Exit(1)
182
+
183
+
184
+ @self.command()
185
+ def delete():
186
+ with TempUI.logging():
187
+ if InstallConfig.current is None:
188
+ msg = (
189
+ "None\n"
190
+ "Please choose an existing install before trying to delete it."
191
+ "\n\n$ esgpull self choose ..."
192
+ )
193
+ raise UnregisteredInstallPath(msg)
194
+ else:
195
+ path = InstallConfig.current.path
196
+ TempUI.print(f"You are going to delete: {path}")
197
+ choice = TempUI.prompt(f"Please enter {path.name!r} to continue")
198
+ if choice == path.name:
199
+ TempUI.print(f"Deleting {path} from config...")
200
+ TempUI.print("To remove all files from this install, run:\n")
201
+ config = Config.load(path=path)
202
+ for p in config.paths:
203
+ if not p.is_relative_to(path):
204
+ TempUI.print(f"$ rm -rf {p}")
205
+ TempUI.print(f"$ rm -rf {path}")
206
+ InstallConfig.remove_current()
207
+ InstallConfig.write()
208
+ else:
209
+ raise Abort
210
+
211
+
212
+ @self.command()
213
+ @args.path
214
+ @opts.verbosity
215
+ def import_synda(
216
+ path: Path | None,
217
+ verbosity: Verbosity,
218
+ ):
219
+ esg = init_esgpull(verbosity)
220
+ with esg.ui.logging("import_synda", onraise=Abort):
221
+ if path is None:
222
+ sdt_home = os.getenv("SDT_HOME")
223
+ prompt_title = "Enter synda database location"
224
+ if sdt_home is not None:
225
+ esg.ui.print(
226
+ "Found existing synda installation at"
227
+ f" SDT_HOME={sdt_home}"
228
+ )
229
+ default = str(get_synda_db_path(sdt_home))
230
+ path = Path(esg.ui.prompt(prompt_title, default=str(default)))
231
+ else:
232
+ path = Path(esg.ui.prompt(prompt_title))
233
+ else:
234
+ path = path.expanduser().resolve()
235
+ if not path.is_file():
236
+ raise FileNotFoundError(path)
237
+ nb_imported = esg.import_synda(url=path, track=True, ask=True)
238
+ esg.ui.print(f"Imported {nb_imported} new files from {path}")
esgpull/cli/show.py ADDED
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+ from click.exceptions import Abort, BadArgumentUsage, Exit
5
+
6
+ from esgpull.cli.decorators import args, groups, opts
7
+ from esgpull.cli.utils import get_queries, init_esgpull, valid_name_tag
8
+ from esgpull.tui import Verbosity
9
+
10
+
11
+ @click.command()
12
+ @args.query_id
13
+ @opts.tag
14
+ @groups.show
15
+ @groups.json_yaml
16
+ @opts.files
17
+ @opts.shas
18
+ @opts.verbosity
19
+ def show(
20
+ query_id: str | None,
21
+ tag: str | None,
22
+ children: bool,
23
+ parents: bool,
24
+ expand: bool,
25
+ files: bool,
26
+ json: bool,
27
+ yaml: bool,
28
+ shas: bool,
29
+ verbosity: Verbosity,
30
+ ) -> None:
31
+ """
32
+ View query tree
33
+ """
34
+ esg = init_esgpull(verbosity)
35
+ with esg.ui.logging("show", onraise=Abort):
36
+ if not valid_name_tag(esg.graph, esg.ui, query_id, tag):
37
+ raise Exit(1)
38
+ if expand and query_id is not None:
39
+ esg.ui.print(esg.graph.expand(query_id))
40
+ raise Exit(0)
41
+ if query_id is None and tag is None:
42
+ esg.graph.load_db()
43
+ graph = esg.graph
44
+ else:
45
+ queries = get_queries(esg.graph, query_id, tag)
46
+ graph = esg.graph.subgraph(
47
+ *queries,
48
+ children=children,
49
+ parents=parents,
50
+ keep_db=True,
51
+ )
52
+ if tag is not None:
53
+ tag_db = esg.graph.get_tag(tag)
54
+ if tag_db is not None and tag_db.description is not None:
55
+ esg.ui.print(tag_db.description)
56
+ if shas:
57
+ esg.ui.print(list(graph.queries.keys()), json=True)
58
+ if json:
59
+ esg.ui.print(graph.asdict(files=files), json=True)
60
+ elif yaml:
61
+ esg.ui.print(graph.asdict(files=files), yaml=True)
62
+ elif files:
63
+ msg = "--files can only be used with --json or --yaml"
64
+ raise BadArgumentUsage(msg)
65
+ else:
66
+ esg.ui.print(graph)
esgpull/cli/status.py ADDED
@@ -0,0 +1,67 @@
1
+ import click
2
+ from click.exceptions import Abort, Exit
3
+ from rich.box import MINIMAL_DOUBLE_HEAD
4
+ from rich.table import Table
5
+ from rich.text import Text
6
+
7
+ from esgpull.cli.decorators import opts
8
+ from esgpull.cli.utils import init_esgpull
9
+ from esgpull.models import sql
10
+ from esgpull.tui import Verbosity
11
+ from esgpull.utils import format_size
12
+
13
+
14
+ @click.command()
15
+ @opts.simple
16
+ @opts.all
17
+ @opts.verbosity
18
+ def status(
19
+ simple: bool,
20
+ all_: bool,
21
+ verbosity: Verbosity,
22
+ ):
23
+ """
24
+ View file queue status
25
+
26
+ Use the `--all` flag to include already downloaded files (`done` status).
27
+ """
28
+ esg = init_esgpull(verbosity)
29
+ with esg.ui.logging("status", onraise=Abort):
30
+ status_count_size = list(esg.db.rows(sql.file.status_count_size(all_)))
31
+ table = Table(box=MINIMAL_DOUBLE_HEAD, show_edge=False)
32
+ table.add_column("status", justify="right", style="bold blue")
33
+ table.add_column("files", justify="center")
34
+ table.add_column("size", justify="right", style="magenta")
35
+ if not status_count_size:
36
+ esg.ui.print("Queue is empty.")
37
+ raise Exit(0)
38
+ if simple:
39
+ for status, count, size in status_count_size:
40
+ table.add_row(status.value, str(count), format_size(size))
41
+ else:
42
+ esg.graph.load_db()
43
+ first_line = True
44
+ for status, count, total_size in status_count_size:
45
+ first_row = True
46
+ for query in esg.graph.queries.values():
47
+ files = [f for f in query.files if f.status == status]
48
+ if files:
49
+ if first_row:
50
+ if first_line:
51
+ first_line = False
52
+ else:
53
+ table.add_row(end_section=True)
54
+ table.add_row(
55
+ Text(status.value, justify="left"),
56
+ str(count),
57
+ format_size(total_size),
58
+ style="bold",
59
+ end_section=True,
60
+ )
61
+ first_row = False
62
+ table.add_row(
63
+ query.rich_name,
64
+ str(len(files)),
65
+ format_size(sum([f.size for f in files])),
66
+ )
67
+ esg.ui.print(table)
esgpull/cli/track.py ADDED
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+ from click.exceptions import Abort, Exit
5
+ from rich.box import MINIMAL_DOUBLE_HEAD
6
+ from rich.table import Table
7
+ from rich.text import Text
8
+
9
+ from esgpull.cli.decorators import args, opts
10
+ from esgpull.cli.utils import init_esgpull, valid_name_tag
11
+ from esgpull.tui import Verbosity
12
+
13
+
14
+ @click.command()
15
+ @args.query_ids
16
+ @opts.record
17
+ @opts.verbosity
18
+ def track(
19
+ query_ids: tuple[str],
20
+ record: bool,
21
+ verbosity: Verbosity,
22
+ ) -> None:
23
+ """
24
+ Track queries
25
+
26
+ As a side effect, tracking a query applies all default options to the query,
27
+ so that modifications of the config's default options have no impact on
28
+ previouly tracked queries.
29
+ """
30
+ esg = init_esgpull(verbosity, record=record)
31
+ with esg.ui.logging("track", onraise=Abort):
32
+ for sha in query_ids:
33
+ if not valid_name_tag(esg.graph, esg.ui, sha, None):
34
+ esg.ui.raise_maybe_record(Exit(1))
35
+ query = esg.graph.get(sha)
36
+ if query.tracked:
37
+ esg.ui.print(f"{query.rich_name} is already tracked.")
38
+ esg.ui.raise_maybe_record(Exit(0))
39
+ if esg.graph.get_children(query.sha):
40
+ msg = f"{query.rich_name} has children, track anyway?"
41
+ if not esg.ui.ask(msg, default=False):
42
+ esg.ui.raise_maybe_record(Abort)
43
+ expanded = esg.graph.expand(query.sha)
44
+ tracked_query = query.clone(compute_sha=False)
45
+ tracked_query.track(expanded.options)
46
+ if query.sha != tracked_query.sha:
47
+ msg = f"For {query.rich_name} to become tracked, options must be set."
48
+ esg.ui.print(msg)
49
+ table = Table(
50
+ box=MINIMAL_DOUBLE_HEAD,
51
+ show_edge=False,
52
+ show_lines=True,
53
+ )
54
+ table.add_column(Text("before", justify="center"))
55
+ table.add_column(Text("after", justify="center"))
56
+ table.add_row(query, tracked_query)
57
+ esg.ui.print(table)
58
+ if not esg.ui.ask("Apply changes?"):
59
+ esg.ui.raise_maybe_record(Abort)
60
+ esg.graph.replace(query, tracked_query)
61
+ esg.graph.merge()
62
+ esg.ui.print(f":+1: {tracked_query.rich_name} is now tracked.")
63
+ esg.ui.raise_maybe_record(Exit(0))
64
+
65
+
66
+ @click.command()
67
+ @args.query_ids
68
+ @opts.verbosity
69
+ def untrack(
70
+ query_ids: tuple[str],
71
+ verbosity: Verbosity,
72
+ ) -> None:
73
+ """
74
+ Untrack queries
75
+ """
76
+ esg = init_esgpull(verbosity)
77
+ with esg.ui.logging("untrack", onraise=Abort):
78
+ for sha in query_ids:
79
+ if not valid_name_tag(esg.graph, esg.ui, sha, None):
80
+ raise Exit(1)
81
+ query = esg.graph.get(sha)
82
+ if not query.tracked:
83
+ esg.ui.print(f"Query {query.rich_name} is already untracked.")
84
+ raise Exit(0)
85
+ query.untrack()
86
+ esg.graph.merge()
87
+ esg.ui.print(f":+1: Query {query.rich_name} is no longer tracked.")
esgpull/cli/update.py ADDED
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ import click
6
+ from click.exceptions import Abort, Exit
7
+
8
+ from esgpull.cli.decorators import args, opts
9
+ from esgpull.cli.utils import get_queries, init_esgpull, valid_name_tag
10
+ from esgpull.context import HintsDict, ResultSearch
11
+ from esgpull.exceptions import UnsetOptionsError
12
+ from esgpull.models import File, FileStatus, Query
13
+ from esgpull.tui import Verbosity, logger
14
+ from esgpull.utils import format_size
15
+
16
+
17
+ @dataclass
18
+ class QueryFiles:
19
+ query: Query
20
+ expanded: Query
21
+ skip: bool = False
22
+ files: list[File] = field(default_factory=list)
23
+ hits: int = field(init=False)
24
+ hints: HintsDict = field(init=False)
25
+ results: list[ResultSearch] = field(init=False)
26
+
27
+
28
+ @click.command()
29
+ @args.query_id
30
+ @opts.tag
31
+ @opts.children
32
+ @opts.yes
33
+ @opts.record
34
+ @opts.verbosity
35
+ def update(
36
+ query_id: str | None,
37
+ tag: str | None,
38
+ children: bool,
39
+ yes: bool,
40
+ record: bool,
41
+ verbosity: Verbosity,
42
+ ) -> None:
43
+ """
44
+ Fetch files, link files <-> queries, send files to download queue
45
+ """
46
+ esg = init_esgpull(verbosity, record=record)
47
+ with esg.ui.logging("update", onraise=Abort):
48
+ # Select which queries to update + setup
49
+ if query_id is None and tag is None:
50
+ esg.graph.load_db()
51
+ queries = list(esg.graph.queries.values())
52
+ else:
53
+ if not valid_name_tag(esg.graph, esg.ui, query_id, tag):
54
+ esg.ui.raise_maybe_record(Exit(1))
55
+ queries = get_queries(
56
+ esg.graph,
57
+ query_id,
58
+ tag,
59
+ children=children,
60
+ )
61
+ qfs: list[QueryFiles] = []
62
+ for query in queries:
63
+ expanded = esg.graph.expand(query.sha)
64
+ if query.tracked and not expanded.trackable():
65
+ esg.ui.print(query)
66
+ raise UnsetOptionsError(query.name)
67
+ elif query.tracked:
68
+ qfs.append(QueryFiles(query, expanded))
69
+ queries = [
70
+ query
71
+ for query in queries
72
+ if query.tracked and query.sha != "LEGACY"
73
+ ]
74
+ if not qfs:
75
+ esg.ui.print(":stop_sign: Trying to update untracked queries.")
76
+ esg.ui.raise_maybe_record(Exit(0))
77
+ hints = esg.context.hints(
78
+ *[qf.expanded for qf in qfs],
79
+ file=True,
80
+ facets=["index_node"],
81
+ )
82
+ for qf, qf_hints in zip(qfs, hints):
83
+ qf.hits = sum(esg.context.hits_from_hints(qf_hints))
84
+ if qf_hints:
85
+ qf.hints = qf_hints
86
+ else:
87
+ qf.skip = True
88
+ for qf in qfs:
89
+ s = "s" if qf.hits > 1 else ""
90
+ esg.ui.print(f"{qf.query.rich_name} -> {qf.hits} file{s}.")
91
+ total_hits = sum([qf.hits for qf in qfs])
92
+ if total_hits == 0:
93
+ esg.ui.print("No files found.")
94
+ esg.ui.raise_maybe_record(Exit(0))
95
+ else:
96
+ esg.ui.print(f"{total_hits} files found.")
97
+ qfs = [qf for qf in qfs if not qf.skip]
98
+ # Prepare optimally distributed requests to ESGF
99
+ # [?] TODO: fetch FastFile first to determine what to fetch in detail later
100
+ # It might be interesting for the special case where all files already
101
+ # exist in db, then the detailed fetch could be skipped.
102
+ for qf in qfs:
103
+ qf_results = esg.context.prepare_search_distributed(
104
+ qf.expanded,
105
+ file=True,
106
+ hints=[qf.hints],
107
+ max_hits=None,
108
+ )
109
+ nb_req = len(qf_results)
110
+ if nb_req > 50:
111
+ msg = (
112
+ f"{nb_req} requests will be sent to ESGF to"
113
+ f" update {qf.query.rich_name}. Send anyway?"
114
+ )
115
+ match esg.ui.choice(msg, ["y", "n", "u"], default="n"):
116
+ case "u":
117
+ esg.ui.print(f"{qf.query.rich_name} is now untracked.")
118
+ qf.query.tracked = False
119
+ qf_results = []
120
+ case "n":
121
+ qf_results = []
122
+ case _:
123
+ ...
124
+ qf.results = qf_results
125
+ # Fetch files and update db
126
+ # [?] TODO: dry_run to print urls here
127
+ with esg.ui.spinner("Fetching files"):
128
+ coros = []
129
+ for qf in qfs:
130
+ coro = esg.context._files(*qf.results, keep_duplicates=False)
131
+ coros.append(coro)
132
+ files = esg.context.sync_gather(*coros)
133
+ for qf, qf_files in zip(qfs, files):
134
+ qf.files = qf_files
135
+ for qf in qfs:
136
+ shas = {f.sha for f in qf.query.files}
137
+ new_files: list[File] = []
138
+ for file in qf.files:
139
+ if file.sha not in shas:
140
+ new_files.append(file)
141
+ nb_files = len(new_files)
142
+ if not qf.query.tracked:
143
+ esg.db.add(qf.query)
144
+ continue
145
+ elif nb_files == 0:
146
+ esg.ui.print(f"{qf.query.rich_name} is already up-to-date.")
147
+ continue
148
+ size = sum([file.size for file in new_files])
149
+ msg = (
150
+ f"\nUpdating {qf.query.rich_name} with {nb_files}"
151
+ f" new files ({format_size(size)})."
152
+ "\nSend to download queue?"
153
+ )
154
+ if yes:
155
+ choice = "y"
156
+ else:
157
+ while (
158
+ choice := esg.ui.choice(
159
+ msg,
160
+ choices=["y", "n", "show"],
161
+ show_choices=True,
162
+ )
163
+ ) and choice == "show":
164
+ esg.ui.print(esg.graph.subgraph(qf.query, parents=True))
165
+ if choice == "y":
166
+ legacy = esg.legacy_query
167
+ has_legacy = legacy.state.persistent
168
+ for file in new_files:
169
+ file_db = esg.db.get(File, file.sha)
170
+ if file_db is None:
171
+ if esg.db.has_file_id(file):
172
+ logger.error(
173
+ "File id already exists in database, "
174
+ "there might be an error with its checksum"
175
+ f"\n{file}"
176
+ )
177
+ continue
178
+ file.status = FileStatus.Queued
179
+ file_db = esg.db.merge(file)
180
+ elif has_legacy and legacy in file_db.queries:
181
+ file_db.queries.remove(legacy)
182
+ file_db.queries.append(qf.query)
183
+ esg.db.add(file_db)
184
+ esg.ui.raise_maybe_record(Exit(0))