esgpull 0.6.3__tar.gz

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 (92) hide show
  1. esgpull-0.6.3/LICENSE +28 -0
  2. esgpull-0.6.3/PKG-INFO +110 -0
  3. esgpull-0.6.3/README.md +54 -0
  4. esgpull-0.6.3/esgpull/__init__.py +12 -0
  5. esgpull-0.6.3/esgpull/auth.py +181 -0
  6. esgpull-0.6.3/esgpull/cli/__init__.py +73 -0
  7. esgpull-0.6.3/esgpull/cli/add.py +103 -0
  8. esgpull-0.6.3/esgpull/cli/autoremove.py +38 -0
  9. esgpull-0.6.3/esgpull/cli/config.py +116 -0
  10. esgpull-0.6.3/esgpull/cli/convert.py +285 -0
  11. esgpull-0.6.3/esgpull/cli/decorators.py +342 -0
  12. esgpull-0.6.3/esgpull/cli/download.py +74 -0
  13. esgpull-0.6.3/esgpull/cli/facet.py +23 -0
  14. esgpull-0.6.3/esgpull/cli/get.py +28 -0
  15. esgpull-0.6.3/esgpull/cli/install.py +85 -0
  16. esgpull-0.6.3/esgpull/cli/link.py +105 -0
  17. esgpull-0.6.3/esgpull/cli/login.py +56 -0
  18. esgpull-0.6.3/esgpull/cli/remove.py +73 -0
  19. esgpull-0.6.3/esgpull/cli/retry.py +43 -0
  20. esgpull-0.6.3/esgpull/cli/search.py +201 -0
  21. esgpull-0.6.3/esgpull/cli/self.py +238 -0
  22. esgpull-0.6.3/esgpull/cli/show.py +66 -0
  23. esgpull-0.6.3/esgpull/cli/status.py +67 -0
  24. esgpull-0.6.3/esgpull/cli/track.py +87 -0
  25. esgpull-0.6.3/esgpull/cli/update.py +184 -0
  26. esgpull-0.6.3/esgpull/cli/utils.py +247 -0
  27. esgpull-0.6.3/esgpull/config.py +410 -0
  28. esgpull-0.6.3/esgpull/constants.py +56 -0
  29. esgpull-0.6.3/esgpull/context.py +724 -0
  30. esgpull-0.6.3/esgpull/database.py +161 -0
  31. esgpull-0.6.3/esgpull/download.py +162 -0
  32. esgpull-0.6.3/esgpull/esgpull.py +447 -0
  33. esgpull-0.6.3/esgpull/exceptions.py +167 -0
  34. esgpull-0.6.3/esgpull/fs.py +253 -0
  35. esgpull-0.6.3/esgpull/graph.py +460 -0
  36. esgpull-0.6.3/esgpull/install_config.py +185 -0
  37. esgpull-0.6.3/esgpull/migrations/README +1 -0
  38. esgpull-0.6.3/esgpull/migrations/env.py +82 -0
  39. esgpull-0.6.3/esgpull/migrations/script.py.mako +24 -0
  40. esgpull-0.6.3/esgpull/migrations/versions/0.3.0_update_tables.py +170 -0
  41. esgpull-0.6.3/esgpull/migrations/versions/0.3.1_update_tables.py +25 -0
  42. esgpull-0.6.3/esgpull/migrations/versions/0.3.2_update_tables.py +26 -0
  43. esgpull-0.6.3/esgpull/migrations/versions/0.3.3_update_tables.py +25 -0
  44. esgpull-0.6.3/esgpull/migrations/versions/0.3.4_update_tables.py +25 -0
  45. esgpull-0.6.3/esgpull/migrations/versions/0.3.5_update_tables.py +25 -0
  46. esgpull-0.6.3/esgpull/migrations/versions/0.3.6_update_tables.py +26 -0
  47. esgpull-0.6.3/esgpull/migrations/versions/0.3.7_update_tables.py +26 -0
  48. esgpull-0.6.3/esgpull/migrations/versions/0.3.8_update_tables.py +26 -0
  49. esgpull-0.6.3/esgpull/migrations/versions/0.4.0_update_tables.py +25 -0
  50. esgpull-0.6.3/esgpull/migrations/versions/0.5.0_update_tables.py +26 -0
  51. esgpull-0.6.3/esgpull/migrations/versions/0.5.1_update_tables.py +26 -0
  52. esgpull-0.6.3/esgpull/migrations/versions/0.5.2_update_tables.py +25 -0
  53. esgpull-0.6.3/esgpull/migrations/versions/0.5.3_update_tables.py +26 -0
  54. esgpull-0.6.3/esgpull/migrations/versions/0.5.4_update_tables.py +25 -0
  55. esgpull-0.6.3/esgpull/migrations/versions/0.5.5_update_tables.py +25 -0
  56. esgpull-0.6.3/esgpull/migrations/versions/0.6.0_update_tables.py +25 -0
  57. esgpull-0.6.3/esgpull/migrations/versions/0.6.1_update_tables.py +25 -0
  58. esgpull-0.6.3/esgpull/migrations/versions/0.6.2_update_tables.py +25 -0
  59. esgpull-0.6.3/esgpull/migrations/versions/0.6.3_update_tables.py +25 -0
  60. esgpull-0.6.3/esgpull/models/__init__.py +31 -0
  61. esgpull-0.6.3/esgpull/models/base.py +50 -0
  62. esgpull-0.6.3/esgpull/models/dataset.py +34 -0
  63. esgpull-0.6.3/esgpull/models/facet.py +18 -0
  64. esgpull-0.6.3/esgpull/models/file.py +65 -0
  65. esgpull-0.6.3/esgpull/models/options.py +164 -0
  66. esgpull-0.6.3/esgpull/models/query.py +481 -0
  67. esgpull-0.6.3/esgpull/models/selection.py +201 -0
  68. esgpull-0.6.3/esgpull/models/sql.py +258 -0
  69. esgpull-0.6.3/esgpull/models/synda_file.py +85 -0
  70. esgpull-0.6.3/esgpull/models/tag.py +19 -0
  71. esgpull-0.6.3/esgpull/models/utils.py +54 -0
  72. esgpull-0.6.3/esgpull/presets.py +13 -0
  73. esgpull-0.6.3/esgpull/processor.py +172 -0
  74. esgpull-0.6.3/esgpull/py.typed +0 -0
  75. esgpull-0.6.3/esgpull/result.py +53 -0
  76. esgpull-0.6.3/esgpull/tui.py +346 -0
  77. esgpull-0.6.3/esgpull/utils.py +54 -0
  78. esgpull-0.6.3/esgpull/version.py +1 -0
  79. esgpull-0.6.3/pyproject.toml +151 -0
  80. esgpull-0.6.3/tests/__init__.py +0 -0
  81. esgpull-0.6.3/tests/conftest.py +45 -0
  82. esgpull-0.6.3/tests/test_auth.py +21 -0
  83. esgpull-0.6.3/tests/test_config.py +41 -0
  84. esgpull-0.6.3/tests/test_context.py +169 -0
  85. esgpull-0.6.3/tests/test_db.py +61 -0
  86. esgpull-0.6.3/tests/test_fs.py +178 -0
  87. esgpull-0.6.3/tests/test_graph.py +281 -0
  88. esgpull-0.6.3/tests/test_processor.py +104 -0
  89. esgpull-0.6.3/tests/test_query.py +70 -0
  90. esgpull-0.6.3/tests/test_selection.py +80 -0
  91. esgpull-0.6.3/tests/test_synda.py +125 -0
  92. esgpull-0.6.3/tests/test_utils.py +23 -0
esgpull-0.6.3/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, Institut Pierre-Simon Laplace (IPSL) and contributors
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
esgpull-0.6.3/PKG-INFO ADDED
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.1
2
+ Name: esgpull
3
+ Version: 0.6.3
4
+ Summary: ESGF data discovery, download, replication tool
5
+ Author-Email: Sven Rodriguez <srodriguez@ipsl.fr>
6
+ License: BSD 3-Clause License
7
+
8
+ Copyright (c) 2023, Institut Pierre-Simon Laplace (IPSL) and contributors
9
+
10
+ Redistribution and use in source and binary forms, with or without
11
+ modification, are permitted provided that the following conditions are met:
12
+
13
+ 1. Redistributions of source code must retain the above copyright notice, this
14
+ list of conditions and the following disclaimer.
15
+
16
+ 2. Redistributions in binary form must reproduce the above copyright notice,
17
+ this list of conditions and the following disclaimer in the documentation
18
+ and/or other materials provided with the distribution.
19
+
20
+ 3. Neither the name of the copyright holder nor the names of its
21
+ contributors may be used to endorse or promote products derived from
22
+ this software without specific prior written permission.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+ Project-URL: Repository, https://github.com/ESGF/esgf-download
35
+ Project-URL: Documentation, https://esgf.github.io/esgf-download/
36
+ Requires-Python: >=3.10
37
+ Requires-Dist: MyProxyClient>=2.1.0
38
+ Requires-Dist: aiofiles>=22.1.0
39
+ Requires-Dist: alembic>=1.8.1
40
+ Requires-Dist: click>=8.1.3
41
+ Requires-Dist: click-params>=0.4.0
42
+ Requires-Dist: httpx>=0.23.0
43
+ Requires-Dist: nest-asyncio>=1.5.6
44
+ Requires-Dist: pyOpenSSL>=22.1.0
45
+ Requires-Dist: pyyaml>=6.0
46
+ Requires-Dist: tomlkit>=0.11.5
47
+ Requires-Dist: rich>=12.6.0
48
+ Requires-Dist: sqlalchemy>=2.0.0b2
49
+ Requires-Dist: setuptools>=65.4.1
50
+ Requires-Dist: aiostream>=0.4.5
51
+ Requires-Dist: attrs>=22.1.0
52
+ Requires-Dist: cattrs>=22.2.0
53
+ Requires-Dist: platformdirs>=2.6.2
54
+ Requires-Dist: pyparsing>=3.0.9
55
+ Description-Content-Type: text/markdown
56
+
57
+ [![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm.fming.dev)
58
+
59
+ # esgpull - ESGF data management utility
60
+
61
+ `esgpull` is a tool that simplifies usage of the [ESGF Search API](https://esgf.github.io/esg-search/ESGF_Search_RESTful_API.html) for data discovery, and manages procedures related to downloading and storing files from ESGF.
62
+
63
+ ```py
64
+ from esgpull import Esgpull, Query
65
+
66
+ query = Query()
67
+ query.selection.project = "CMIP6"
68
+ query.options.distrib = True # default=False
69
+ esg = Esgpull()
70
+ nb_datasets = esg.context.hits(query, file=False)[0]
71
+ nb_files = esg.context.hits(query, file=True)[0]
72
+ datasets = esg.context.datasets(query, max_hits=5)
73
+ print(f"Number of CMIP6 datasets: {nb_datasets}")
74
+ print(f"Number of CMIP6 files: {nb_files}")
75
+ for dataset in datasets:
76
+ print(dataset)
77
+ ```
78
+
79
+ ## Features
80
+
81
+ - Command-line interface
82
+ - HTTP download (async multi-file)
83
+
84
+ ## Usage
85
+
86
+ ```console
87
+ Usage: esgpull [OPTIONS] COMMAND [ARGS]...
88
+
89
+ esgpull is a management utility for files and datasets from ESGF.
90
+
91
+ Options:
92
+ -V, --version Show the version and exit.
93
+ -h, --help Show this message and exit.
94
+
95
+ Commands:
96
+ add Add queries to the database
97
+ config View/modify config
98
+ convert Convert synda selection files to esgpull queries
99
+ download Asynchronously download files linked to queries
100
+ login OpenID authentication and certificates renewal
101
+ remove Remove queries from the database
102
+ retry Re-queue failed and cancelled downloads
103
+ search Search datasets and files on ESGF
104
+ self Manage esgpull installations / import synda database
105
+ show View query tree
106
+ status View file queue status
107
+ track Track queries
108
+ untrack Untrack queries
109
+ update Fetch files, link files <-> queries, send files to download...
110
+ ```
@@ -0,0 +1,54 @@
1
+ [![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm.fming.dev)
2
+
3
+ # esgpull - ESGF data management utility
4
+
5
+ `esgpull` is a tool that simplifies usage of the [ESGF Search API](https://esgf.github.io/esg-search/ESGF_Search_RESTful_API.html) for data discovery, and manages procedures related to downloading and storing files from ESGF.
6
+
7
+ ```py
8
+ from esgpull import Esgpull, Query
9
+
10
+ query = Query()
11
+ query.selection.project = "CMIP6"
12
+ query.options.distrib = True # default=False
13
+ esg = Esgpull()
14
+ nb_datasets = esg.context.hits(query, file=False)[0]
15
+ nb_files = esg.context.hits(query, file=True)[0]
16
+ datasets = esg.context.datasets(query, max_hits=5)
17
+ print(f"Number of CMIP6 datasets: {nb_datasets}")
18
+ print(f"Number of CMIP6 files: {nb_files}")
19
+ for dataset in datasets:
20
+ print(dataset)
21
+ ```
22
+
23
+ ## Features
24
+
25
+ - Command-line interface
26
+ - HTTP download (async multi-file)
27
+
28
+ ## Usage
29
+
30
+ ```console
31
+ Usage: esgpull [OPTIONS] COMMAND [ARGS]...
32
+
33
+ esgpull is a management utility for files and datasets from ESGF.
34
+
35
+ Options:
36
+ -V, --version Show the version and exit.
37
+ -h, --help Show this message and exit.
38
+
39
+ Commands:
40
+ add Add queries to the database
41
+ config View/modify config
42
+ convert Convert synda selection files to esgpull queries
43
+ download Asynchronously download files linked to queries
44
+ login OpenID authentication and certificates renewal
45
+ remove Remove queries from the database
46
+ retry Re-queue failed and cancelled downloads
47
+ search Search datasets and files on ESGF
48
+ self Manage esgpull installations / import synda database
49
+ show View query tree
50
+ status View file queue status
51
+ track Track queries
52
+ untrack Untrack queries
53
+ update Fetch files, link files <-> queries, send files to download...
54
+ ```
@@ -0,0 +1,12 @@
1
+ from esgpull.context import Context
2
+ from esgpull.esgpull import Esgpull
3
+ from esgpull.models import File, Query
4
+ from esgpull.version import __version__
5
+
6
+ __all__ = [
7
+ "Context",
8
+ "Esgpull",
9
+ "File",
10
+ "Query",
11
+ "__version__",
12
+ ]
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum, unique
4
+ from pathlib import Path
5
+ from shutil import rmtree
6
+ from typing import Any
7
+ from urllib.parse import (
8
+ ParseResult,
9
+ ParseResultBytes,
10
+ urljoin,
11
+ urlparse,
12
+ urlunparse,
13
+ )
14
+ from xml.etree import ElementTree
15
+
16
+ import httpx
17
+ import tomlkit
18
+ from attrs import Factory, define, field
19
+ from myproxy.client import MyProxyClient
20
+ from OpenSSL import crypto
21
+
22
+ from esgpull.config import Config
23
+ from esgpull.constants import PROVIDERS
24
+
25
+
26
+ class Secret:
27
+ def __init__(self, value: str | None = None) -> None:
28
+ self._value = value
29
+
30
+ def get_value(self) -> str | None:
31
+ return self._value
32
+
33
+ def __str__(self) -> str:
34
+ if self.get_value() is None:
35
+ return str(None)
36
+ else:
37
+ return "*" * 10
38
+
39
+ def __repr__(self) -> str:
40
+ return str(self)
41
+
42
+
43
+ @define
44
+ class Credentials:
45
+ provider: str | None = None
46
+ user: str | None = None
47
+ password: Secret = field(default=None, converter=Secret)
48
+
49
+ @staticmethod
50
+ def from_config(config: Config) -> Credentials:
51
+ path = config.paths.auth / config.credentials.filename
52
+ return Credentials.from_path(path)
53
+
54
+ @staticmethod
55
+ def from_path(path: Path) -> Credentials:
56
+ if path.is_file():
57
+ with path.open() as fh:
58
+ doc = tomlkit.load(fh)
59
+ return Credentials(**doc)
60
+ else:
61
+ return Credentials()
62
+
63
+ def write(self, path: Path) -> None:
64
+ if path.is_file():
65
+ raise FileExistsError(path)
66
+ with path.open("w") as f:
67
+ cred_dict = dict(
68
+ provider=self.provider,
69
+ user=self.user,
70
+ password=self.password.get_value(),
71
+ )
72
+ tomlkit.dump(cred_dict, f)
73
+
74
+ def parse_openid(self) -> ParseResult | ParseResultBytes | Any:
75
+ if self.provider not in PROVIDERS:
76
+ raise ValueError(f"unknown provider: {self.provider}")
77
+ ns = {"x": "xri://$xrd*($v*2.0)"}
78
+ provider = urlunparse(
79
+ [
80
+ "https",
81
+ self.provider,
82
+ urljoin(PROVIDERS[self.provider], self.user),
83
+ "",
84
+ "",
85
+ "",
86
+ ]
87
+ )
88
+ resp = httpx.get(str(provider))
89
+ resp.raise_for_status()
90
+ root = ElementTree.fromstring(resp.text)
91
+ services = root.findall(".//x:Service", namespaces=ns)
92
+ for service in services:
93
+ t = service.find("x:Type", namespaces=ns)
94
+ if t is None:
95
+ continue
96
+ elif t.text == "urn:esg:security:myproxy-service":
97
+ url = service.find("x:URI", namespaces=ns)
98
+ if url is not None:
99
+ return urlparse(url.text)
100
+ raise ValueError("did not found host/port")
101
+
102
+
103
+ @unique
104
+ class AuthStatus(Enum):
105
+ Valid = ("valid", "green")
106
+ Expired = ("expired", "orange")
107
+ Missing = ("missing", "red")
108
+
109
+
110
+ @define
111
+ class Auth:
112
+ cert_dir: Path
113
+ cert_file: Path
114
+ credentials: Credentials = Factory(Credentials)
115
+ __status: AuthStatus | None = field(init=False, default=None, repr=False)
116
+
117
+ Valid = AuthStatus.Valid
118
+ Expired = AuthStatus.Expired
119
+ Missing = AuthStatus.Missing
120
+
121
+ @staticmethod
122
+ def from_config(
123
+ config: Config, credentials: Credentials = Credentials()
124
+ ) -> Auth:
125
+ return Auth.from_path(config.paths.auth, credentials)
126
+
127
+ @staticmethod
128
+ def from_path(
129
+ path: Path, credentials: Credentials = Credentials()
130
+ ) -> Auth:
131
+ cert_dir = path / "certificates"
132
+ cert_file = path / "credentials.pem"
133
+ return Auth(cert_dir, cert_file, credentials)
134
+
135
+ @property
136
+ def cert(self) -> str | None:
137
+ if self.status == AuthStatus.Valid:
138
+ return str(self.cert_file)
139
+ else:
140
+ return None
141
+
142
+ @property
143
+ def status(self) -> AuthStatus:
144
+ if self.__status is None:
145
+ self.__status = self._get_status()
146
+ return self.__status
147
+
148
+ def _get_status(self) -> AuthStatus:
149
+ if not self.cert_file.exists():
150
+ return AuthStatus.Missing
151
+ with self.cert_file.open("rb") as f:
152
+ content = f.read()
153
+ filetype = crypto.FILETYPE_PEM
154
+ pem = crypto.load_certificate(filetype, content)
155
+ if pem.has_expired():
156
+ return AuthStatus.Expired
157
+ return AuthStatus.Valid
158
+
159
+ # TODO: review this
160
+ def renew(self) -> None:
161
+ if self.cert_dir.is_dir():
162
+ rmtree(self.cert_dir)
163
+ self.cert_file.unlink(missing_ok=True)
164
+ openid = self.credentials.parse_openid()
165
+ client = MyProxyClient(
166
+ hostname=openid.hostname,
167
+ port=openid.port,
168
+ caCertDir=str(self.cert_dir),
169
+ proxyCertLifetime=12 * 60 * 60,
170
+ )
171
+ creds = client.logon(
172
+ self.credentials.user,
173
+ self.credentials.password.get_value(),
174
+ bootstrap=True,
175
+ updateTrustRoots=True,
176
+ authnGetTrustRootsCall=False,
177
+ )
178
+ with self.cert_file.open("wb") as file:
179
+ for cred in creds:
180
+ file.write(cred)
181
+ self.__status = None
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # https://click.palletsprojects.com/en/latest/
4
+ import click
5
+
6
+ from esgpull import __version__
7
+ from esgpull.cli.add import add
8
+ from esgpull.cli.config import config
9
+ from esgpull.cli.convert import convert
10
+ from esgpull.cli.download import download
11
+ from esgpull.cli.login import login
12
+ from esgpull.cli.remove import remove
13
+ from esgpull.cli.retry import retry
14
+ from esgpull.cli.search import search
15
+ from esgpull.cli.self import self
16
+ from esgpull.cli.show import show
17
+ from esgpull.cli.status import status
18
+ from esgpull.cli.track import track, untrack
19
+ from esgpull.cli.update import update
20
+ from esgpull.tui import UI
21
+
22
+ # from esgpull.cli.autoremove import autoremove
23
+ # from esgpull.cli.facet import facet
24
+ # from esgpull.cli.get import get
25
+ # from esgpull.cli.install import install
26
+
27
+ # [-]TODO: stats
28
+ # - speed per index/data node
29
+ # - total disk usage
30
+ # - log config for later optimisation ?
31
+
32
+ SUBCOMMANDS: list[click.Command] = [
33
+ add,
34
+ # autoremove,
35
+ config,
36
+ convert,
37
+ download,
38
+ # facet,
39
+ # get,
40
+ self,
41
+ # install,
42
+ login,
43
+ remove,
44
+ retry,
45
+ search,
46
+ show,
47
+ track,
48
+ untrack,
49
+ status,
50
+ # # stats,
51
+ update,
52
+ ]
53
+
54
+ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
55
+
56
+ _ui = UI("/tmp")
57
+ version_msg = _ui.render(f"esgpull, version [green]{__version__}[/]")
58
+
59
+
60
+ @click.group(context_settings=CONTEXT_SETTINGS)
61
+ @click.version_option(None, "-V", "--version", message=version_msg)
62
+ def cli():
63
+ """
64
+ esgpull is a management utility for files and datasets from ESGF.
65
+ """
66
+
67
+
68
+ for subcmd in SUBCOMMANDS:
69
+ cli.add_command(subcmd)
70
+
71
+
72
+ def main():
73
+ cli()
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
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 (
10
+ init_esgpull,
11
+ parse_query,
12
+ serialize_queries_from_file,
13
+ )
14
+ from esgpull.graph import Graph
15
+ from esgpull.models import Query
16
+ from esgpull.tui import Verbosity
17
+
18
+
19
+ @click.command()
20
+ @args.facets
21
+ @groups.query_def
22
+ @opts.query_file
23
+ @opts.track
24
+ @opts.record
25
+ @opts.verbosity
26
+ def add(
27
+ facets: list[str],
28
+ # query options
29
+ tags: list[str],
30
+ require: str | None,
31
+ distrib: str | None,
32
+ latest: str | None,
33
+ replica: str | None,
34
+ retracted: str | None,
35
+ # since: str | None,
36
+ query_file: Path | None,
37
+ track: bool,
38
+ record: bool,
39
+ verbosity: Verbosity,
40
+ ) -> None:
41
+ """
42
+ Add queries to the database
43
+
44
+ OPTIONS / FACETS examples:
45
+
46
+ esgpull add --distrib true variable_id:co2,co3 mip_era:CMIP6
47
+
48
+ Syntax reference: http://www.esgf.io/esgf-download/search/
49
+
50
+ esgpull add --query-file path/to/query.yaml
51
+
52
+ Valid query files are usually created with either `show --json/--yaml` or `convert` commands.
53
+
54
+ Queries are `untracked` by default.
55
+
56
+ To fetch files from ESGF and link them to a query, see the `track` and `update` commands.
57
+ """
58
+ esg = init_esgpull(verbosity, record=record)
59
+ with esg.ui.logging("add", onraise=Abort):
60
+ if query_file is not None:
61
+ queries = serialize_queries_from_file(query_file)
62
+ else:
63
+ query = parse_query(
64
+ facets=facets,
65
+ tags=tags,
66
+ require=require,
67
+ distrib=distrib,
68
+ latest=latest,
69
+ replica=replica,
70
+ retracted=retracted,
71
+ )
72
+ esg.graph.resolve_require(query)
73
+ if track:
74
+ if query.require is not None:
75
+ expanded = esg.graph.expand(query.require)
76
+ else:
77
+ expanded = query
78
+ query.track(expanded.options)
79
+ queries = [query]
80
+ subgraph = Graph(None)
81
+ subgraph.add(*queries)
82
+ esg.ui.print(subgraph)
83
+ empty = Query()
84
+ empty.compute_sha()
85
+ for query in queries:
86
+ query.compute_sha()
87
+ esg.graph.resolve_require(query)
88
+ if query.sha == empty.sha:
89
+ esg.ui.print(":stop_sign: Trying to add empty query.")
90
+ esg.ui.raise_maybe_record(Exit(1))
91
+ if query.sha in esg.graph: # esg.graph.has(sha=query.sha):
92
+ esg.ui.print(f"Skipping existing query: {query.rich_name}")
93
+ else:
94
+ esg.graph.add(query)
95
+ esg.ui.print(f"New query added: {query.rich_name}")
96
+ new_queries = esg.graph.merge()
97
+ nb = len(new_queries)
98
+ ies = "ies" if nb > 1 else "y"
99
+ if new_queries:
100
+ esg.ui.print(f":+1: {nb} new quer{ies} added.")
101
+ else:
102
+ esg.ui.print(":stop_sign: No new query was added.")
103
+ esg.ui.raise_maybe_record(Exit(0))
@@ -0,0 +1,38 @@
1
+ # import click
2
+ # from click.exceptions import Abort, Exit
3
+
4
+ # from esgpull import Esgpull
5
+ # from esgpull.cli.decorators import opts
6
+ # from esgpull.cli.utils import filter_docs, totable
7
+ # from esgpull.tui import Verbosity
8
+
9
+
10
+ # @click.command()
11
+ # @opts.force
12
+ # @opts.verbosity
13
+ # def autoremove(
14
+ # force: bool,
15
+ # verbosity: Verbosity,
16
+ # ):
17
+ # esg = Esgpull(verbosity=verbosity)
18
+ # with esg.ui.logging("autoremove", onraise=Abort):
19
+ # deprecated = esg.db.get_deprecated_files()
20
+ # nb = len(deprecated)
21
+ # if not nb:
22
+ # esg.ui.print("All files are up to date.")
23
+ # raise Exit(0)
24
+ # if not force:
25
+ # docs = filter_docs([file.raw for file in deprecated])
26
+ # esg.ui.print(totable(docs))
27
+ # s = "s" if nb > 1 else ""
28
+ # esg.ui.print(f"Found {nb} file{s} to remove.")
29
+ # if not esg.ui.ask("Continue?", default=True):
30
+ # raise Abort
31
+ # removed = esg.remove(*deprecated)
32
+ # esg.ui.print(f"Removed {len(removed)} files with newer version.")
33
+ # nb_remain = len(removed) - nb
34
+ # if nb_remain:
35
+ # esg.ui.print(f"{nb_remain} files could not be removed.")
36
+ # if force:
37
+ # docs = filter_docs([file.raw for file in removed])
38
+ # esg.ui.print(totable(docs))