esgpull 0.8.0__py3-none-any.whl → 0.9.0__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/cli/__init__.py CHANGED
@@ -7,9 +7,9 @@ from esgpull import __version__
7
7
  from esgpull.cli.add import add
8
8
  from esgpull.cli.config import config
9
9
  from esgpull.cli.convert import convert
10
- from esgpull.cli.datasets import datasets
11
10
  from esgpull.cli.download import download
12
11
  from esgpull.cli.login import login
12
+ from esgpull.cli.plugins import plugins
13
13
  from esgpull.cli.remove import remove
14
14
  from esgpull.cli.retry import retry
15
15
  from esgpull.cli.search import search
@@ -35,13 +35,13 @@ SUBCOMMANDS: list[click.Command] = [
35
35
  # autoremove,
36
36
  config,
37
37
  convert,
38
- datasets,
39
38
  download,
40
39
  # facet,
41
40
  # get,
42
41
  self,
43
42
  # install,
44
43
  login,
44
+ plugins,
45
45
  remove,
46
46
  retry,
47
47
  search,
esgpull/cli/add.py CHANGED
@@ -89,6 +89,7 @@ def add(
89
89
  esg.ui.print(subgraph)
90
90
  empty = Query()
91
91
  empty.compute_sha()
92
+ next_steps_queries = []
92
93
  for query in queries:
93
94
  query.compute_sha()
94
95
  esg.graph.resolve_require(query)
@@ -99,7 +100,8 @@ def add(
99
100
  esg.ui.print(f"Skipping existing query: {query.rich_name}")
100
101
  else:
101
102
  esg.graph.add(query)
102
- esg.ui.print(f"New query added: {query.rich_name}")
103
+ if query.tracked:
104
+ next_steps_queries.append(query)
103
105
  new_queries = esg.graph.merge()
104
106
  nb = len(new_queries)
105
107
  ies = "ies" if nb > 1 else "y"
@@ -107,4 +109,8 @@ def add(
107
109
  esg.ui.print(f":+1: {nb} new quer{ies} added.")
108
110
  else:
109
111
  esg.ui.print(":stop_sign: No new query was added.")
112
+ if next_steps_queries:
113
+ esg.ui.print("\nNext steps:\n")
114
+ for query in next_steps_queries:
115
+ esg.ui.print(f"\tesgpull update {query.sha[:6]}")
110
116
  esg.ui.raise_maybe_record(Exit(0))
esgpull/cli/config.py CHANGED
@@ -4,26 +4,11 @@ import click
4
4
  from click.exceptions import Abort, BadOptionUsage, Exit
5
5
 
6
6
  from esgpull.cli.decorators import args, opts
7
- from esgpull.cli.utils import init_esgpull
7
+ from esgpull.cli.utils import extract_subdict, init_esgpull
8
8
  from esgpull.config import ConfigKind
9
9
  from esgpull.tui import Verbosity
10
10
 
11
11
 
12
- def extract_command(doc: dict, key: str | None) -> dict:
13
- if key is None:
14
- return doc
15
- for part in key.split("."):
16
- if not part:
17
- raise KeyError(key)
18
- elif part in doc:
19
- doc = doc[part]
20
- else:
21
- raise KeyError(part)
22
- for part in key.split(".")[::-1]:
23
- doc = {part: doc}
24
- return doc
25
-
26
-
27
12
  @click.command()
28
13
  @args.key
29
14
  @args.value
@@ -73,7 +58,7 @@ def config(
73
58
  )
74
59
  kind = esg.config.kind
75
60
  old_value = esg.config.update_item(key, value, empty_ok=True)
76
- info = extract_command(esg.config.dump(), key)
61
+ info = extract_subdict(esg.config.dump(), key)
77
62
  esg.config.write()
78
63
  esg.ui.print(info, toml=True)
79
64
  if kind == ConfigKind.NoFile:
@@ -86,12 +71,12 @@ def config(
86
71
  elif key is not None:
87
72
  if default:
88
73
  old_value = esg.config.set_default(key)
89
- info = extract_command(esg.config.dump(), key)
74
+ info = extract_subdict(esg.config.dump(), key)
90
75
  esg.config.write()
91
76
  esg.ui.print(info, toml=True)
92
77
  esg.ui.print(f"Previous value: {old_value}")
93
78
  else:
94
- info = extract_command(esg.config.dump(), key)
79
+ info = extract_subdict(esg.config.dump(), key)
95
80
  esg.ui.print(info, toml=True)
96
81
  elif generate:
97
82
  overwrite = False
@@ -102,8 +87,7 @@ def config(
102
87
  )
103
88
  esg.ui.raise_maybe_record(Exit(0))
104
89
  elif esg.config.kind == ConfigKind.Partial and esg.ui.ask(
105
- "A config file already exists,"
106
- " fill it with missing defaults?",
90
+ "A config file already exists, fill it with missing defaults?",
107
91
  default=False,
108
92
  ):
109
93
  overwrite = True
esgpull/cli/plugins.py ADDED
@@ -0,0 +1,398 @@
1
+ from collections import OrderedDict
2
+ from textwrap import dedent
3
+
4
+ import click
5
+ from click.exceptions import Abort, BadOptionUsage
6
+
7
+ from esgpull import Esgpull
8
+ from esgpull.cli.decorators import args, opts
9
+ from esgpull.cli.utils import extract_subdict, init_esgpull, totable
10
+ from esgpull.models import Dataset, File, FileStatus
11
+ from esgpull.plugin import Event, emit
12
+ from esgpull.tui import Verbosity
13
+ from esgpull.version import __version__
14
+
15
+
16
+ @click.group()
17
+ def plugins():
18
+ """Manage plugins."""
19
+ pass
20
+
21
+
22
+ def check_enabled(esg: Esgpull):
23
+ if not esg.plugin_manager.enabled:
24
+ esg.ui.print("Plugin system is [red]disabled[/]")
25
+ esg.ui.print("To enable it, run:")
26
+ esg.ui.print(" esgpull config plugins.enabled true")
27
+ raise Abort
28
+
29
+
30
+ @plugins.command("ls")
31
+ @click.option(
32
+ "--json", "json_output", is_flag=True, help="Output in JSON format"
33
+ )
34
+ @opts.verbosity
35
+ def list_plugins(verbosity: Verbosity, json_output: bool = False):
36
+ """List all available plugins and their status."""
37
+ esg = init_esgpull(verbosity=verbosity)
38
+
39
+ with esg.ui.logging("plugins", onraise=Abort):
40
+ check_enabled(esg)
41
+ esg.plugin_manager.discover_plugins(
42
+ esg.config.paths.plugins, load_all=True
43
+ )
44
+ if json_output:
45
+ # Format for JSON output
46
+ result = {}
47
+ for name, p in esg.plugin_manager.plugins.items():
48
+ # Get handlers by event type
49
+ handlers = {}
50
+ for h in p.handlers:
51
+ event_type = h.event.value
52
+ if event_type not in handlers:
53
+ handlers[event_type] = []
54
+ handlers[event_type].append(
55
+ {
56
+ "function": h.func.__name__,
57
+ "priority": h.priority,
58
+ }
59
+ )
60
+
61
+ result[name] = {
62
+ "enabled": esg.plugin_manager.is_plugin_enabled(name),
63
+ "handlers": handlers,
64
+ "min_version": p.min_version,
65
+ "max_version": p.max_version,
66
+ }
67
+
68
+ esg.ui.print(result, json=True)
69
+ else:
70
+ # Format for human-readable output
71
+ if not esg.plugin_manager.plugins:
72
+ esg.ui.print("No plugins found.")
73
+ return
74
+
75
+ # Prepare table data
76
+ table_data = []
77
+ for name, p in esg.plugin_manager.plugins.items():
78
+ enabled = esg.plugin_manager.is_plugin_enabled(name)
79
+ status_circle = "🟢" if enabled else "🔴"
80
+ plugin_name = f"{status_circle} {name}"
81
+
82
+ # Collect event handlers with their details
83
+ handler_rows = []
84
+ events_by_type = {}
85
+ for h in p.handlers:
86
+ event_type = h.event.value
87
+ if event_type not in events_by_type:
88
+ events_by_type[event_type] = []
89
+ events_by_type[event_type].append(h.func.__name__)
90
+
91
+ # Create handler rows with event type and function names
92
+ for event_type, handler_names in events_by_type.items():
93
+ for handler_name in handler_names:
94
+ handler_rows.append(
95
+ {"event": event_type, "function": handler_name}
96
+ )
97
+
98
+ # First row with plugin info and first handler
99
+ if handler_rows:
100
+ first_handler = handler_rows[0]
101
+ first_row = OrderedDict(
102
+ [
103
+ ("plugin", plugin_name),
104
+ ("event", first_handler["event"]),
105
+ ("function", first_handler["function"]),
106
+ ]
107
+ )
108
+ table_data.append(first_row)
109
+
110
+ # Additional rows for remaining handlers
111
+ for handler in handler_rows[1:]:
112
+ additional_row = OrderedDict(
113
+ [
114
+ ("plugin", ""),
115
+ ("event", handler["event"]),
116
+ ("function", handler["function"]),
117
+ ]
118
+ )
119
+ table_data.append(additional_row)
120
+ else:
121
+ # Plugin with no handlers
122
+ row = OrderedDict(
123
+ [
124
+ ("plugin", plugin_name),
125
+ ("event", ""),
126
+ ("function", ""),
127
+ ]
128
+ )
129
+ table_data.append(row)
130
+
131
+ # Create and print table
132
+ table = totable(table_data)
133
+ esg.ui.print(table)
134
+
135
+
136
+ @plugins.command("enable")
137
+ @click.argument("plugin_name")
138
+ @opts.verbosity
139
+ def enable_plugin_cmd(plugin_name: str, verbosity: Verbosity):
140
+ """Enable a plugin."""
141
+ esg = init_esgpull(verbosity=verbosity)
142
+ plugin_path = esg.config.paths.plugins / f"{plugin_name}.py"
143
+ if not plugin_path.exists():
144
+ esg.ui.print(f"Plugin file '{plugin_name}.py' not found.")
145
+ return
146
+
147
+ result = esg.plugin_manager.enable_plugin(plugin_name)
148
+ if result:
149
+ esg.ui.print(f"Plugin '{plugin_name}' enabled.")
150
+ else:
151
+ esg.ui.print(f"Failed to enable plugin '{plugin_name}'.")
152
+
153
+
154
+ @plugins.command("disable")
155
+ @click.argument("plugin_name")
156
+ @opts.verbosity
157
+ def disable_plugin_cmd(plugin_name: str, verbosity: Verbosity):
158
+ """Disable a plugin."""
159
+ esg = init_esgpull(verbosity=verbosity)
160
+
161
+ # Check if the plugin file exists (even if not loaded)
162
+ plugin_path = esg.config.paths.plugins / f"{plugin_name}.py"
163
+ if not plugin_path.exists():
164
+ esg.ui.print(f"Plugin file '{plugin_name}.py' not found.")
165
+ return
166
+
167
+ result = esg.plugin_manager.disable_plugin(plugin_name)
168
+ if result:
169
+ esg.ui.print(f"Plugin '{plugin_name}' disabled.")
170
+ else:
171
+ esg.ui.print(f"Failed to disable plugin '{plugin_name}'.")
172
+
173
+
174
+ @plugins.command("config")
175
+ @args.key
176
+ @args.value
177
+ @opts.default
178
+ @opts.generate
179
+ @opts.verbosity
180
+ def config_plugin(
181
+ verbosity: Verbosity,
182
+ key: str | None,
183
+ value: str | None,
184
+ default: bool,
185
+ generate: bool,
186
+ # config_set: str | None = None,
187
+ # config_unset: str | None = None,
188
+ ):
189
+ """View or modify plugin configuration."""
190
+ esg = init_esgpull(verbosity=verbosity)
191
+
192
+ with esg.ui.logging("plugins", onraise=Abort):
193
+ check_enabled(esg)
194
+ esg.plugin_manager.discover_plugins(
195
+ esg.config.paths.plugins, load_all=True
196
+ )
197
+ if key is not None:
198
+ plugin_name = key.split(".", 1)[0]
199
+ plugin_path = esg.config.paths.plugins / f"{plugin_name}.py"
200
+ if not plugin_path.exists():
201
+ esg.ui.print(f"Plugin file '{plugin_name}.py' not found.")
202
+ raise Abort
203
+ if key is not None and value is not None:
204
+ """update config"""
205
+ if default:
206
+ raise BadOptionUsage(
207
+ "default",
208
+ dedent(
209
+ f"""
210
+ --default/-d is invalid with a value.
211
+ Instead use:
212
+
213
+ $ esgpull plugins config {key} -d
214
+ """
215
+ ),
216
+ )
217
+ # Split key into plugin_name and remaining_path
218
+ plugin_name, *rest = key.split(".", 1)
219
+ remaining_path = rest[0] if rest else None
220
+
221
+ # Set the configuration value
222
+ if remaining_path:
223
+ old_value = esg.plugin_manager.set_plugin_config(
224
+ plugin_name, remaining_path, value
225
+ )
226
+ info = extract_subdict(esg.plugin_manager.config.plugins, key)
227
+ esg.ui.print(info, toml=True)
228
+ esg.ui.print(f"Previous value: {old_value}")
229
+ else:
230
+ esg.ui.print(f"Cannot set plugin name directly: {plugin_name}")
231
+ raise Abort
232
+ elif key is not None:
233
+ if default:
234
+ # Split key into plugin_name and remaining_path
235
+ plugin_name, *rest = key.split(".", 1)
236
+ remaining_path = rest[0] if rest else None
237
+
238
+ if remaining_path:
239
+ esg.plugin_manager.unset_plugin_config(
240
+ plugin_name, remaining_path
241
+ )
242
+ msg = f":+1: Config reset to default for {key}"
243
+ esg.ui.print(msg)
244
+ else:
245
+ esg.ui.print(
246
+ f"Cannot reset plugin directly: {plugin_name}"
247
+ )
248
+ raise Abort
249
+ else:
250
+ config = esg.plugin_manager.config.plugins
251
+ config = extract_subdict(config, key)
252
+ esg.ui.print(config, toml=True)
253
+ elif generate:
254
+ # Write the config file with all current settings
255
+ esg.plugin_manager.write_config(generate_full_config=True)
256
+ msg = f":+1: Plugin config generated at {esg.plugin_manager.config_path}"
257
+ esg.ui.print(msg)
258
+
259
+ else:
260
+ esg.ui.rule(str(esg.plugin_manager.config_path))
261
+ esg.ui.print(esg.plugin_manager.config.plugins, toml=True)
262
+
263
+
264
+ @plugins.command("test")
265
+ @click.argument(
266
+ "event_type",
267
+ type=click.Choice([e.value for e in Event]),
268
+ )
269
+ @opts.verbosity
270
+ def test_plugin(
271
+ event_type: str,
272
+ verbosity: Verbosity,
273
+ ):
274
+ """Test plugin events."""
275
+ esg = init_esgpull(verbosity=verbosity)
276
+ with esg.ui.logging("plugins", onraise=Abort):
277
+ check_enabled(esg)
278
+ event = Event(event_type)
279
+ # Create sample test data
280
+ file1 = File(
281
+ file_id="file1",
282
+ dataset_id="dataset",
283
+ master_id="master",
284
+ url="file",
285
+ version="v0",
286
+ filename="file.nc",
287
+ local_path="project/folder",
288
+ data_node="data_node",
289
+ checksum="0",
290
+ checksum_type="0",
291
+ size=2**42,
292
+ status=FileStatus.Queued,
293
+ )
294
+ file2 = file1.clone()
295
+ file2.file_id = "file2"
296
+ file2.status = FileStatus.Done
297
+ error = ValueError("Placeholder example error")
298
+ dataset = Dataset(dataset_id="dataset", total_files=2)
299
+ dataset.files = [file1, file2]
300
+ emit(
301
+ event,
302
+ file=file1,
303
+ dataset=dataset,
304
+ error=error,
305
+ )
306
+
307
+
308
+ @plugins.command("create")
309
+ @click.argument(
310
+ "events",
311
+ nargs=-1,
312
+ type=click.Choice([e.value for e in Event]),
313
+ )
314
+ @click.option("-n", "--name", required=True, type=str)
315
+ @opts.verbosity
316
+ def create_plugin(
317
+ name: str, verbosity: Verbosity, events: list[str] | None = None
318
+ ):
319
+ """Create a new plugin template."""
320
+ esg = init_esgpull(verbosity=verbosity)
321
+
322
+ # Create plugin file path
323
+ plugin_path = esg.config.paths.plugins / f"{name}.py"
324
+ if plugin_path.exists():
325
+ esg.ui.print(f"Plugin file already exists: {plugin_path}")
326
+ return
327
+
328
+ # If no events specified, include them all
329
+ if not events:
330
+ events = [e.value for e in Event]
331
+
332
+ # Generate template
333
+ template = f"""# {name} plugin for ESGPull
334
+ #
335
+ # This plugin was auto-generated. Edit as needed.
336
+ from datetime import datetime
337
+ from pathlib import Path
338
+ from logging import Logger
339
+
340
+ from esgpull.models import File, Query
341
+ from esgpull.plugin import Event, on
342
+
343
+ # Specify version compatibility (optional)
344
+ MIN_ESGPULL_VERSION = "{__version__}"
345
+ MAX_ESGPULL_VERSION = None
346
+
347
+ # Configuration class for the plugin (optional)
348
+ class Config:
349
+ \"""Configuration for {name} plugin\"""
350
+ # Add your configuration options here
351
+ timeout = 30 # Example config option
352
+ log_level = "INFO" # Example config option
353
+
354
+ """
355
+
356
+ # Add handlers for requested events
357
+ handlers = {
358
+ "file_complete": """
359
+ # File complete event handler
360
+ @on(Event.file_complete, priority="normal")
361
+ def handle_file_complete(
362
+ file: File,
363
+ destination: Path,
364
+ start_time: datetime,
365
+ end_time: datetime,
366
+ logger: Logger
367
+ ):
368
+ \"""Handle file complete event\"""
369
+ logger.info(f"File downloaded: {file.filename}")
370
+ # Add your custom logic here
371
+ """,
372
+ "file_error": """
373
+ # Download error event handler
374
+ @on(Event.file_error, priority="normal")
375
+ def handle_file_error(file: File, exception: Exception, logger: Logger):
376
+ \"""Handle file error event\"""
377
+ logger.error(f"Download failed for {file.filename}: {exception}")
378
+ # Add your custom logic here
379
+ """,
380
+ "dataset_complete": """
381
+ # Dataset complete event handler
382
+ @on(Event.dataset_complete, priority="normal")
383
+ def handle_dataset_complete(dataset: Dataset, logger: Logger):
384
+ \"""Handle dataset complete event\"""
385
+ logger.error(f"Dataset downloaded: {dataset.dataset_id}")
386
+ # Add your custom logic here
387
+ """,
388
+ }
389
+
390
+ for event in events:
391
+ template += handlers[event]
392
+
393
+ # Write the plugin file
394
+ with open(plugin_path, "w") as f:
395
+ f.write(template)
396
+
397
+ esg.ui.print(f"Plugin template created at: {plugin_path}")
398
+ esg.ui.print("Edit the file to implement your custom plugin logic.")
esgpull/cli/update.py CHANGED
@@ -10,7 +10,7 @@ from esgpull.cli.decorators import args, opts
10
10
  from esgpull.cli.utils import get_queries, init_esgpull, valid_name_tag
11
11
  from esgpull.context import HintsDict, ResultSearch
12
12
  from esgpull.exceptions import UnsetOptionsError
13
- from esgpull.models import File, FileStatus, Query
13
+ from esgpull.models import Dataset, File, FileStatus, Query
14
14
  from esgpull.tui import Verbosity, logger
15
15
  from esgpull.utils import format_size
16
16
 
@@ -20,10 +20,13 @@ class QueryFiles:
20
20
  query: Query
21
21
  expanded: Query
22
22
  skip: bool = False
23
+ datasets: list[Dataset] = field(default_factory=list)
23
24
  files: list[File] = field(default_factory=list)
25
+ dataset_hits: int = field(init=False)
24
26
  hits: int = field(init=False)
25
27
  hints: HintsDict = field(init=False)
26
28
  results: list[ResultSearch] = field(init=False)
29
+ dataset_results: list[ResultSearch] = field(init=False)
27
30
 
28
31
 
29
32
  @click.command()
@@ -80,20 +83,27 @@ def update(
80
83
  file=True,
81
84
  facets=["index_node"],
82
85
  )
83
- for qf, qf_hints in zip(qfs, hints):
86
+ dataset_hits = esg.context.hits(
87
+ *[qf.expanded for qf in qfs],
88
+ file=False,
89
+ )
90
+ for qf, qf_hints, qf_dataset_hits in zip(qfs, hints, dataset_hits):
84
91
  qf.hits = sum(esg.context.hits_from_hints(qf_hints))
85
92
  if qf_hints:
86
93
  qf.hints = qf_hints
94
+ qf.dataset_hits = qf_dataset_hits
87
95
  else:
88
96
  qf.skip = True
89
97
  for qf in qfs:
90
98
  s = "s" if qf.hits > 1 else ""
91
- esg.ui.print(f"{qf.query.rich_name} -> {qf.hits} file{s}.")
99
+ esg.ui.print(
100
+ f"{qf.query.rich_name} -> {qf.hits} file{s} (before replica de-duplication)."
101
+ )
92
102
  total_hits = sum([qf.hits for qf in qfs])
93
103
  if total_hits == 0:
94
104
  esg.ui.print("No files found.")
95
105
  esg.ui.raise_maybe_record(Exit(0))
96
- else:
106
+ elif len(qfs) > 1:
97
107
  esg.ui.print(f"{total_hits} files found.")
98
108
  qfs = [qf for qf in qfs if not qf.skip]
99
109
  # Prepare optimally distributed requests to ESGF
@@ -101,6 +111,12 @@ def update(
101
111
  # It might be interesting for the special case where all files already
102
112
  # exist in db, then the detailed fetch could be skipped.
103
113
  for qf in qfs:
114
+ qf_dataset_results = esg.context.prepare_search(
115
+ qf.expanded,
116
+ file=False,
117
+ hits=[qf.dataset_hits],
118
+ max_hits=None,
119
+ )
104
120
  if esg.config.api.use_custom_distribution_algorithm:
105
121
  qf_results = esg.context.prepare_search_distributed(
106
122
  qf.expanded,
@@ -115,7 +131,7 @@ def update(
115
131
  hits=[qf.hits],
116
132
  max_hits=None,
117
133
  )
118
- nb_req = len(qf_results)
134
+ nb_req = len(qf_dataset_results) + len(qf_results)
119
135
  if nb_req > 50:
120
136
  msg = (
121
137
  f"{nb_req} requests will be sent to ESGF to"
@@ -126,13 +142,32 @@ def update(
126
142
  esg.ui.print(f"{qf.query.rich_name} is now untracked.")
127
143
  qf.query.tracked = False
128
144
  qf_results = []
145
+ qf_dataset_results = []
129
146
  case "n":
130
147
  qf_results = []
148
+ qf_dataset_results = []
131
149
  case _:
132
150
  ...
133
151
  qf.results = qf_results
152
+ qf.dataset_results = qf_dataset_results
134
153
  # Fetch files and update db
135
154
  # [?] TODO: dry_run to print urls here
155
+ with esg.ui.spinner("Fetching datasets"):
156
+ coros = []
157
+ for qf in qfs:
158
+ coro = esg.context._datasets(
159
+ *qf.dataset_results, keep_duplicates=False
160
+ )
161
+ coros.append(coro)
162
+ datasets = esg.context.sync_gather(*coros)
163
+ for qf, qf_datasets in zip(qfs, datasets):
164
+ qf.datasets = [
165
+ Dataset(
166
+ dataset_id=record.dataset_id,
167
+ total_files=record.number_of_files,
168
+ )
169
+ for record in qf_datasets
170
+ ]
136
171
  with esg.ui.spinner("Fetching files"):
137
172
  coros = []
138
173
  for qf in qfs:
@@ -142,23 +177,26 @@ def update(
142
177
  for qf, qf_files in zip(qfs, files):
143
178
  qf.files = qf_files
144
179
  for qf in qfs:
145
- shas = {f.sha for f in qf.query.files}
146
- new_files: list[File] = []
147
- for file in qf.files:
148
- if file.sha not in shas:
149
- new_files.append(file)
180
+ new_files = [f for f in qf.files if f not in esg.db]
181
+ new_datasets = [d for d in qf.datasets if d not in esg.db]
182
+ nb_datasets = len(new_datasets)
150
183
  nb_files = len(new_files)
151
184
  if not qf.query.tracked:
152
185
  esg.db.add(qf.query)
153
186
  continue
154
- elif nb_files == 0:
187
+ elif nb_datasets == nb_files == 0:
155
188
  esg.ui.print(f"{qf.query.rich_name} is already up-to-date.")
156
189
  continue
157
190
  size = sum([file.size for file in new_files])
191
+ if size > 0:
192
+ queue_msg = " and send new files to download queue"
193
+ else:
194
+ queue_msg = ""
158
195
  msg = (
159
- f"\nUpdating {qf.query.rich_name} with {nb_files}"
160
- f" new files ({format_size(size)})."
161
- "\nSend to download queue?"
196
+ f"\n{qf.query.rich_name}: {nb_files} new"
197
+ f" files, {nb_datasets} new datasets"
198
+ f" ({format_size(size)})."
199
+ f"\nUpdate the database{queue_msg}?"
162
200
  )
163
201
  if yes:
164
202
  choice = "y"
@@ -175,9 +213,14 @@ def update(
175
213
  legacy = esg.legacy_query
176
214
  has_legacy = legacy.state.persistent
177
215
  with esg.db.commit_context():
216
+ for dataset in esg.ui.track(
217
+ new_datasets,
218
+ description=f"{qf.query.rich_name} (datasets)",
219
+ ):
220
+ esg.db.session.add(dataset)
178
221
  for file in esg.ui.track(
179
222
  new_files,
180
- description=qf.query.rich_name,
223
+ description=f"{qf.query.rich_name} (files)",
181
224
  ):
182
225
  file_db = esg.db.get(File, file.sha)
183
226
  if file_db is None:
esgpull/cli/utils.py CHANGED
@@ -103,7 +103,7 @@ def totable(docs: list[OrderedDict[str, Any]]) -> Table:
103
103
  table = Table(box=MINIMAL_DOUBLE_HEAD, show_edge=False)
104
104
  for key in docs[0].keys():
105
105
  justify: Literal["left", "right", "center"]
106
- if key in ["file", "dataset"]:
106
+ if key in ["file", "dataset", "plugin"]:
107
107
  justify = "left"
108
108
  else:
109
109
  justify = "right"
@@ -243,3 +243,18 @@ def get_queries(
243
243
  kids = graph.get_all_children(query.sha)
244
244
  queries.extend(kids)
245
245
  return queries
246
+
247
+
248
+ def extract_subdict(doc: dict, key: str | None) -> dict:
249
+ if key is None:
250
+ return doc
251
+ for part in key.split("."):
252
+ if not part:
253
+ raise KeyError(key)
254
+ elif part in doc:
255
+ doc = doc[part]
256
+ else:
257
+ raise KeyError(part)
258
+ for part in key.split(".")[::-1]:
259
+ doc = {part: doc}
260
+ return doc