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/utils.py ADDED
@@ -0,0 +1,247 @@
1
+ import sys
2
+ from collections import OrderedDict
3
+ from enum import Enum
4
+ from pathlib import Path
5
+ from typing import Any, Literal, MutableMapping, Sequence
6
+
7
+ import click
8
+ import yaml
9
+ from click.exceptions import BadArgumentUsage
10
+ from rich.box import MINIMAL_DOUBLE_HEAD
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+
14
+ from esgpull import Esgpull
15
+ from esgpull.graph import Graph
16
+ from esgpull.models import Dataset, File, Option, Options, Query, Selection
17
+ from esgpull.tui import UI, TempUI, Verbosity
18
+ from esgpull.utils import format_size
19
+
20
+
21
+ def get_command() -> Text:
22
+ exe, *args = sys.argv
23
+ args = [arg for arg in args if arg != "--record"]
24
+ return Text(" ".join(["$", Path(exe).name, *args]))
25
+
26
+
27
+ def init_esgpull(
28
+ verbosity: Verbosity,
29
+ safe: bool = True,
30
+ record: bool = False,
31
+ load_db: bool = True,
32
+ ) -> Esgpull:
33
+ TempUI.verbosity = Verbosity.Errors
34
+ with TempUI.logging():
35
+ esg = Esgpull(
36
+ verbosity=verbosity,
37
+ safe=safe,
38
+ record=record,
39
+ load_db=load_db,
40
+ )
41
+ if record:
42
+ esg.ui.print(get_command())
43
+ return esg
44
+
45
+
46
+ class Messages:
47
+ @staticmethod
48
+ def no_such_query(name: str) -> str:
49
+ return f":stop_sign: No such query [green]{name}[/]"
50
+
51
+ @staticmethod
52
+ def none_tagged(tag: str) -> str:
53
+ return f":stop_sign: No query tagged with [magenta]{tag}[/]"
54
+
55
+ @staticmethod
56
+ def multimatch(name: str) -> str:
57
+ return f"Found multiple queries starting with [b cyan]{name}[/]"
58
+
59
+
60
+ class EnumParam(click.Choice):
61
+ name = "enum"
62
+
63
+ def __init__(self, enum: type[Enum]):
64
+ self.__enum = enum
65
+ super().__init__(choices=[item.value for item in enum])
66
+
67
+ def convert(self, value, param, ctx) -> Enum:
68
+ converted_str = super().convert(value, param, ctx)
69
+ return self.__enum(converted_str)
70
+
71
+
72
+ def filter_keys(
73
+ docs: Sequence[File | Dataset],
74
+ ids: range,
75
+ size: bool = True,
76
+ data_node: bool = False,
77
+ # date: bool = False,
78
+ ) -> list[OrderedDict[str, Any]]:
79
+ result: list[OrderedDict[str, Any]] = []
80
+ for i, doc in zip(ids, docs):
81
+ od: OrderedDict[str, Any] = OrderedDict()
82
+ od["id"] = str(i)
83
+ if isinstance(doc, File):
84
+ od["file"] = doc.file_id
85
+ else:
86
+ od["dataset"] = doc.dataset_id
87
+ od["#"] = str(doc.number_of_files)
88
+ if size:
89
+ od["size"] = doc.size
90
+ if data_node:
91
+ od["data_node"] = doc.data_node
92
+ # if date:
93
+ # od["date"] = doc.get("timestamp") or doc.get("_timestamp")
94
+ result.append(od)
95
+ return result
96
+
97
+
98
+ def totable(docs: list[OrderedDict[str, Any]]) -> Table:
99
+ table = Table(box=MINIMAL_DOUBLE_HEAD, show_edge=False)
100
+ for key in docs[0].keys():
101
+ justify: Literal["left", "right", "center"]
102
+ if key in ["file", "dataset"]:
103
+ justify = "left"
104
+ else:
105
+ justify = "right"
106
+ table.add_column(
107
+ Text(key, justify="center"),
108
+ justify=justify,
109
+ )
110
+ for doc in docs:
111
+ row: list[str] = []
112
+ for key, value in doc.items():
113
+ if key == "size":
114
+ value = format_size(value)
115
+ row.append(value)
116
+ table.add_row(*row)
117
+ return table
118
+
119
+
120
+ def parse_facets(facets: list[str]) -> Selection:
121
+ facet_dict: dict[str, list[str]] = {}
122
+ exact_terms: list[str] | None = None
123
+ for facet in facets:
124
+ match facet.split(":"):
125
+ case [value]:
126
+ name = "query"
127
+ case [name, value] if name and value:
128
+ ...
129
+ case _:
130
+ raise BadArgumentUsage(f"{facet!r} is not valid syntax.")
131
+ if value.startswith("/"):
132
+ if exact_terms is not None:
133
+ raise BadArgumentUsage("Nested exact string is forbidden.")
134
+ exact_terms = []
135
+ if exact_terms is not None:
136
+ if name != "query":
137
+ raise BadArgumentUsage(
138
+ "Cannot use facet term inside an exact string."
139
+ )
140
+ exact_terms.append(value)
141
+ if value.endswith("/"):
142
+ final_exact_str = " ".join(exact_terms)
143
+ value = '"' + final_exact_str.strip("/") + '"'
144
+ exact_terms = None
145
+ else:
146
+ continue
147
+ facet_dict.setdefault(name, [])
148
+ facet_dict[name].append(value)
149
+ else:
150
+ values = value.split(",")
151
+ facet_dict.setdefault(name, [])
152
+ facet_dict[name].extend(values)
153
+ selection = Selection()
154
+ for name, values in facet_dict.items():
155
+ selection[name] = values
156
+ return selection
157
+
158
+
159
+ def parse_query(
160
+ facets: list[str],
161
+ # query options
162
+ tags: list[str],
163
+ require: str | None,
164
+ distrib: str | None,
165
+ latest: str | None,
166
+ replica: str | None,
167
+ retracted: str | None,
168
+ ) -> Query:
169
+ options = Options(
170
+ distrib=distrib or Option.notset,
171
+ latest=latest or Option.notset,
172
+ replica=replica or Option.notset,
173
+ retracted=retracted or Option.notset,
174
+ )
175
+ selection = parse_facets(facets)
176
+ return Query(
177
+ tags=tags,
178
+ require=require,
179
+ options=options,
180
+ selection=selection,
181
+ )
182
+
183
+
184
+ def is_list_of_maps(seq: Sequence) -> bool:
185
+ return all(isinstance(item, MutableMapping) for item in seq)
186
+
187
+
188
+ def serialize_queries_from_file(path: Path) -> list[Query]:
189
+ with path.open() as f:
190
+ content = yaml.safe_load(f)
191
+ queries: list[MutableMapping]
192
+ if isinstance(content, list):
193
+ queries = content
194
+ elif isinstance(content, MutableMapping):
195
+ values = list(content.values())
196
+ if is_list_of_maps(values):
197
+ queries = values
198
+ else:
199
+ queries = [content]
200
+ else:
201
+ raise ValueError(content)
202
+ return [Query(**query) for query in queries]
203
+
204
+
205
+ def valid_name_tag(
206
+ graph: Graph,
207
+ ui: UI,
208
+ query_id: str | None,
209
+ tag: str | None,
210
+ ) -> bool:
211
+ result = True
212
+ if query_id is not None:
213
+ shas = graph.matching_shas(query_id, graph._shas)
214
+ if len(shas) > 1:
215
+ ui.print(Messages.multimatch(query_id))
216
+ ui.print(shas, json=True)
217
+ result = False
218
+ elif len(shas) == 0:
219
+ ui.print(Messages.no_such_query(query_id), err=True)
220
+ result = False
221
+ elif tag is not None:
222
+ tags = [t.name for t in graph.get_tags()]
223
+ if tag not in tags:
224
+ ui.print(Messages.none_tagged(tag), err=True)
225
+ result = False
226
+ return result
227
+
228
+
229
+ def get_queries(
230
+ graph: Graph,
231
+ query_id: str | None,
232
+ tag: str | None,
233
+ children: bool = False,
234
+ ) -> list[Query]:
235
+ queries: list[Query] = []
236
+ if query_id is not None:
237
+ query = graph.get(name=query_id)
238
+ if query is not None:
239
+ queries = [query]
240
+ elif tag is not None:
241
+ queries = graph.with_tag(tag)
242
+ if children:
243
+ for i in range(len(queries)):
244
+ query = queries[0]
245
+ kids = graph.get_all_children(query.sha)
246
+ queries.extend(kids)
247
+ return queries
esgpull/config.py ADDED
@@ -0,0 +1,410 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from collections.abc import Container, Iterator, Mapping
5
+ from enum import Enum, auto
6
+ from pathlib import Path
7
+ from typing import Any, cast
8
+
9
+ import tomlkit
10
+ from attrs import Factory, define, field
11
+ from cattrs import Converter
12
+ from cattrs.gen import make_dict_unstructure_fn, override
13
+ from tomlkit import TOMLDocument
14
+
15
+ from esgpull.constants import CONFIG_FILENAME
16
+ from esgpull.exceptions import BadConfigError, VirtualConfigError
17
+ from esgpull.install_config import InstallConfig
18
+ from esgpull.models.options import Options
19
+
20
+ logger = logging.getLogger("esgpull")
21
+
22
+
23
+ @define
24
+ class Paths:
25
+ auth: Path = field(converter=Path)
26
+ data: Path = field(converter=Path)
27
+ db: Path = field(converter=Path)
28
+ log: Path = field(converter=Path)
29
+ tmp: Path = field(converter=Path)
30
+
31
+ @auth.default
32
+ def _auth_factory(self) -> Path:
33
+ if InstallConfig.current is not None:
34
+ root = InstallConfig.current.path
35
+ else:
36
+ root = InstallConfig.default
37
+ return root / "auth"
38
+
39
+ @data.default
40
+ def _data_factory(self) -> Path:
41
+ if InstallConfig.current is not None:
42
+ root = InstallConfig.current.path
43
+ else:
44
+ root = InstallConfig.default
45
+ return root / "data"
46
+
47
+ @db.default
48
+ def _db_factory(self) -> Path:
49
+ if InstallConfig.current is not None:
50
+ root = InstallConfig.current.path
51
+ else:
52
+ root = InstallConfig.default
53
+ return root / "db"
54
+
55
+ @log.default
56
+ def _log_factory(self) -> Path:
57
+ if InstallConfig.current is not None:
58
+ root = InstallConfig.current.path
59
+ else:
60
+ root = InstallConfig.default
61
+ return root / "log"
62
+
63
+ @tmp.default
64
+ def _tmp_factory(self) -> Path:
65
+ if InstallConfig.current is not None:
66
+ root = InstallConfig.current.path
67
+ else:
68
+ root = InstallConfig.default
69
+ return root / "tmp"
70
+
71
+ def __iter__(self) -> Iterator[Path]:
72
+ yield self.auth
73
+ yield self.data
74
+ yield self.db
75
+ yield self.log
76
+ yield self.tmp
77
+
78
+
79
+ @define
80
+ class Credentials:
81
+ filename: str = "credentials.toml"
82
+
83
+
84
+ @define
85
+ class Cli:
86
+ page_size: int = 20
87
+
88
+
89
+ @define
90
+ class Db:
91
+ filename: str = "esgpull.db"
92
+
93
+
94
+ @define
95
+ class Download:
96
+ chunk_size: int = 1 << 26 # 64 MiB
97
+ http_timeout: int = 20
98
+ max_concurrent: int = 5
99
+ disable_ssl: bool = False
100
+ disable_checksum: bool = False
101
+
102
+
103
+ @define
104
+ class DefaultOptions:
105
+ distrib: str = Options._distrib_.name
106
+ latest: str = Options._latest_.name
107
+ replica: str = Options._replica_.name
108
+ retracted: str = Options._retracted_.name
109
+
110
+ def asdict(self) -> dict[str, str]:
111
+ return dict(
112
+ distrib=self.distrib,
113
+ latest=self.latest,
114
+ replica=self.replica,
115
+ retracted=self.retracted,
116
+ )
117
+
118
+
119
+ @define
120
+ class API:
121
+ index_node: str = "esgf-node.ipsl.upmc.fr"
122
+ http_timeout: int = 20
123
+ max_concurrent: int = 5
124
+ page_limit: int = 50
125
+ default_options: DefaultOptions = Factory(DefaultOptions)
126
+
127
+
128
+ def fix_rename_search_api(doc: TOMLDocument) -> TOMLDocument:
129
+ if "api" in doc and "search" in doc:
130
+ raise KeyError(
131
+ "Both 'api' and 'search' (deprecated) are used in your "
132
+ "config, please use 'api' only."
133
+ )
134
+ elif "search" in doc:
135
+ logger.warn(
136
+ "Deprecated key 'search' is used in your config, "
137
+ "please use 'api' instead."
138
+ )
139
+ doc["api"] = doc.pop("search")
140
+ return doc
141
+
142
+
143
+ config_fixers = [fix_rename_search_api]
144
+
145
+
146
+ class ConfigKind(Enum):
147
+ Virtual = auto()
148
+ NoFile = auto()
149
+ Partial = auto()
150
+ Complete = auto()
151
+
152
+
153
+ class ConfigKey:
154
+ path: tuple[str, ...]
155
+
156
+ def __init__(self, first: str | tuple[str, ...], *rest: str) -> None:
157
+ if isinstance(first, tuple):
158
+ self.path = first + rest
159
+ elif "." in first:
160
+ self.path = tuple(first.split(".")) + rest
161
+ else:
162
+ self.path = (first,) + rest
163
+
164
+ def __iter__(self) -> Iterator[str]:
165
+ yield from self.path
166
+
167
+ def __hash__(self) -> int:
168
+ return hash(self.path)
169
+
170
+ def __repr__(self) -> str:
171
+ return ".".join(self)
172
+
173
+ def __add__(self, path: str) -> ConfigKey:
174
+ return ConfigKey(self.path, path)
175
+
176
+ def __len__(self) -> int:
177
+ return len(self.path)
178
+
179
+ def exists_in(self, source: Mapping | None) -> bool:
180
+ if source is None:
181
+ return False
182
+ doc = source
183
+ for key in self:
184
+ if key in doc:
185
+ doc = doc[key]
186
+ else:
187
+ return False
188
+ return True
189
+
190
+ def value_of(self, source: Mapping) -> Any:
191
+ doc = source
192
+ for key in self:
193
+ doc = doc[key]
194
+ return doc
195
+
196
+
197
+ def iter_keys(
198
+ source: Mapping,
199
+ path: ConfigKey | None = None,
200
+ ) -> Iterator[ConfigKey]:
201
+ for key in source.keys():
202
+ if path is None:
203
+ local_path = ConfigKey(key)
204
+ else:
205
+ local_path = path + key
206
+ if isinstance(source[key], Mapping):
207
+ yield from iter_keys(source[key], local_path)
208
+ else:
209
+ yield local_path
210
+
211
+
212
+ @define
213
+ class Config:
214
+ paths: Paths = Factory(Paths)
215
+ credentials: Credentials = Factory(Credentials)
216
+ cli: Cli = Factory(Cli)
217
+ db: Db = Factory(Db)
218
+ download: Download = Factory(Download)
219
+ api: API = Factory(API)
220
+ _raw: TOMLDocument | None = field(init=False, default=None)
221
+ _config_file: Path | None = field(init=False, default=None)
222
+
223
+ @classmethod
224
+ def load(cls, path: Path) -> Config:
225
+ config_file = path / CONFIG_FILENAME
226
+ if config_file.is_file():
227
+ with config_file.open() as fh:
228
+ doc = tomlkit.load(fh)
229
+ for fixer in config_fixers:
230
+ try:
231
+ doc = fixer(doc)
232
+ except Exception:
233
+ raise BadConfigError(config_file)
234
+ raw = doc
235
+ else:
236
+ doc = TOMLDocument()
237
+ raw = None
238
+ config = _converter_defaults.structure(doc, cls)
239
+ config._raw = raw
240
+ config._config_file = config_file
241
+ return config
242
+
243
+ @classmethod
244
+ def default(cls) -> Config:
245
+ if InstallConfig.current is not None:
246
+ root = InstallConfig.current.path
247
+ else:
248
+ root = InstallConfig.default
249
+ return cls.load(root)
250
+
251
+ @property
252
+ def kind(self) -> ConfigKind:
253
+ if self._config_file is None:
254
+ return ConfigKind.Virtual
255
+ elif not self._config_file.is_file():
256
+ return ConfigKind.NoFile
257
+ elif self.unset_options():
258
+ return ConfigKind.Partial
259
+ else:
260
+ return ConfigKind.Complete
261
+
262
+ def dumps(self, defaults: bool = True, comments: bool = False) -> str:
263
+ return self.dump(defaults, comments).as_string()
264
+
265
+ def dump(
266
+ self,
267
+ defaults: bool = True,
268
+ comments: bool = False,
269
+ ) -> TOMLDocument:
270
+ if defaults:
271
+ converter = _converter_defaults
272
+ else:
273
+ converter = _converter_no_defaults
274
+ dump = converter.unstructure(self)
275
+ if not defaults:
276
+ pop_empty(dump)
277
+ doc = TOMLDocument()
278
+ doc.update(dump)
279
+ # if comments and self._raw is not None:
280
+ # original = tomlkit.loads(self._raw)
281
+ return doc
282
+
283
+ def update_item(
284
+ self,
285
+ key: str,
286
+ value: int | str,
287
+ empty_ok: bool = False,
288
+ ) -> int | str | None:
289
+ if self._raw is None and empty_ok:
290
+ self._raw = TOMLDocument()
291
+ if self._raw is None:
292
+ raise VirtualConfigError
293
+ else:
294
+ doc: dict = self._raw
295
+ obj = self
296
+ *parts, last = ConfigKey(key)
297
+ for part in parts:
298
+ doc.setdefault(part, {})
299
+ doc = doc[part]
300
+ obj = getattr(obj, part)
301
+ old_value = getattr(obj, last)
302
+ if isinstance(old_value, str):
303
+ ...
304
+ elif isinstance(old_value, Container):
305
+ raise KeyError(key)
306
+ try:
307
+ value = int(value)
308
+ except ValueError:
309
+ ...
310
+ setattr(obj, last, value)
311
+ doc[last] = value
312
+ return old_value
313
+
314
+ def set_default(self, key: str) -> int | str | None:
315
+ ckey = ConfigKey(key)
316
+ if self._raw is None:
317
+ raise VirtualConfigError()
318
+ elif not ckey.exists_in(self._raw):
319
+ return None
320
+ default_config = self.__class__()
321
+ default_value = ckey.value_of(default_config.dump())
322
+ old_value: Any = ckey.value_of(self.dump())
323
+ first_pass = True
324
+ obj = self
325
+ for idx in range(len(ckey), 0, -1):
326
+ *parts, last = ckey.path[:idx]
327
+ doc: tomlkit.container.Container = self._raw
328
+ for part in parts:
329
+ if first_pass:
330
+ obj = getattr(obj, part)
331
+ doc = cast(tomlkit.container.Container, doc[part])
332
+ if first_pass:
333
+ doc.remove(last)
334
+ setattr(obj, last, default_value)
335
+ first_pass = False
336
+ elif (
337
+ (value := doc[last])
338
+ and isinstance(value, tomlkit.container.Container)
339
+ and len(value) == 0
340
+ ):
341
+ doc.remove(last)
342
+ return old_value
343
+
344
+ def unset_options(self) -> list[ConfigKey]:
345
+ result: list[ConfigKey] = []
346
+ raw: dict
347
+ dump = self.dump()
348
+ if self._raw is None:
349
+ raw = {}
350
+ else:
351
+ raw = self._raw
352
+ for ckey in iter_keys(dump):
353
+ if not ckey.exists_in(raw):
354
+ result.append(ckey)
355
+ return result
356
+
357
+ def generate(
358
+ self,
359
+ overwrite: bool = False,
360
+ ) -> None:
361
+ match (self.kind, overwrite):
362
+ case (ConfigKind.Virtual, _):
363
+ raise VirtualConfigError
364
+ case (ConfigKind.Partial, overwrite):
365
+ defaults = self.dump()
366
+ for ckey in self.unset_options():
367
+ self.update_item(str(ckey), ckey.value_of(defaults))
368
+ case (ConfigKind.Partial | ConfigKind.Complete, _):
369
+ raise FileExistsError(self._config_file)
370
+ case (ConfigKind.NoFile, _):
371
+ self._raw = self.dump()
372
+ case _:
373
+ raise ValueError(self.kind)
374
+ self.write()
375
+
376
+ def write(self) -> None:
377
+ if self.kind == ConfigKind.Virtual or self._raw is None:
378
+ raise VirtualConfigError
379
+ config_file = cast(Path, self._config_file)
380
+ with config_file.open("w") as f:
381
+ tomlkit.dump(self._raw, f)
382
+
383
+
384
+ def _make_converter(omit_default: bool) -> Converter:
385
+ conv = Converter(omit_if_default=omit_default, forbid_extra_keys=True)
386
+ conv.register_unstructure_hook(Path, str)
387
+ conv.register_unstructure_hook(
388
+ Config,
389
+ make_dict_unstructure_fn(
390
+ Config,
391
+ conv,
392
+ _raw=override(omit=True),
393
+ _config_file=override(omit=True),
394
+ ),
395
+ )
396
+ return conv
397
+
398
+
399
+ _converter_defaults = _make_converter(omit_default=False)
400
+ _converter_no_defaults = _make_converter(omit_default=True)
401
+
402
+
403
+ def pop_empty(d: dict[str, Any]) -> None:
404
+ keys = list(d.keys())
405
+ for key in keys:
406
+ value = d[key]
407
+ if isinstance(value, dict):
408
+ pop_empty(value)
409
+ if not value:
410
+ d.pop(key)
esgpull/constants.py ADDED
@@ -0,0 +1,56 @@
1
+ CONFIG_FILENAME = "config.toml"
2
+ ROOT_ENV = "ESGPULL_CURRENT"
3
+
4
+ IDP = "/esgf-idp/openid/"
5
+ CEDA_IDP = "/OpenID/Provider/server/"
6
+ PROVIDERS = {
7
+ "esg-dn1.nsc.liu.se": IDP,
8
+ "esgf-data.dkrz.de": IDP,
9
+ "ceda.ac.uk": CEDA_IDP,
10
+ "esgf-node.ipsl.upmc.fr": IDP,
11
+ "esgf-node.llnl.gov": IDP,
12
+ "esgf.nci.org.au": IDP,
13
+ }
14
+
15
+
16
+ DEFAULT_FACETS = [
17
+ "project",
18
+ "mip_era",
19
+ "experiment",
20
+ "experiment_id",
21
+ "institute",
22
+ "institution_id",
23
+ "model",
24
+ "table_id",
25
+ "activity_id",
26
+ "ensemble",
27
+ "variant_label",
28
+ "realm",
29
+ "frequency",
30
+ "time_frequency",
31
+ "variable",
32
+ "variable_id",
33
+ "dataset_id",
34
+ "source_id",
35
+ "domain",
36
+ "driving_model",
37
+ "rcm_name",
38
+ "member_id",
39
+ "cmor_table",
40
+ ]
41
+ EXTRA_FACETS = [
42
+ "query",
43
+ "start",
44
+ "end",
45
+ # "fields",
46
+ "facets",
47
+ "url",
48
+ "data_node",
49
+ "index_node",
50
+ "master_id",
51
+ "instance_id", # search does not work with instance_id
52
+ "title",
53
+ "variable_long_name",
54
+ "experiment_family",
55
+ ]
56
+ DEFAULT_CONSTRAINTS_WITH_VALUE: dict[str, str] = {}