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/facet.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from click.exceptions import Abort
|
|
3
|
+
|
|
4
|
+
from esgpull.cli.decorators import args, opts
|
|
5
|
+
from esgpull.cli.utils import init_esgpull
|
|
6
|
+
from esgpull.models import sql
|
|
7
|
+
from esgpull.tui import Verbosity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@args.key
|
|
12
|
+
@opts.verbosity
|
|
13
|
+
def facet(
|
|
14
|
+
key: str | None,
|
|
15
|
+
verbosity: Verbosity,
|
|
16
|
+
):
|
|
17
|
+
esg = init_esgpull(verbosity)
|
|
18
|
+
with esg.ui.logging("facet", onraise=Abort):
|
|
19
|
+
if key is None:
|
|
20
|
+
results = esg.db.scalars(sql.facet.names())
|
|
21
|
+
else:
|
|
22
|
+
results = esg.db.scalars(sql.facet.values(key))
|
|
23
|
+
esg.ui.print(sorted(results))
|
esgpull/cli/get.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import rich
|
|
3
|
+
from click_params import PUBLIC_URL
|
|
4
|
+
|
|
5
|
+
from esgpull.context import Context
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command()
|
|
9
|
+
@click.option("--metadata", "-m", is_flag=True, default=False)
|
|
10
|
+
@click.argument("url", nargs=1, type=PUBLIC_URL)
|
|
11
|
+
def get(
|
|
12
|
+
url: str,
|
|
13
|
+
metadata: bool, # display file metadata, no download
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
Steps:
|
|
17
|
+
1. validate url (version+filename)
|
|
18
|
+
2. create Context
|
|
19
|
+
3. fetch File from Context
|
|
20
|
+
4. search database (if exist -> [Y/N])
|
|
21
|
+
5. check file exists in output directory (if exist -> [Y/N]) -> rename
|
|
22
|
+
6. download
|
|
23
|
+
"""
|
|
24
|
+
rich.print(url)
|
|
25
|
+
ctx = Context()
|
|
26
|
+
print(ctx)
|
|
27
|
+
|
|
28
|
+
raise NotImplementedError()
|
esgpull/cli/install.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# import click
|
|
4
|
+
# from click.exceptions import Abort, Exit
|
|
5
|
+
|
|
6
|
+
# from esgpull import Esgpull
|
|
7
|
+
# from esgpull.cli.decorators import args, opts
|
|
8
|
+
# from esgpull.cli.utils import load_facets
|
|
9
|
+
# from esgpull.db.models import File
|
|
10
|
+
# from esgpull.tui import Verbosity
|
|
11
|
+
# from esgpull.utils import format_size
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# @click.command()
|
|
15
|
+
# @opts.distrib
|
|
16
|
+
# @opts.dry_run
|
|
17
|
+
# @opts.force
|
|
18
|
+
# @opts.replica
|
|
19
|
+
# @opts.since
|
|
20
|
+
# @opts.selection_file
|
|
21
|
+
# @opts.verbosity
|
|
22
|
+
# @args.facets
|
|
23
|
+
# def install(
|
|
24
|
+
# facets: list[str],
|
|
25
|
+
# distrib: bool,
|
|
26
|
+
# dry_run: bool,
|
|
27
|
+
# force: bool,
|
|
28
|
+
# replica: bool | None,
|
|
29
|
+
# selection_file: str | None,
|
|
30
|
+
# since: str | None,
|
|
31
|
+
# verbosity: Verbosity,
|
|
32
|
+
# ) -> None:
|
|
33
|
+
# esg = Esgpull(verbosity=verbosity)
|
|
34
|
+
# with (
|
|
35
|
+
# esg.context(
|
|
36
|
+
# distrib=distrib,
|
|
37
|
+
# latest=True,
|
|
38
|
+
# since=since,
|
|
39
|
+
# replica=replica,
|
|
40
|
+
# ) as ctx,
|
|
41
|
+
# esg.ui.logging("install", onraise=Abort),
|
|
42
|
+
# ):
|
|
43
|
+
# load_facets(ctx.query, facets, selection_file)
|
|
44
|
+
# if not ctx.query.dump():
|
|
45
|
+
# raise click.UsageError("No search terms provided.")
|
|
46
|
+
# hits = ctx.file_hits
|
|
47
|
+
# nb_files = sum(hits)
|
|
48
|
+
# esg.ui.print(f"Found {nb_files} files.")
|
|
49
|
+
# if nb_files > 500 and distrib:
|
|
50
|
+
# # Enable better distrib
|
|
51
|
+
# ctx.index_nodes = esg.fetch_index_nodes()
|
|
52
|
+
# if dry_run:
|
|
53
|
+
# queries = ctx._build_queries_search(
|
|
54
|
+
# hits, file=True, max_results=nb_files, offset=0
|
|
55
|
+
# )
|
|
56
|
+
# esg.ui.print(queries)
|
|
57
|
+
# raise Exit(0)
|
|
58
|
+
# if not force and nb_files > 5000:
|
|
59
|
+
# nb_req = nb_files // esg.config.api.page_limit
|
|
60
|
+
# message = f"{nb_req} requests will be send to ESGF. Continue?"
|
|
61
|
+
# if not esg.ui.ask(message, default=True):
|
|
62
|
+
# raise Abort
|
|
63
|
+
# results = ctx.search(
|
|
64
|
+
# file=True,
|
|
65
|
+
# max_results=None,
|
|
66
|
+
# offset=0,
|
|
67
|
+
# hits=hits,
|
|
68
|
+
# )
|
|
69
|
+
# files = [File.from_dict(result) for result in results]
|
|
70
|
+
# total_size = sum([file.size for file in files])
|
|
71
|
+
# esg.ui.print(f"Total size: {format_size(total_size)}")
|
|
72
|
+
# if not force:
|
|
73
|
+
# if not esg.ui.ask("Continue?", default=True):
|
|
74
|
+
# raise Abort
|
|
75
|
+
# to_download, already_on_disk = esg.install(*files)
|
|
76
|
+
# if to_download:
|
|
77
|
+
# nb = len(to_download)
|
|
78
|
+
# s = "s" if nb > 1 else ""
|
|
79
|
+
# esg.ui.print(f"Installed {nb} new file{s} ready for download.")
|
|
80
|
+
# if already_on_disk:
|
|
81
|
+
# nb = len(already_on_disk)
|
|
82
|
+
# s = "s" if nb > 1 else ""
|
|
83
|
+
# esg.ui.print(f"Tracking {nb} file{s} already downloaded.")
|
|
84
|
+
# if not to_download and not already_on_disk:
|
|
85
|
+
# esg.ui.print("All files are already in the database.")
|
esgpull/cli/link.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# import click
|
|
4
|
+
# from click.exceptions import Abort, Exit
|
|
5
|
+
|
|
6
|
+
# from esgpull import Esgpull
|
|
7
|
+
# from esgpull.cli.decorators import args, opts
|
|
8
|
+
# from esgpull.cli.utils import get_queries, valid_name_tag
|
|
9
|
+
# from esgpull.graph import Graph
|
|
10
|
+
# from esgpull.tui import Verbosity
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# @click.command()
|
|
14
|
+
# @args.query_id
|
|
15
|
+
# @opts.tag
|
|
16
|
+
# @opts.children
|
|
17
|
+
# @opts.verbosity
|
|
18
|
+
# def link(
|
|
19
|
+
# query_id: str | None,
|
|
20
|
+
# tag: str | None,
|
|
21
|
+
# children: bool,
|
|
22
|
+
# verbosity: Verbosity,
|
|
23
|
+
# ) -> None:
|
|
24
|
+
# """
|
|
25
|
+
# Link known files to query/tag.
|
|
26
|
+
# Offline mode to avoid searching esgf ?
|
|
27
|
+
# """
|
|
28
|
+
# esg = Esgpull(verbosity=verbosity)
|
|
29
|
+
# with esg.ui.logging("remove", onraise=Abort):
|
|
30
|
+
# if query_id is None and tag is None:
|
|
31
|
+
# raise click.UsageError("No query or tag provided.")
|
|
32
|
+
# if not valid_name_tag(esg.graph, esg.ui, query_id, tag):
|
|
33
|
+
# raise Exit(1)
|
|
34
|
+
# queries = get_queries(
|
|
35
|
+
# esg.graph,
|
|
36
|
+
# query_id,
|
|
37
|
+
# tag,
|
|
38
|
+
# children=children,
|
|
39
|
+
# )
|
|
40
|
+
# nb = len(queries)
|
|
41
|
+
# ies = "ies" if nb > 1 else "y"
|
|
42
|
+
# graph = Graph(None)
|
|
43
|
+
# graph.add(*queries)
|
|
44
|
+
# esg.ui.print(graph)
|
|
45
|
+
# msg = f"Remove {nb} quer{ies}?"
|
|
46
|
+
# if not esg.ui.ask(msg, default=True):
|
|
47
|
+
# raise Abort
|
|
48
|
+
# for query in queries:
|
|
49
|
+
# if not children and esg.graph.get_children(query.sha):
|
|
50
|
+
# esg.ui.print(
|
|
51
|
+
# ":stop_sign: Some queries block "
|
|
52
|
+
# f"removal of {query.rich_name}."
|
|
53
|
+
# )
|
|
54
|
+
# if esg.ui.ask("Show blocking queries?", default=False):
|
|
55
|
+
# esg.ui.print(esg.graph.subgraph(query, children=True))
|
|
56
|
+
# raise Exit(1)
|
|
57
|
+
# esg.db.delete(*queries)
|
|
58
|
+
# esg.ui.print(":+1:")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# @click.command()
|
|
62
|
+
# @args.query_id
|
|
63
|
+
# @opts.tag
|
|
64
|
+
# @opts.children
|
|
65
|
+
# @opts.verbosity
|
|
66
|
+
# def unlink(
|
|
67
|
+
# query_id: str | None,
|
|
68
|
+
# tag: str | None,
|
|
69
|
+
# children: bool,
|
|
70
|
+
# verbosity: Verbosity,
|
|
71
|
+
# ) -> None:
|
|
72
|
+
# """
|
|
73
|
+
# Unlink files that were associated to query/tag
|
|
74
|
+
# """
|
|
75
|
+
# esg = Esgpull(verbosity=verbosity)
|
|
76
|
+
# with esg.ui.logging("remove", onraise=Abort):
|
|
77
|
+
# if query_id is None and tag is None:
|
|
78
|
+
# raise click.UsageError("No query or tag provided.")
|
|
79
|
+
# if not valid_name_tag(esg.graph, esg.ui, query_id, tag):
|
|
80
|
+
# raise Exit(1)
|
|
81
|
+
# queries = get_queries(
|
|
82
|
+
# esg.graph,
|
|
83
|
+
# query_id,
|
|
84
|
+
# tag,
|
|
85
|
+
# children=children,
|
|
86
|
+
# )
|
|
87
|
+
# nb = len(queries)
|
|
88
|
+
# ies = "ies" if nb > 1 else "y"
|
|
89
|
+
# graph = Graph(None)
|
|
90
|
+
# graph.add(*queries)
|
|
91
|
+
# esg.ui.print(graph)
|
|
92
|
+
# msg = f"Remove {nb} quer{ies}?"
|
|
93
|
+
# if not esg.ui.ask(msg, default=True):
|
|
94
|
+
# raise Abort
|
|
95
|
+
# for query in queries:
|
|
96
|
+
# if not children and esg.graph.get_children(query.sha):
|
|
97
|
+
# esg.ui.print(
|
|
98
|
+
# ":stop_sign: Some queries block "
|
|
99
|
+
# f"removal of {query.rich_name}."
|
|
100
|
+
# )
|
|
101
|
+
# if esg.ui.ask("Show blocking queries?", default=False):
|
|
102
|
+
# esg.ui.print(esg.graph.subgraph(query, children=True))
|
|
103
|
+
# raise Exit(1)
|
|
104
|
+
# esg.db.delete(*queries)
|
|
105
|
+
# esg.ui.print(":+1:")
|
esgpull/cli/login.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from click.exceptions import Abort
|
|
3
|
+
|
|
4
|
+
from esgpull.auth import Auth, AuthStatus, Credentials
|
|
5
|
+
from esgpull.cli.decorators import opts
|
|
6
|
+
from esgpull.cli.utils import init_esgpull
|
|
7
|
+
from esgpull.constants import PROVIDERS
|
|
8
|
+
from esgpull.tui import Verbosity
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@opts.verbosity
|
|
13
|
+
@opts.force
|
|
14
|
+
def login(verbosity: Verbosity, force: bool):
|
|
15
|
+
"""
|
|
16
|
+
OpenID authentication and certificates renewal
|
|
17
|
+
|
|
18
|
+
The first call to `login` is a prompt asking for provider/username/password.
|
|
19
|
+
|
|
20
|
+
Subsequent calls check whether the login certificates are valid, renewing them if needed.
|
|
21
|
+
Renewal can be forced using the `--force` flag.
|
|
22
|
+
"""
|
|
23
|
+
esg = init_esgpull(verbosity)
|
|
24
|
+
with esg.ui.logging("login", onraise=Abort):
|
|
25
|
+
cred_file = esg.config.paths.auth / esg.config.credentials.filename
|
|
26
|
+
if not cred_file.is_file():
|
|
27
|
+
esg.ui.print("No credentials found.")
|
|
28
|
+
choices = []
|
|
29
|
+
providers = list(PROVIDERS)
|
|
30
|
+
for i, provider in enumerate(providers):
|
|
31
|
+
choices.append(str(i))
|
|
32
|
+
esg.ui.print(f" [{i}] [i green]{provider}[/]")
|
|
33
|
+
provider_idx = esg.ui.choice(
|
|
34
|
+
"Select a provider",
|
|
35
|
+
choices=choices,
|
|
36
|
+
show_choices=False,
|
|
37
|
+
)
|
|
38
|
+
provider = providers[int(provider_idx)]
|
|
39
|
+
user = esg.ui.prompt("User")
|
|
40
|
+
password = esg.ui.prompt("Password", password=True)
|
|
41
|
+
credentials = Credentials(provider, user, password)
|
|
42
|
+
credentials.write(cred_file)
|
|
43
|
+
esg.auth = Auth.from_config(esg.config, credentials)
|
|
44
|
+
renew = force
|
|
45
|
+
status = esg.auth.status
|
|
46
|
+
status_name = status.value[0]
|
|
47
|
+
status_color = status.value[1]
|
|
48
|
+
esg.ui.print(f"Certificates are [{status_color}]{status_name}[/].")
|
|
49
|
+
if esg.auth.status == AuthStatus.Expired:
|
|
50
|
+
renew = renew or esg.ui.ask("Renew?")
|
|
51
|
+
elif esg.auth.status == AuthStatus.Missing:
|
|
52
|
+
renew = True
|
|
53
|
+
if renew:
|
|
54
|
+
with esg.ui.spinner("Renewing certificates"):
|
|
55
|
+
esg.auth.renew()
|
|
56
|
+
esg.ui.print(":+1: Renewed successfully")
|
esgpull/cli/remove.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from click.exceptions import Abort, Exit
|
|
5
|
+
|
|
6
|
+
from esgpull.cli.decorators import args, opts
|
|
7
|
+
from esgpull.cli.utils import get_queries, init_esgpull, valid_name_tag
|
|
8
|
+
from esgpull.graph import Graph
|
|
9
|
+
from esgpull.models import FileStatus
|
|
10
|
+
from esgpull.tui import Verbosity
|
|
11
|
+
from esgpull.utils import format_size
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command()
|
|
15
|
+
@args.query_id
|
|
16
|
+
@opts.tag
|
|
17
|
+
@opts.children
|
|
18
|
+
@opts.verbosity
|
|
19
|
+
def remove(
|
|
20
|
+
query_id: str | None,
|
|
21
|
+
tag: str | None,
|
|
22
|
+
children: bool,
|
|
23
|
+
verbosity: Verbosity,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Remove queries from the database
|
|
27
|
+
|
|
28
|
+
A query required by other queries cannot be removed.
|
|
29
|
+
|
|
30
|
+
Upon removal, no files are deleted from the database nor from the filesystem, only links to that query are deleted.
|
|
31
|
+
|
|
32
|
+
There is currently no mechanism to delete "orphaned" files, as this functionality should be designed carefully and rooted in practical needs.
|
|
33
|
+
"""
|
|
34
|
+
esg = init_esgpull(verbosity)
|
|
35
|
+
with esg.ui.logging("remove", onraise=Abort):
|
|
36
|
+
if query_id is None and tag is None:
|
|
37
|
+
raise click.UsageError("No query or tag provided.")
|
|
38
|
+
if not valid_name_tag(esg.graph, esg.ui, query_id, tag):
|
|
39
|
+
raise Exit(1)
|
|
40
|
+
queries = get_queries(
|
|
41
|
+
esg.graph,
|
|
42
|
+
query_id,
|
|
43
|
+
tag,
|
|
44
|
+
children=children,
|
|
45
|
+
)
|
|
46
|
+
nb = len(queries)
|
|
47
|
+
ies = "ies" if nb > 1 else "y"
|
|
48
|
+
graph = Graph(None)
|
|
49
|
+
graph.add(*queries)
|
|
50
|
+
esg.ui.print(graph)
|
|
51
|
+
msg = f"Remove {nb} quer{ies}?"
|
|
52
|
+
if not esg.ui.ask(msg, default=True):
|
|
53
|
+
raise Abort
|
|
54
|
+
for query in queries:
|
|
55
|
+
if query.has_files:
|
|
56
|
+
nb, size = query.files_count_size(FileStatus.Done)
|
|
57
|
+
if nb:
|
|
58
|
+
esg.ui.print(
|
|
59
|
+
f":stop_sign: {query.rich_name} is linked"
|
|
60
|
+
f" to {nb} downloaded files ({format_size(size)})."
|
|
61
|
+
)
|
|
62
|
+
if not esg.ui.ask("Delete anyway?", default=False):
|
|
63
|
+
raise Abort
|
|
64
|
+
if not children and esg.graph.get_children(query.sha):
|
|
65
|
+
esg.ui.print(
|
|
66
|
+
":stop_sign: Some queries block"
|
|
67
|
+
f" removal of {query.rich_name}."
|
|
68
|
+
)
|
|
69
|
+
if esg.ui.ask("Show blocking queries?", default=False):
|
|
70
|
+
esg.ui.print(esg.graph.subgraph(query, children=True))
|
|
71
|
+
raise Exit(1)
|
|
72
|
+
esg.db.delete(*queries)
|
|
73
|
+
esg.ui.print(":+1:")
|
esgpull/cli/retry.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from click.exceptions import Abort, Exit
|
|
6
|
+
|
|
7
|
+
from esgpull.cli.decorators import args, opts
|
|
8
|
+
from esgpull.cli.utils import init_esgpull
|
|
9
|
+
from esgpull.models import FileStatus, sql
|
|
10
|
+
from esgpull.tui import Verbosity
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command()
|
|
14
|
+
@args.status
|
|
15
|
+
@opts.verbosity
|
|
16
|
+
def retry(
|
|
17
|
+
status: Sequence[FileStatus],
|
|
18
|
+
verbosity: Verbosity,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Re-queue failed and cancelled downloads
|
|
22
|
+
"""
|
|
23
|
+
if not status:
|
|
24
|
+
status = FileStatus.retryable()
|
|
25
|
+
esg = init_esgpull(verbosity)
|
|
26
|
+
with esg.ui.logging("retry", onraise=Abort):
|
|
27
|
+
assert FileStatus.Done not in status
|
|
28
|
+
assert FileStatus.Queued not in status
|
|
29
|
+
files = list(esg.db.scalars(sql.file.with_status(*status)))
|
|
30
|
+
status_str = "/".join(f"[bold red]{s.value}[/]" for s in status)
|
|
31
|
+
if not files:
|
|
32
|
+
esg.ui.print(f"No {status_str} files found.")
|
|
33
|
+
raise Exit(0)
|
|
34
|
+
counts = Counter(file.status for file in files)
|
|
35
|
+
for file in files:
|
|
36
|
+
file.status = FileStatus.Queued
|
|
37
|
+
esg.db.add(*files)
|
|
38
|
+
msg = "Sent back to the queue: "
|
|
39
|
+
msg += ", ".join(
|
|
40
|
+
f"{count} [bold red]{status.value}[/]"
|
|
41
|
+
for status, count in counts.items()
|
|
42
|
+
)
|
|
43
|
+
esg.ui.print(msg)
|
esgpull/cli/search.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from click.exceptions import Abort, Exit
|
|
7
|
+
|
|
8
|
+
from esgpull.cli.decorators import args, groups, opts
|
|
9
|
+
from esgpull.cli.utils import filter_keys, init_esgpull, parse_query, totable
|
|
10
|
+
from esgpull.exceptions import PageIndexError
|
|
11
|
+
from esgpull.graph import Graph
|
|
12
|
+
from esgpull.models import Query
|
|
13
|
+
from esgpull.tui import Verbosity
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command()
|
|
17
|
+
@args.facets
|
|
18
|
+
@groups.query_def
|
|
19
|
+
@groups.query_date
|
|
20
|
+
@groups.display
|
|
21
|
+
@opts.dry_run
|
|
22
|
+
@opts.file
|
|
23
|
+
@opts.facets_hints
|
|
24
|
+
@opts.hints
|
|
25
|
+
@groups.json_yaml
|
|
26
|
+
@opts.detail
|
|
27
|
+
@opts.show
|
|
28
|
+
@opts.yes
|
|
29
|
+
@opts.record
|
|
30
|
+
@opts.verbosity
|
|
31
|
+
# @opts.selection_file
|
|
32
|
+
def search(
|
|
33
|
+
facets: list[str],
|
|
34
|
+
## query_def
|
|
35
|
+
tags: list[str],
|
|
36
|
+
require: str | None,
|
|
37
|
+
distrib: str | None,
|
|
38
|
+
latest: str | None,
|
|
39
|
+
replica: str | None,
|
|
40
|
+
retracted: str | None,
|
|
41
|
+
date_from: datetime | None,
|
|
42
|
+
date_to: datetime | None,
|
|
43
|
+
## display
|
|
44
|
+
_all: bool,
|
|
45
|
+
zero: bool,
|
|
46
|
+
page: int,
|
|
47
|
+
## json_yaml
|
|
48
|
+
json: bool,
|
|
49
|
+
yaml: bool,
|
|
50
|
+
## ungrouped
|
|
51
|
+
show: bool,
|
|
52
|
+
detail: int | None,
|
|
53
|
+
dry_run: bool,
|
|
54
|
+
file: bool,
|
|
55
|
+
facets_hints: bool,
|
|
56
|
+
hints: list[str] | None,
|
|
57
|
+
yes: bool,
|
|
58
|
+
# selection_file: str | None,
|
|
59
|
+
record: bool,
|
|
60
|
+
verbosity: Verbosity,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Search datasets and files on ESGF
|
|
64
|
+
|
|
65
|
+
More info
|
|
66
|
+
"""
|
|
67
|
+
esg = init_esgpull(verbosity, safe=False, record=record)
|
|
68
|
+
with esg.ui.logging("search", onraise=Abort):
|
|
69
|
+
query = parse_query(
|
|
70
|
+
facets=facets,
|
|
71
|
+
tags=tags,
|
|
72
|
+
require=require,
|
|
73
|
+
distrib=distrib,
|
|
74
|
+
latest=latest,
|
|
75
|
+
replica=replica,
|
|
76
|
+
retracted=retracted,
|
|
77
|
+
)
|
|
78
|
+
query.compute_sha()
|
|
79
|
+
esg.graph.resolve_require(query)
|
|
80
|
+
if show:
|
|
81
|
+
if json:
|
|
82
|
+
esg.ui.print(query.asdict(), json=True)
|
|
83
|
+
elif yaml:
|
|
84
|
+
esg.ui.print(query.asdict(), yaml=True)
|
|
85
|
+
else:
|
|
86
|
+
try:
|
|
87
|
+
graph = esg.graph.subgraph(query, parents=True)
|
|
88
|
+
esg.ui.print(graph)
|
|
89
|
+
except KeyError:
|
|
90
|
+
esg.ui.print(query)
|
|
91
|
+
esg.ui.raise_maybe_record(Exit(0))
|
|
92
|
+
esg.graph.add(query, force=True)
|
|
93
|
+
query = esg.graph.expand(query.sha)
|
|
94
|
+
hits = esg.context.hits(
|
|
95
|
+
query,
|
|
96
|
+
file=file,
|
|
97
|
+
date_from=date_from,
|
|
98
|
+
date_to=date_to,
|
|
99
|
+
)
|
|
100
|
+
nb = sum(hits)
|
|
101
|
+
page_size = esg.config.cli.page_size
|
|
102
|
+
if detail is not None:
|
|
103
|
+
page_size = 1
|
|
104
|
+
page = detail
|
|
105
|
+
nb_pages = (nb // page_size) or 1
|
|
106
|
+
offset = page * page_size
|
|
107
|
+
max_hits = min(page_size, nb - offset)
|
|
108
|
+
if page > nb_pages:
|
|
109
|
+
raise PageIndexError(page, nb_pages)
|
|
110
|
+
elif zero:
|
|
111
|
+
max_hits = 0
|
|
112
|
+
elif _all:
|
|
113
|
+
offset = 0
|
|
114
|
+
max_hits = nb
|
|
115
|
+
ids = range(offset, offset + max_hits)
|
|
116
|
+
if dry_run:
|
|
117
|
+
search_results = esg.context.prepare_search(
|
|
118
|
+
query,
|
|
119
|
+
file=file,
|
|
120
|
+
hits=hits,
|
|
121
|
+
offset=offset,
|
|
122
|
+
max_hits=max_hits,
|
|
123
|
+
date_from=date_from,
|
|
124
|
+
date_to=date_to,
|
|
125
|
+
)
|
|
126
|
+
for result in search_results:
|
|
127
|
+
esg.ui.print(result.request.url)
|
|
128
|
+
esg.ui.raise_maybe_record(Exit(0))
|
|
129
|
+
if facets_hints:
|
|
130
|
+
not_distrib_query = query << Query(options=dict(distrib=False))
|
|
131
|
+
facet_counts = esg.context.hints(
|
|
132
|
+
not_distrib_query,
|
|
133
|
+
file=file,
|
|
134
|
+
facets=["*"],
|
|
135
|
+
date_from=date_from,
|
|
136
|
+
date_to=date_to,
|
|
137
|
+
)
|
|
138
|
+
esg.ui.print(list(facet_counts[0]), json=True)
|
|
139
|
+
esg.ui.raise_maybe_record(Exit(0))
|
|
140
|
+
if hints is not None:
|
|
141
|
+
facet_counts = esg.context.hints(
|
|
142
|
+
query,
|
|
143
|
+
file=file,
|
|
144
|
+
facets=hints,
|
|
145
|
+
date_from=date_from,
|
|
146
|
+
date_to=date_to,
|
|
147
|
+
)
|
|
148
|
+
esg.ui.print(facet_counts, json=True)
|
|
149
|
+
esg.ui.raise_maybe_record(Exit(0))
|
|
150
|
+
if max_hits > 200 and not yes:
|
|
151
|
+
nb_req = max_hits // esg.config.api.page_limit
|
|
152
|
+
message = f"{nb_req} requests will be sent to ESGF. Send anyway?"
|
|
153
|
+
if not esg.ui.ask(message, default=True):
|
|
154
|
+
esg.ui.raise_maybe_record(Abort)
|
|
155
|
+
if detail is not None:
|
|
156
|
+
queries = esg.context.search_as_queries(
|
|
157
|
+
query,
|
|
158
|
+
file=file,
|
|
159
|
+
hits=hits,
|
|
160
|
+
offset=offset,
|
|
161
|
+
max_hits=max_hits,
|
|
162
|
+
keep_duplicates=True,
|
|
163
|
+
date_from=date_from,
|
|
164
|
+
date_to=date_to,
|
|
165
|
+
)
|
|
166
|
+
if json:
|
|
167
|
+
selections = [q.selection.asdict() for q in queries]
|
|
168
|
+
esg.ui.print(selections, json=True)
|
|
169
|
+
elif yaml:
|
|
170
|
+
selections = [q.selection.asdict() for q in queries]
|
|
171
|
+
esg.ui.print(selections, yaml=True)
|
|
172
|
+
else:
|
|
173
|
+
graph = Graph(None)
|
|
174
|
+
graph.add(*queries, clone=False)
|
|
175
|
+
esg.ui.print(graph)
|
|
176
|
+
esg.ui.raise_maybe_record(Exit(0))
|
|
177
|
+
results = esg.context.search(
|
|
178
|
+
query,
|
|
179
|
+
file=file,
|
|
180
|
+
hits=hits,
|
|
181
|
+
offset=offset,
|
|
182
|
+
max_hits=max_hits,
|
|
183
|
+
keep_duplicates=True,
|
|
184
|
+
date_from=date_from,
|
|
185
|
+
date_to=date_to,
|
|
186
|
+
)
|
|
187
|
+
if json:
|
|
188
|
+
esg.ui.print([f.asdict() for f in results], json=True)
|
|
189
|
+
elif yaml:
|
|
190
|
+
esg.ui.print([f.asdict() for f in results], yaml=True)
|
|
191
|
+
else:
|
|
192
|
+
f_or_d = "file" if file else "dataset"
|
|
193
|
+
s = "s" if nb != 1 else ""
|
|
194
|
+
esg.ui.print(f"Found {nb} {f_or_d}{s}.")
|
|
195
|
+
if results:
|
|
196
|
+
unique_ids = {r.master_id for r in results}
|
|
197
|
+
unique_nodes = {(r.master_id, r.data_node) for r in results}
|
|
198
|
+
needs_data_node = len(unique_nodes) > len(unique_ids)
|
|
199
|
+
docs = filter_keys(results, ids=ids, data_node=needs_data_node)
|
|
200
|
+
esg.ui.print(totable(docs))
|
|
201
|
+
esg.ui.raise_maybe_record(Exit(0))
|