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.
- esgpull/__init__.py +12 -0
- esgpull/auth.py +181 -0
- esgpull/cli/__init__.py +73 -0
- esgpull/cli/add.py +103 -0
- esgpull/cli/autoremove.py +38 -0
- esgpull/cli/config.py +116 -0
- esgpull/cli/convert.py +285 -0
- esgpull/cli/decorators.py +342 -0
- esgpull/cli/download.py +74 -0
- esgpull/cli/facet.py +23 -0
- esgpull/cli/get.py +28 -0
- esgpull/cli/install.py +85 -0
- esgpull/cli/link.py +105 -0
- esgpull/cli/login.py +56 -0
- esgpull/cli/remove.py +73 -0
- esgpull/cli/retry.py +43 -0
- esgpull/cli/search.py +201 -0
- esgpull/cli/self.py +238 -0
- esgpull/cli/show.py +66 -0
- esgpull/cli/status.py +67 -0
- esgpull/cli/track.py +87 -0
- esgpull/cli/update.py +184 -0
- esgpull/cli/utils.py +247 -0
- esgpull/config.py +410 -0
- esgpull/constants.py +56 -0
- esgpull/context.py +724 -0
- esgpull/database.py +161 -0
- esgpull/download.py +162 -0
- esgpull/esgpull.py +447 -0
- esgpull/exceptions.py +167 -0
- esgpull/fs.py +253 -0
- esgpull/graph.py +460 -0
- esgpull/install_config.py +185 -0
- esgpull/migrations/README +1 -0
- esgpull/migrations/env.py +82 -0
- esgpull/migrations/script.py.mako +24 -0
- esgpull/migrations/versions/0.3.0_update_tables.py +170 -0
- esgpull/migrations/versions/0.3.1_update_tables.py +25 -0
- esgpull/migrations/versions/0.3.2_update_tables.py +26 -0
- esgpull/migrations/versions/0.3.3_update_tables.py +25 -0
- esgpull/migrations/versions/0.3.4_update_tables.py +25 -0
- esgpull/migrations/versions/0.3.5_update_tables.py +25 -0
- esgpull/migrations/versions/0.3.6_update_tables.py +26 -0
- esgpull/migrations/versions/0.3.7_update_tables.py +26 -0
- esgpull/migrations/versions/0.3.8_update_tables.py +26 -0
- esgpull/migrations/versions/0.4.0_update_tables.py +25 -0
- esgpull/migrations/versions/0.5.0_update_tables.py +26 -0
- esgpull/migrations/versions/0.5.1_update_tables.py +26 -0
- esgpull/migrations/versions/0.5.2_update_tables.py +25 -0
- esgpull/migrations/versions/0.5.3_update_tables.py +26 -0
- esgpull/migrations/versions/0.5.4_update_tables.py +25 -0
- esgpull/migrations/versions/0.5.5_update_tables.py +25 -0
- esgpull/migrations/versions/0.6.0_update_tables.py +25 -0
- esgpull/migrations/versions/0.6.1_update_tables.py +25 -0
- esgpull/migrations/versions/0.6.2_update_tables.py +25 -0
- esgpull/migrations/versions/0.6.3_update_tables.py +25 -0
- esgpull/models/__init__.py +31 -0
- esgpull/models/base.py +50 -0
- esgpull/models/dataset.py +34 -0
- esgpull/models/facet.py +18 -0
- esgpull/models/file.py +65 -0
- esgpull/models/options.py +164 -0
- esgpull/models/query.py +481 -0
- esgpull/models/selection.py +201 -0
- esgpull/models/sql.py +258 -0
- esgpull/models/synda_file.py +85 -0
- esgpull/models/tag.py +19 -0
- esgpull/models/utils.py +54 -0
- esgpull/presets.py +13 -0
- esgpull/processor.py +172 -0
- esgpull/py.typed +0 -0
- esgpull/result.py +53 -0
- esgpull/tui.py +346 -0
- esgpull/utils.py +54 -0
- esgpull/version.py +1 -0
- esgpull-0.6.3.dist-info/METADATA +110 -0
- esgpull-0.6.3.dist-info/RECORD +80 -0
- esgpull-0.6.3.dist-info/WHEEL +4 -0
- esgpull-0.6.3.dist-info/entry_points.txt +3 -0
- 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))
|