esgpull 0.8.0__py3-none-any.whl → 0.9.1__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/remove.py CHANGED
@@ -15,11 +15,13 @@ from esgpull.utils import format_size
15
15
  @args.query_id
16
16
  @opts.tag
17
17
  @opts.children
18
+ @opts.yes
18
19
  @opts.verbosity
19
20
  def remove(
20
21
  query_id: str | None,
21
22
  tag: str | None,
22
23
  children: bool,
24
+ yes: bool,
23
25
  verbosity: Verbosity,
24
26
  ) -> None:
25
27
  """
@@ -49,7 +51,7 @@ def remove(
49
51
  graph.add(*queries)
50
52
  esg.ui.print(graph)
51
53
  msg = f"Remove {nb} quer{ies}?"
52
- if not esg.ui.ask(msg, default=True):
54
+ if not yes and not esg.ui.ask(msg, default=True):
53
55
  raise Abort
54
56
  for query in queries:
55
57
  if query.has_files:
@@ -59,14 +61,18 @@ def remove(
59
61
  f":stop_sign: {query.rich_name} is linked"
60
62
  f" to {nb} downloaded files ({format_size(size)})."
61
63
  )
62
- if not esg.ui.ask("Delete anyway?", default=False):
64
+ if not yes and not esg.ui.ask(
65
+ "Delete anyway?", default=False
66
+ ):
63
67
  raise Abort
64
68
  if not children and esg.graph.get_children(query.sha):
65
69
  esg.ui.print(
66
70
  ":stop_sign: Some queries block"
67
71
  f" removal of {query.rich_name}."
68
72
  )
69
- if esg.ui.ask("Show blocking queries?", default=False):
73
+ if not yes and esg.ui.ask(
74
+ "Show blocking queries?", default=False
75
+ ):
70
76
  esg.ui.print(esg.graph.subgraph(query, children=True))
71
77
  raise Exit(1)
72
78
  esg.db.delete(*queries)
esgpull/cli/self.py CHANGED
@@ -199,7 +199,7 @@ def delete():
199
199
  TempUI.print(f"Deleting {path} from config...")
200
200
  TempUI.print("To remove all files from this install, run:\n")
201
201
  config = Config.load(path=path)
202
- for p in config.paths:
202
+ for p in config.paths.values():
203
203
  if not p.is_relative_to(path):
204
204
  TempUI.print(f"$ rm -rf {p}")
205
205
  TempUI.print(f"$ rm -rf {path}")