esgpull 0.6.5__py3-none-any.whl → 0.7.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,6 +7,7 @@ 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
10
11
  from esgpull.cli.download import download
11
12
  from esgpull.cli.login import login
12
13
  from esgpull.cli.remove import remove
@@ -34,6 +35,7 @@ SUBCOMMANDS: list[click.Command] = [
34
35
  # autoremove,
35
36
  config,
36
37
  convert,
38
+ datasets,
37
39
  download,
38
40
  # facet,
39
41
  # get,
esgpull/cli/add.py CHANGED
@@ -21,20 +21,22 @@ from esgpull.tui import Verbosity
21
21
  @groups.query_def
22
22
  @opts.query_file
23
23
  @opts.track
24
+ @opts.no_default_query
24
25
  @opts.record
25
26
  @opts.verbosity
26
27
  def add(
27
28
  facets: list[str],
28
- # query options
29
+ ## query_def
29
30
  tags: list[str],
30
31
  require: str | None,
31
32
  distrib: str | None,
32
33
  latest: str | None,
33
34
  replica: str | None,
34
35
  retracted: str | None,
35
- # since: str | None,
36
+ ## ungrouped
36
37
  query_file: Path | None,
37
38
  track: bool,
39
+ no_default_query: bool,
38
40
  record: bool,
39
41
  verbosity: Verbosity,
40
42
  ) -> None:
@@ -55,7 +57,11 @@ def add(
55
57
 
56
58
  To fetch files from ESGF and link them to a query, see the `track` and `update` commands.
57
59
  """
58
- esg = init_esgpull(verbosity, record=record)
60
+ esg = init_esgpull(
61
+ verbosity,
62
+ record=record,
63
+ no_default_query=no_default_query,
64
+ )
59
65
  with esg.ui.logging("add", onraise=Abort):
60
66
  if query_file is not None:
61
67
  queries = serialize_queries_from_file(query_file)
@@ -77,6 +83,7 @@ def add(
77
83
  expanded = query
78
84
  query.track(expanded.options)
79
85
  queries = [query]
86
+ queries = esg.insert_default_query(*queries)
80
87
  subgraph = Graph(None)
81
88
  subgraph.add(*queries)
82
89
  esg.ui.print(subgraph)
@@ -0,0 +1,78 @@
1
+ from collections import defaultdict
2
+ from dataclasses import dataclass
3
+
4
+ import click
5
+ from click.exceptions import Abort, Exit
6
+ from rich.box import MINIMAL_DOUBLE_HEAD
7
+ from rich.table import Table
8
+
9
+ from esgpull.cli.decorators import args, groups, opts
10
+ from esgpull.cli.utils import init_esgpull, valid_name_tag
11
+ from esgpull.models import FileStatus
12
+ from esgpull.tui import Verbosity
13
+
14
+
15
+ @dataclass
16
+ class DatasetCounter:
17
+ done: int = 0
18
+ total: int = 0
19
+
20
+ def is_complete(self) -> int:
21
+ return self.done == self.total
22
+
23
+ def asdict(self) -> dict:
24
+ return {
25
+ "done": self.done,
26
+ "total": self.total,
27
+ "complete": self.is_complete(),
28
+ }
29
+
30
+
31
+ @click.command()
32
+ @args.query_id_required
33
+ @groups.json_yaml
34
+ @opts.verbosity
35
+ def datasets(
36
+ query_id: str,
37
+ json: bool,
38
+ yaml: bool,
39
+ verbosity: Verbosity,
40
+ ):
41
+ """
42
+ View datasets completeness per query.
43
+ """
44
+ esg = init_esgpull(verbosity)
45
+ with esg.ui.logging("datasets", onraise=Abort):
46
+ if not valid_name_tag(esg.graph, esg.ui, query_id, None):
47
+ raise Exit(1)
48
+ query = esg.graph.get(query_id)
49
+ datasets: defaultdict[str, DatasetCounter] = defaultdict(
50
+ DatasetCounter
51
+ )
52
+ for file in query.files:
53
+ datasets[file.dataset_id].total += 1
54
+ if file.status == FileStatus.Done:
55
+ datasets[file.dataset_id].done += 1
56
+ if json or yaml:
57
+ datasets_dict = {
58
+ dataset_id: counts.asdict()
59
+ for dataset_id, counts in datasets.items()
60
+ }
61
+ if json:
62
+ esg.ui.print(datasets_dict, json=True)
63
+ elif yaml:
64
+ esg.ui.print(datasets_dict, yaml=True)
65
+ else:
66
+ table = Table(box=MINIMAL_DOUBLE_HEAD, show_edge=False)
67
+ table.add_column("dataset_id", justify="right", style="bold blue")
68
+ table.add_column("done", justify="center")
69
+ table.add_column("total", justify="center")
70
+ table.add_column("complete", justify="center")
71
+ for dataset_id, counts in datasets.items():
72
+ table.add_row(
73
+ dataset_id,
74
+ str(counts.done),
75
+ str(counts.total),
76
+ str(counts.is_complete()),
77
+ )
78
+ esg.ui.print(table)
esgpull/cli/decorators.py CHANGED
@@ -59,6 +59,12 @@ class args:
59
59
  nargs=1,
60
60
  required=False,
61
61
  )
62
+ query_id_required: Dec = click.argument(
63
+ "query_id",
64
+ type=str,
65
+ nargs=1,
66
+ required=True,
67
+ )
62
68
  query_ids: Dec = click.argument(
63
69
  "query_ids",
64
70
  type=str,
@@ -143,6 +149,11 @@ class opts:
143
149
  type=str,
144
150
  default=None,
145
151
  )
152
+ no_default_query: Dec = click.option(
153
+ "--no-default-query",
154
+ is_flag=True,
155
+ default=False,
156
+ )
146
157
  query_file: Dec = click.option(
147
158
  "--query-file",
148
159
  "-q",
esgpull/cli/search.py CHANGED
@@ -18,17 +18,17 @@ from esgpull.tui import Verbosity
18
18
  @groups.query_def
19
19
  @groups.query_date
20
20
  @groups.display
21
+ @groups.json_yaml
22
+ @opts.detail
23
+ @opts.no_default_query
24
+ @opts.show
21
25
  @opts.dry_run
22
26
  @opts.file
23
27
  @opts.facets_hints
24
28
  @opts.hints
25
- @groups.json_yaml
26
- @opts.detail
27
- @opts.show
28
29
  @opts.yes
29
30
  @opts.record
30
31
  @opts.verbosity
31
- # @opts.selection_file
32
32
  def search(
33
33
  facets: list[str],
34
34
  ## query_def
@@ -38,6 +38,7 @@ def search(
38
38
  latest: str | None,
39
39
  replica: str | None,
40
40
  retracted: str | None,
41
+ ## query_date
41
42
  date_from: datetime | None,
42
43
  date_to: datetime | None,
43
44
  ## display
@@ -48,14 +49,14 @@ def search(
48
49
  json: bool,
49
50
  yaml: bool,
50
51
  ## ungrouped
51
- show: bool,
52
52
  detail: int | None,
53
+ no_default_query: bool,
54
+ show: bool,
53
55
  dry_run: bool,
54
56
  file: bool,
55
57
  facets_hints: bool,
56
58
  hints: list[str] | None,
57
59
  yes: bool,
58
- # selection_file: str | None,
59
60
  record: bool,
60
61
  verbosity: Verbosity,
61
62
  ) -> None:
@@ -64,7 +65,12 @@ def search(
64
65
 
65
66
  More info
66
67
  """
67
- esg = init_esgpull(verbosity, safe=False, record=record)
68
+ esg = init_esgpull(
69
+ verbosity,
70
+ safe=False,
71
+ record=record,
72
+ no_default_query=no_default_query,
73
+ )
68
74
  with esg.ui.logging("search", onraise=Abort):
69
75
  query = parse_query(
70
76
  facets=facets,
@@ -77,6 +83,7 @@ def search(
77
83
  )
78
84
  query.compute_sha()
79
85
  esg.graph.resolve_require(query)
86
+ query = esg.insert_default_query(query)[0]
80
87
  if show:
81
88
  if json:
82
89
  esg.ui.print(query.asdict(), json=True)
esgpull/cli/update.py CHANGED
@@ -165,20 +165,23 @@ def update(
165
165
  if choice == "y":
166
166
  legacy = esg.legacy_query
167
167
  has_legacy = legacy.state.persistent
168
- for file in new_files:
169
- file_db = esg.db.get(File, file.sha)
170
- if file_db is None:
171
- if esg.db.has_file_id(file):
172
- logger.error(
173
- "File id already exists in database, "
174
- "there might be an error with its checksum"
175
- f"\n{file}"
176
- )
177
- continue
178
- file.status = FileStatus.Queued
179
- file_db = esg.db.merge(file)
180
- elif has_legacy and legacy in file_db.queries:
181
- file_db.queries.remove(legacy)
182
- file_db.queries.append(qf.query)
183
- esg.db.add(file_db)
168
+ with esg.db.commit_context():
169
+ for file in esg.ui.track(
170
+ new_files,
171
+ description=qf.query.rich_name,
172
+ ):
173
+ file_db = esg.db.get(File, file.sha)
174
+ if file_db is None:
175
+ if esg.db.has_file_id(file):
176
+ logger.error(
177
+ "File id already exists in database, "
178
+ "there might be an error with its checksum"
179
+ f"\n{file}"
180
+ )
181
+ continue
182
+ file.status = FileStatus.Queued
183
+ esg.db.session.add(file)
184
+ elif has_legacy and legacy in file_db.queries:
185
+ esg.db.unlink(query=legacy, file=file_db)
186
+ esg.db.link(query=qf.query, file=file)
184
187
  esg.ui.raise_maybe_record(Exit(0))
esgpull/cli/utils.py CHANGED
@@ -1,8 +1,9 @@
1
1
  import sys
2
2
  from collections import OrderedDict
3
+ from collections.abc import MutableMapping, Sequence
3
4
  from enum import Enum
4
5
  from pathlib import Path
5
- from typing import Any, Literal, MutableMapping, Sequence
6
+ from typing import Any, Literal
6
7
 
7
8
  import click
8
9
  import yaml
@@ -29,6 +30,7 @@ def init_esgpull(
29
30
  safe: bool = True,
30
31
  record: bool = False,
31
32
  load_db: bool = True,
33
+ no_default_query: bool = False,
32
34
  ) -> Esgpull:
33
35
  TempUI.verbosity = Verbosity.Errors
34
36
  with TempUI.logging():
@@ -38,6 +40,8 @@ def init_esgpull(
38
40
  record=record,
39
41
  load_db=load_db,
40
42
  )
43
+ if no_default_query:
44
+ esg.config.api.default_query_id = ""
41
45
  if record:
42
46
  esg.ui.print(get_command())
43
47
  return esg
esgpull/config.py CHANGED
@@ -1,13 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from collections.abc import Container, Iterator, Mapping
4
+ from collections.abc import Iterator, Mapping
5
5
  from enum import Enum, auto
6
6
  from pathlib import Path
7
7
  from typing import Any, cast
8
8
 
9
9
  import tomlkit
10
- from attrs import Factory, define, field
10
+ from attrs import Factory, define, field, fields
11
+ from attrs import has as attrs_has
11
12
  from cattrs import Converter
12
13
  from cattrs.gen import make_dict_unstructure_fn, override
13
14
  from tomlkit import TOMLDocument
@@ -98,6 +99,7 @@ class Download:
98
99
  max_concurrent: int = 5
99
100
  disable_ssl: bool = False
100
101
  disable_checksum: bool = False
102
+ show_filename: bool = False
101
103
 
102
104
 
103
105
  @define
@@ -123,6 +125,7 @@ class API:
123
125
  max_concurrent: int = 5
124
126
  page_limit: int = 50
125
127
  default_options: DefaultOptions = Factory(DefaultOptions)
128
+ default_query_id: str = ""
126
129
 
127
130
 
128
131
  def fix_rename_search_api(doc: TOMLDocument) -> TOMLDocument:
@@ -298,15 +301,29 @@ class Config:
298
301
  doc.setdefault(part, {})
299
302
  doc = doc[part]
300
303
  obj = getattr(obj, part)
304
+ value_type = getattr(fields(type(obj)), last).type
301
305
  old_value = getattr(obj, last)
302
- if isinstance(old_value, str):
303
- ...
304
- elif isinstance(old_value, Container):
306
+ if attrs_has(value_type):
305
307
  raise KeyError(key)
306
- try:
307
- value = int(value)
308
- except ValueError:
308
+ elif value_type is str:
309
309
  ...
310
+ elif value_type is int:
311
+ try:
312
+ value = value_type(value)
313
+ except Exception:
314
+ ...
315
+ elif value_type is bool:
316
+ if isinstance(value, bool):
317
+ ...
318
+ elif isinstance(value, str):
319
+ if value.lower() in ["on", "true"]:
320
+ value = True
321
+ elif value.lower() in ["off", "false"]:
322
+ value = False
323
+ else:
324
+ raise ValueError(value)
325
+ else:
326
+ raise TypeError(value)
310
327
  setattr(obj, last, value)
311
328
  doc[last] = value
312
329
  return old_value
esgpull/constants.py CHANGED
@@ -1,4 +1,5 @@
1
1
  CONFIG_FILENAME = "config.toml"
2
+ INSTALLS_PATH_ENV = "ESGPULL_INSTALLS_PATH"
2
3
  ROOT_ENV = "ESGPULL_CURRENT"
3
4
 
4
5
  IDP = "/esgf-idp/openid/"
esgpull/database.py CHANGED
@@ -16,11 +16,10 @@ from sqlalchemy.orm import Session, joinedload, make_transient
16
16
 
17
17
  from esgpull import __file__
18
18
  from esgpull.config import Config
19
- from esgpull.models import File, Table, sql
19
+ from esgpull.models import File, Query, Table, sql
20
20
  from esgpull.version import __version__
21
21
 
22
22
  # from esgpull.exceptions import NoClauseError
23
- # from esgpull.models import Query
24
23
 
25
24
  T = TypeVar("T")
26
25
 
@@ -42,8 +41,16 @@ class Database:
42
41
  url = f"sqlite:///{config.paths.db / config.db.filename}"
43
42
  return Database(url, run_migrations=run_migrations)
44
43
 
44
+ def _setup_sqlite(self, conn, record):
45
+ cursor = conn.cursor()
46
+ cursor.execute("PRAGMA journal_mode = WAL;")
47
+ cursor.execute("PRAGMA synchronous = NORMAL;")
48
+ cursor.execute("PRAGMA cache_size = 20000;")
49
+ cursor.close()
50
+
45
51
  def __post_init__(self, run_migrations: bool) -> None:
46
52
  self._engine = sa.create_engine(self.url)
53
+ sa.event.listen(self._engine, "connect", self._setup_sqlite)
47
54
  self.session = Session(self._engine)
48
55
  if run_migrations:
49
56
  self._update()
@@ -59,10 +66,10 @@ class Database:
59
66
  opts = {"version_table": "version"}
60
67
  ctx = MigrationContext.configure(conn, opts=opts)
61
68
  self.version = ctx.get_current_revision()
62
- if self.version != head:
63
- alembic.command.upgrade(alembic_config, __version__)
69
+ if head is not None and self.version != head:
70
+ alembic.command.upgrade(alembic_config, head)
64
71
  self.version = head
65
- if self.version != __version__:
72
+ if "+dev" not in __version__ and self.version != __version__:
66
73
  alembic.command.revision(
67
74
  alembic_config,
68
75
  message="update tables",
@@ -80,6 +87,12 @@ class Database:
80
87
  self.session.rollback()
81
88
  raise
82
89
 
90
+ @contextmanager
91
+ def commit_context(self) -> Iterator[None]:
92
+ with self.safe:
93
+ yield
94
+ self.session.commit()
95
+
83
96
  def get(
84
97
  self,
85
98
  table: type[Table],
@@ -132,6 +145,12 @@ class Database:
132
145
  for item in items:
133
146
  make_transient(item)
134
147
 
148
+ def link(self, query: Query, file: File):
149
+ self.session.execute(sql.query_file.link(query, file))
150
+
151
+ def unlink(self, query: Query, file: File):
152
+ self.session.execute(sql.query_file.unlink(query, file))
153
+
135
154
  def __contains__(self, item: Table) -> bool:
136
155
  return self.scalars(sql.count(item))[0] > 0
137
156
 
esgpull/esgpull.py CHANGED
@@ -13,6 +13,7 @@ from rich.progress import (
13
13
  DownloadColumn,
14
14
  MofNCompleteColumn,
15
15
  Progress,
16
+ ProgressColumn,
16
17
  SpinnerColumn,
17
18
  TaskID,
18
19
  TextColumn,
@@ -28,6 +29,7 @@ from esgpull.exceptions import (
28
29
  DownloadCancelled,
29
30
  InvalidInstallPath,
30
31
  NoInstallPath,
32
+ UnknownDefaultQueryID,
31
33
  )
32
34
  from esgpull.fs import Filesystem
33
35
  from esgpull.graph import Graph
@@ -337,7 +339,10 @@ class Esgpull:
337
339
  data_node = (
338
340
  f"[blue]{task.fields['data_node']}[/]"
339
341
  )
340
- msg = " · ".join([sha, size, speed, data_node])
342
+ parts = [sha, size, speed, data_node]
343
+ if self.config.download.show_filename:
344
+ parts.append(task.fields["filename"])
345
+ msg = " · ".join(parts)
341
346
  logger.info(msg)
342
347
  live.console.print(msg)
343
348
  yield result
@@ -366,7 +371,7 @@ class Esgpull:
366
371
  MofNCompleteColumn(),
367
372
  TimeRemainingColumn(compact=True, elapsed_when_finished=True),
368
373
  )
369
- file_progress = self.ui.make_progress(
374
+ file_columns: list[str | ProgressColumn] = [
370
375
  TextColumn("[cyan][{task.id}] [b blue]{task.fields[sha]}"),
371
376
  "[progress.percentage]{task.percentage:>3.0f}%",
372
377
  BarColumn(),
@@ -376,6 +381,16 @@ class Esgpull:
376
381
  TransferSpeedColumn(),
377
382
  "·",
378
383
  TextColumn("[blue]{task.fields[data_node]}"),
384
+ ]
385
+ if self.config.download.show_filename:
386
+ file_columns.extend(
387
+ [
388
+ "·",
389
+ TextColumn("{task.fields[filename]}"),
390
+ ]
391
+ )
392
+ file_progress = self.ui.make_progress(
393
+ *file_columns,
379
394
  transient=True,
380
395
  )
381
396
  file_task_shas = {}
@@ -387,6 +402,7 @@ class Esgpull:
387
402
  visible=False,
388
403
  start=False,
389
404
  sha=short_sha(file.sha),
405
+ filename=file.filename,
390
406
  data_node=file.data_node,
391
407
  )
392
408
  callback = partial(file_progress.start_task, task_id)
@@ -445,3 +461,31 @@ class Esgpull:
445
461
  if use_db:
446
462
  self.db.add(*cancelled)
447
463
  return files, errors
464
+
465
+ def replace_queries(
466
+ self,
467
+ graph: Graph,
468
+ mapping: tuple[str | None, str],
469
+ ) -> None:
470
+ to_replace = [
471
+ q for q in graph.queries.values() if q.require == mapping[0]
472
+ ]
473
+ for query in to_replace:
474
+ new_query = query.clone(compute_sha=False)
475
+ new_query.require = mapping[1]
476
+ new_query.compute_sha()
477
+ graph.replace(query, new_query)
478
+ self.replace_queries(graph, (query.sha, new_query.sha))
479
+
480
+ def insert_default_query(self, *queries: Query) -> list[Query]:
481
+ if self.config.api.default_query_id == "":
482
+ return list(queries)
483
+ default_query_id = self.config.api.default_query_id
484
+ try:
485
+ default_query = self.graph.get(default_query_id)
486
+ except KeyError:
487
+ raise UnknownDefaultQueryID(default_query_id)
488
+ graph = Graph(None)
489
+ graph.add(*queries)
490
+ self.replace_queries(graph, (None, default_query.sha))
491
+ return list(graph.queries.values())
esgpull/exceptions.py CHANGED
@@ -109,8 +109,11 @@ class VirtualConfigError(EsgpullException):
109
109
  """
110
110
 
111
111
 
112
- class InstallException(EsgpullException):
113
- ...
112
+ class InstallException(EsgpullException): ...
113
+
114
+
115
+ class UnknownDefaultQueryID(EsgpullException):
116
+ msg = "{}"
114
117
 
115
118
 
116
119
  class UntrackableQuery(EsgpullException):
esgpull/install_config.py CHANGED
@@ -8,7 +8,7 @@ from pathlib import Path
8
8
  import platformdirs
9
9
  from typing_extensions import NotRequired, TypedDict
10
10
 
11
- from esgpull.constants import ROOT_ENV
11
+ from esgpull.constants import INSTALLS_PATH_ENV, ROOT_ENV
12
12
  from esgpull.exceptions import AlreadyInstalledName, AlreadyInstalledPath
13
13
 
14
14
 
@@ -45,7 +45,15 @@ class _InstallConfig:
45
45
  installs: list[Install]
46
46
 
47
47
  def __init__(self) -> None:
48
- user_config_dir = platformdirs.user_config_path("esgpull")
48
+ self.setup()
49
+
50
+ def setup(self, install_path: Path | None = None):
51
+ if install_path is not None:
52
+ user_config_dir = install_path
53
+ elif (env := os.environ.get(INSTALLS_PATH_ENV)) is not None:
54
+ user_config_dir = Path(env)
55
+ else:
56
+ user_config_dir = platformdirs.user_config_path("esgpull")
49
57
  self.path = user_config_dir / "installs.json"
50
58
  if self.path.is_file():
51
59
  with self.path.open() as f:
esgpull/migrations/env.py CHANGED
@@ -43,8 +43,10 @@ def run_migrations_offline() -> None:
43
43
  url=url,
44
44
  target_metadata=target_metadata,
45
45
  literal_binds=True,
46
+ dialect_name="sqlite",
46
47
  dialect_opts={"paramstyle": "named"},
47
48
  version_table="version",
49
+ render_as_batch=True,
48
50
  )
49
51
 
50
52
  with context.begin_transaction():
@@ -70,6 +72,7 @@ def run_migrations_online() -> None:
70
72
  connection=connection,
71
73
  target_metadata=target_metadata,
72
74
  version_table="version",
75
+ render_as_batch=True,
73
76
  )
74
77
 
75
78
  with context.begin_transaction():
@@ -0,0 +1,38 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.7.0
4
+ Revises: 0.6.5
5
+ Create Date: 2024-09-12 16:32:48.745570
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '0.7.0'
14
+ down_revision = '0.6.5'
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # ### commands auto generated by Alembic - please adjust! ###
21
+ with op.batch_alter_table('file', schema=None) as batch_op:
22
+ batch_op.alter_column('size',
23
+ existing_type=sa.INTEGER(),
24
+ type_=sa.BigInteger(),
25
+ existing_nullable=False)
26
+
27
+ # ### end Alembic commands ###
28
+
29
+
30
+ def downgrade() -> None:
31
+ # ### commands auto generated by Alembic - please adjust! ###
32
+ with op.batch_alter_table('file', schema=None) as batch_op:
33
+ batch_op.alter_column('size',
34
+ existing_type=sa.BigInteger(),
35
+ type_=sa.INTEGER(),
36
+ existing_nullable=False)
37
+
38
+ # ### end Alembic commands ###
@@ -0,0 +1,28 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.7.1
4
+ Revises: 0.7.0
5
+ Create Date: 2024-09-12 17:00:21.157499
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '0.7.1'
14
+ down_revision = '0.7.0'
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # ### commands auto generated by Alembic - please adjust! ###
21
+ pass
22
+ # ### end Alembic commands ###
23
+
24
+
25
+ def downgrade() -> None:
26
+ # ### commands auto generated by Alembic - please adjust! ###
27
+ pass
28
+ # ### end Alembic commands ###
esgpull/models/query.py CHANGED
@@ -235,7 +235,7 @@ class Query(Base):
235
235
  return nb_files is not None and nb_files > 0
236
236
 
237
237
  def files_count_size(self, *status: FileStatus) -> tuple[int, int]:
238
- stmt: sa.Select[tuple[int, int | None]] = (
238
+ stmt: sa.Select[tuple[int, int]] = (
239
239
  sa.select(sa.func.count("*"), sa.func.sum(File.size))
240
240
  .join_from(query_file_proxy, File)
241
241
  .where(query_file_proxy.c.query_sha == self.sha)
@@ -71,6 +71,10 @@ class Selection(Base):
71
71
 
72
72
  setattr(cls, name, property(getter, setter))
73
73
 
74
+ @classmethod
75
+ def reset(cls) -> None:
76
+ cls.configure(*DefaultFacets, *BaseFacets, replace=True)
77
+
74
78
  @classmethod
75
79
  def configure(cls, *names: str, replace: bool = True) -> None:
76
80
  nameset = set(names) | {f"!{name}" for name in names}
@@ -195,7 +199,8 @@ BaseFacets = [
195
199
  "member_id",
196
200
  "cmor_table",
197
201
  "grid_label",
202
+ "nominal_resolution",
198
203
  ]
199
204
 
200
205
 
201
- Selection.configure(*DefaultFacets, *BaseFacets, replace=True)
206
+ Selection.reset()
esgpull/models/sql.py CHANGED
@@ -217,9 +217,7 @@ class selection:
217
217
  @functools.cache
218
218
  def orphans() -> sa.Select[tuple[Selection]]:
219
219
  return (
220
- sa.select(Selection)
221
- .outerjoin(Query)
222
- .where(Query.sha == None) # noqa
220
+ sa.select(Selection).outerjoin(Query).where(Query.sha == None) # noqa
223
221
  )
224
222
 
225
223
 
@@ -256,3 +254,19 @@ class synda_file:
256
254
  @staticmethod
257
255
  def with_ids(*ids: int) -> sa.Select[tuple[SyndaFile]]:
258
256
  return sa.select(SyndaFile).where(SyndaFile.file_id.in_(ids))
257
+
258
+
259
+ class query_file:
260
+ @staticmethod
261
+ def link(query: Query, file: File) -> sa.Insert:
262
+ return sa.insert(query_file_proxy).values(
263
+ query_sha=query.sha, file_sha=file.sha
264
+ )
265
+
266
+ @staticmethod
267
+ def unlink(query: Query, file: File) -> sa.Delete:
268
+ return (
269
+ sa.delete(query_file_proxy)
270
+ .where(query_file_proxy.c.query_sha == query.sha)
271
+ .where(query_file_proxy.c.file_sha == file.sha)
272
+ )
esgpull/tui.py CHANGED
@@ -71,8 +71,7 @@ class DummyLive:
71
71
  def __enter__(self) -> DummyLive:
72
72
  return self
73
73
 
74
- def __exit__(self, *args):
75
- ...
74
+ def __exit__(self, *args): ...
76
75
 
77
76
  @property
78
77
  def console(self) -> DummyConsole:
@@ -259,9 +258,9 @@ class UI:
259
258
  # use _console to avoid recording the progress bar
260
259
  return Live(renderables, console=_console)
261
260
 
262
- def track(self, iterable: Iterable[T]) -> Iterable[T]:
261
+ def track(self, iterable: Iterable[T], **kwargs) -> Iterable[T]:
263
262
  # use _console to avoid recording the progress bar
264
- return track(iterable, console=_console)
263
+ return track(iterable, console=_console, **kwargs)
265
264
 
266
265
  def make_progress(
267
266
  self,
esgpull/version.py CHANGED
@@ -1 +1,3 @@
1
- __version__ = "0.6.5"
1
+ import importlib.metadata
2
+
3
+ __version__ = importlib.metadata.version(__package__ or __name__)
@@ -1,41 +1,42 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: esgpull
3
- Version: 0.6.5
3
+ Version: 0.7.1
4
4
  Summary: ESGF data discovery, download, replication tool
5
- Author-Email: Sven Rodriguez <srodriguez@ipsl.fr>
5
+ Project-URL: Repository, https://github.com/ESGF/esgf-download
6
+ Project-URL: Documentation, https://esgf.github.io/esgf-download/
7
+ Author-email: Sven Rodriguez <srodriguez@ipsl.fr>
6
8
  License: BSD-3-Clause
9
+ License-File: LICENSE
7
10
  Classifier: License :: OSI Approved :: BSD License
8
11
  Classifier: Programming Language :: Python :: 3
9
12
  Classifier: Programming Language :: Python :: 3.10
10
13
  Classifier: Programming Language :: Python :: 3.11
11
14
  Classifier: Programming Language :: Python :: 3.12
12
- Project-URL: Repository, https://github.com/ESGF/esgf-download
13
- Project-URL: Documentation, https://esgf.github.io/esgf-download/
14
15
  Requires-Python: >=3.10
15
- Requires-Dist: MyProxyClient>=2.1.0
16
16
  Requires-Dist: aiofiles>=22.1.0
17
+ Requires-Dist: aiostream>=0.4.5
17
18
  Requires-Dist: alembic>=1.8.1
18
- Requires-Dist: click>=8.1.3
19
+ Requires-Dist: attrs>=22.1.0
20
+ Requires-Dist: cattrs>=22.2.0
19
21
  Requires-Dist: click-params>=0.4.0
22
+ Requires-Dist: click>=8.1.3
20
23
  Requires-Dist: httpx>=0.23.0
24
+ Requires-Dist: myproxyclient>=2.1.0
21
25
  Requires-Dist: nest-asyncio>=1.5.6
22
- Requires-Dist: pyOpenSSL>=22.1.0
26
+ Requires-Dist: platformdirs>=2.6.2
27
+ Requires-Dist: pyopenssl>=22.1.0
28
+ Requires-Dist: pyparsing>=3.0.9
23
29
  Requires-Dist: pyyaml>=6.0
24
- Requires-Dist: tomlkit>=0.11.5
25
30
  Requires-Dist: rich>=12.6.0
26
- Requires-Dist: sqlalchemy>=2.0.0b2
27
31
  Requires-Dist: setuptools>=65.4.1
28
- Requires-Dist: aiostream>=0.4.5
29
- Requires-Dist: attrs>=22.1.0
30
- Requires-Dist: cattrs>=22.2.0
31
- Requires-Dist: platformdirs>=2.6.2
32
- Requires-Dist: pyparsing>=3.0.9
32
+ Requires-Dist: sqlalchemy>=2.0.0b2
33
+ Requires-Dist: tomlkit>=0.11.5
33
34
  Description-Content-Type: text/markdown
34
35
 
35
- [![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm.fming.dev)
36
-
37
36
  # esgpull - ESGF data management utility
38
37
 
38
+ [![Rye](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/rye/main/artwork/badge.json)](https://rye.astral.sh)
39
+
39
40
  `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.
40
41
 
41
42
  ```py
@@ -1,15 +1,28 @@
1
- esgpull-0.6.5.dist-info/METADATA,sha256=vycdqEO7yU4ZlM-A_Db2By-LyaHl-QcqtGD3tnOd1Tc,2982
2
- esgpull-0.6.5.dist-info/WHEEL,sha256=mbxFTmdEUhG7evcdMkR3aBt9SWcoFBJ4CDwnfguNegA,90
3
- esgpull-0.6.5.dist-info/entry_points.txt,sha256=nfKsESeZyCiVD6dDCTCturf-vRnI-6GCuwqv9jxXCq8,46
4
- esgpull-0.6.5.dist-info/licenses/LICENSE,sha256=lUqGPGWDHHxjkUDuYgjLLY2XQXXn_EHU7fnrQWHGugc,1540
5
1
  esgpull/__init__.py,sha256=XItFDIMNmFUNNcKtUgXdfmGwUIWt4AAv0a4mZkfj5P8,240
6
2
  esgpull/auth.py,sha256=QZ-l1ySLMP0fvuwYHRLv9FZYp1gqfju_eGaTMDByUxw,5205
7
- esgpull/cli/__init__.py,sha256=boqxa_ku266IcZ6GY-7fVA2T9GLku1e01TXe3aqZmR4,1630
8
- esgpull/cli/add.py,sha256=c-tYR8h5OlSiavSiDzpUa5gIhBPzIPV9P7M2UvHOptc,3111
3
+ esgpull/config.py,sha256=3E3jRGem09M_Hm1_uqWHu4dy7gzgShiUecZp-6XC9E4,12149
4
+ esgpull/constants.py,sha256=fQE6vE2EU8V5m2lRNYBeMkL3u1_Em343Zs8u7i6ZRlo,1118
5
+ esgpull/context.py,sha256=oSymljW0hZLGhCJxo7lh96Eq3SI_KOT_jFMPHckIrm0,23203
6
+ esgpull/database.py,sha256=TDo5xIXWyMEPFpaeygVhLZkeDAwYlkBG9UWhmL6ntfo,6144
7
+ esgpull/download.py,sha256=3YcGLrffzAmuiV49tYsW-7PuVYcSFtbsZnH2Q26khm0,5491
8
+ esgpull/esgpull.py,sha256=R7lnlyOo92FB_Qem5ST4hNA5Iwn9PHYsvCpKxqRX-qM,17717
9
+ esgpull/exceptions.py,sha256=wgLyhyIITdusNucPjnnURJX1Jxv1VVIr9PzJV_77qhg,3275
10
+ esgpull/fs.py,sha256=O52QD7DVJImYYf9wWECMNWYrGt8EcHQ3z9TzroK6VAY,7623
11
+ esgpull/graph.py,sha256=CeaRcIAWMpVAJ-o6v6b3ffzy-ter-8HZ3tmxoN-Z-FM,15925
12
+ esgpull/install_config.py,sha256=hzYpcHMtPMOK9fYcvVH-Hn_8zYsbs3yXlYgMumXo1zE,5598
13
+ esgpull/processor.py,sha256=noenWExukON2P4klMN8Vt1ALzApmvc5gpsFqd7J5g78,5446
14
+ esgpull/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ esgpull/result.py,sha256=f64l9gPFpFWgctmHVYrNWJvuXXB1sxpxXzJEIssLXxc,1020
16
+ esgpull/tui.py,sha256=GTk86277Vv4msP2YJGpFWMQqxFhW_WSvxViU9nDFeAU,10407
17
+ esgpull/utils.py,sha256=eKipKCqj15dzCAqs1o7x5O1XClWPmDEM-qSvNwGZmXY,1233
18
+ esgpull/version.py,sha256=IHT4mKrIr8eV-C3HtmIVD85iGVH25n2ohoff31kaJ1A,93
19
+ esgpull/cli/__init__.py,sha256=mvxiV7bRJlJ3uAfxr6hHDcuQDvy70TGkKyY5W4wE4SY,1686
20
+ esgpull/cli/add.py,sha256=PhTe-jY7CNWYVtD1qYptGgIwYat2RYUfV-RHG_-SWbU,3270
9
21
  esgpull/cli/autoremove.py,sha256=g76_qnc3q84zSO7W0JsbWtGN4AfWBXTkQJO6gPCs2Pw,1336
10
22
  esgpull/cli/config.py,sha256=JbSa2NvXxccbOpEyvJd4vSlBnwb_NClx5sCJp_WY7-k,4048
11
23
  esgpull/cli/convert.py,sha256=Nonu49aijY3OFq2MNCqs7EQ08NXFiT0ehvNxoGp4NCE,9847
12
- esgpull/cli/decorators.py,sha256=a8lhbhe9ppaNj_kSwPfdeVkq9pEwchiGb52ZbG8EYWI,6475
24
+ esgpull/cli/datasets.py,sha256=wO_gZUhvWy027q2jX2hAW25wJkIGrYHHCYuGAbcTmG8,2374
25
+ esgpull/cli/decorators.py,sha256=X5Ja6HlB_AgpTohpbF2EjtT5sA0IdPxX20g-j1K7raM,6727
13
26
  esgpull/cli/download.py,sha256=3_Fm8JJYBWKs63oQ1OLaHJCv9ccbeY2gIW8SDasaYNE,2356
14
27
  esgpull/cli/facet.py,sha256=V1u-DxNkhswwSt0qpXvuHrCI_tE8jAJGEe6_fMhYbaM,584
15
28
  esgpull/cli/get.py,sha256=2WXL01Ri0P_2Rf1xrp9bnsrrxir6whxkAC0SnohjFpg,678
@@ -17,25 +30,15 @@ esgpull/cli/install.py,sha256=fd8nKwIFvOivgn_gOGn7XIk1BB9LXnhQB47KuIIy5AU,2880
17
30
  esgpull/cli/login.py,sha256=FZ63SsB4fCDixwf7gMUR697Dk89W1OvpgeadKE4IqEU,2153
18
31
  esgpull/cli/remove.py,sha256=p26hPhsgAgmAF4IsvjAI3jtzlIUq3k8Rxb0GTKYoEQM,2517
19
32
  esgpull/cli/retry.py,sha256=UVpAjW_N7l6RTJ-T5qXojwcXPzzjT7sDKb_wBdvavrg,1310
20
- esgpull/cli/search.py,sha256=6-c-W78b45-wo9GIVIzq8fYkqIY5NE7QvILsUb3M8Uk,6232
33
+ esgpull/cli/search.py,sha256=c2OI0Xvw6h2zBnC8msjxidKJZ0KKUuWJK9FuAmQ15ZU,6369
21
34
  esgpull/cli/self.py,sha256=7nEEsK5W_Pth8IOSmvJHRlfwPPgXldhHAQK9yqf01S8,7932
22
35
  esgpull/cli/show.py,sha256=4O1TvH31x9Fg-SI0a8dtblMZuJHJ30eBXOV9bwEl9Lk,1897
23
36
  esgpull/cli/status.py,sha256=jgGBbmvcCjw21QTDNXdKAUo664p6junRg5kMwQ7My2I,2470
24
37
  esgpull/cli/track.py,sha256=Q9ZvvV5FFGzp6wQZflAd_OFmqhAWgl1JFBad2dCbEF0,3089
25
- esgpull/cli/update.py,sha256=HvdVkF-ZmJLAL29D757oDAolwXvP_ehOtD0TnGkmZ24,6828
26
- esgpull/cli/utils.py,sha256=0SAJpeE13pHxt_5lfKn9tnjutUxeXqQ8xZWyJaeW-sU,7121
27
- esgpull/config.py,sha256=WelBOrkaTPldk4UasFJvku5DRh7062kO6CPHNWSV3Wc,11529
28
- esgpull/constants.py,sha256=3eq5Yow519p3YLAKjNdwOt1Ooei62FE9HykqCiyhZYE,1074
29
- esgpull/context.py,sha256=oSymljW0hZLGhCJxo7lh96Eq3SI_KOT_jFMPHckIrm0,23203
30
- esgpull/database.py,sha256=4JONNJpt7JzeohgBtRO2hpQUUFtV_wTncU20HjWPGVA,5431
31
- esgpull/download.py,sha256=3YcGLrffzAmuiV49tYsW-7PuVYcSFtbsZnH2Q26khm0,5491
32
- esgpull/esgpull.py,sha256=nmRZA72erSav2EfJJUNljkZzVKogKPRWuYHyNiCB7NI,16098
33
- esgpull/exceptions.py,sha256=uK5ApBZJPwLlR7hYpvwQdApVtugn7_-N-MMmR8HpEgE,3215
34
- esgpull/fs.py,sha256=O52QD7DVJImYYf9wWECMNWYrGt8EcHQ3z9TzroK6VAY,7623
35
- esgpull/graph.py,sha256=CeaRcIAWMpVAJ-o6v6b3ffzy-ter-8HZ3tmxoN-Z-FM,15925
36
- esgpull/install_config.py,sha256=mGZLaYEDaXlmX9obl_9d7jTyweT8UcP3XSo5RZRkkpk,5295
38
+ esgpull/cli/update.py,sha256=x_R4x7hdUp5qowcFUibIMozwWTlJybuZMY_EXWhj_kI,7024
39
+ esgpull/cli/utils.py,sha256=mMCVUuHEG1g3KPGq_Jx1p1AfJCVFwxR23GsQRtV88xA,7262
37
40
  esgpull/migrations/README,sha256=heMzebYwlGhnE8_4CWJ4LS74WoEZjBy-S-mIJRxAEKI,39
38
- esgpull/migrations/env.py,sha256=qv0RBnOOTx-f8rnemoTIm48JasLxNzbczq5wUBRQchg,2190
41
+ esgpull/migrations/env.py,sha256=am2HhFrlIZNlXCaA5Ye7yKbIJ2MRSO5UFmUwB8l9fyE,2285
39
42
  esgpull/migrations/script.py.mako,sha256=HNlf26BI1xvQKjiUojnj15BPrVUfVVr81IOgliJf83c,510
40
43
  esgpull/migrations/versions/0.3.0_update_tables.py,sha256=r0rGX_cQSNX74JM5tZ8iqgstsAdfJH5rO3gGPkOxpok,5540
41
44
  esgpull/migrations/versions/0.3.1_update_tables.py,sha256=Fm7o-ZVvuyFcyReXXLhXQJQ-feCrbY2c0wnKwYnj_5k,493
@@ -59,22 +62,22 @@ esgpull/migrations/versions/0.6.2_update_tables.py,sha256=LFUVgDhxSll9FpvNFm-idD
59
62
  esgpull/migrations/versions/0.6.3_update_tables.py,sha256=ut4M0b90OWVB9pKSGTJ4Bl5cucXFJzRD4-WA_ego0WY,493
60
63
  esgpull/migrations/versions/0.6.4_update_tables.py,sha256=PFALlSAjCFOqqoQgjq2TF0HjGc22fVdStMK2NNNa3Yk,493
61
64
  esgpull/migrations/versions/0.6.5_update_tables.py,sha256=NYO8vnS4h_g4Co4M1CJibB2WYLqmVAy6ZaApFk-do3c,493
65
+ esgpull/migrations/versions/0.7.0_update_tables.py,sha256=aCmR7q-1V49JIfvFR-1iVskTwR3J8O1iKysgF0oJZ4k,971
66
+ esgpull/migrations/versions/0.7.1_update_tables.py,sha256=f_PakdA0ZKmekcWWDC86u5PlUrF_Kl2xzkNV3Am9sd0,541
62
67
  esgpull/models/__init__.py,sha256=rfa1yGLVDahFrnhq_8TPGzr7_oeBOFKNVD9EF0slUtY,725
63
68
  esgpull/models/base.py,sha256=3nbR2lYMHWfovWz4EiAJ2bIvKpMadRvYZDdMRQDvN7M,1237
64
69
  esgpull/models/dataset.py,sha256=1fOIVYIWKK5BivqvBpjfxrNpy9VfUHZng9Yc6ipPK1Q,905
65
70
  esgpull/models/facet.py,sha256=COMgFjsxQcgb4uGMLy5JDRFWeMSHO-QDdG-cWpwvYqQ,459
66
71
  esgpull/models/file.py,sha256=-8PPYtq7BWp-O_QtCDbkLdhTGTPhI1F1nodQacMnYGA,1517
67
72
  esgpull/models/options.py,sha256=yzADDeDgtMymFdKG31JMl8sds-LnWDmjtaCpQ6EUKQ0,4745
68
- esgpull/models/query.py,sha256=D-D0q1oz1L6FoshXgJDflg6iyR9W85dxnP8vKTRvdww,16719
69
- esgpull/models/selection.py,sha256=5LlKhCM7FK3WieGuI2xLw5b_GvSlAGrmu67NtgIDd6c,5803
70
- esgpull/models/sql.py,sha256=T2whCvz_MFb0tY5Ij5xcFlZ4TxrS0JT1rRU_gd2qVsg,7041
73
+ esgpull/models/query.py,sha256=sMtwdnTRcPzK_KCIXXN9YEQ8TdaGW2z3m6RDeyQJU94,16712
74
+ esgpull/models/selection.py,sha256=QCX_2eoCJcYB1ULll-J7UV5lCpkweis92FANQY8pH0o,5895
75
+ esgpull/models/sql.py,sha256=Wlw9cGJ_buv51qC0grV8HvprEwoG92e2zagxkQXKosk,7481
71
76
  esgpull/models/synda_file.py,sha256=6o5unPhzVJGnbpA2MxcS0r-hrBwocHYVnLrqjSGtmuk,2387
72
77
  esgpull/models/tag.py,sha256=5CQDB9rAeCqog63ec9LPFN46HOFNkHPy-maY4gkBQ3E,461
73
78
  esgpull/models/utils.py,sha256=exwlIlIKYjhhfUE82w1kU_HeSQOSY97PTvpkhW0udMA,1631
74
- esgpull/processor.py,sha256=noenWExukON2P4klMN8Vt1ALzApmvc5gpsFqd7J5g78,5446
75
- esgpull/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
- esgpull/result.py,sha256=f64l9gPFpFWgctmHVYrNWJvuXXB1sxpxXzJEIssLXxc,1020
77
- esgpull/tui.py,sha256=MG3VULh8uTrRspw_kFuhunmHqscO9Xd7PS7MG9WyvMY,10395
78
- esgpull/utils.py,sha256=eKipKCqj15dzCAqs1o7x5O1XClWPmDEM-qSvNwGZmXY,1233
79
- esgpull/version.py,sha256=KDgkBrBsBSUzbLgrOZ89YsNN06fU4j5bmcuEwo6q5pg,22
80
- esgpull-0.6.5.dist-info/RECORD,,
79
+ esgpull-0.7.1.dist-info/METADATA,sha256=1igfjfbOvh_U8yLbJbv027Q2t4Ft5NteUGnOSleOMPA,3052
80
+ esgpull-0.7.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
81
+ esgpull-0.7.1.dist-info/entry_points.txt,sha256=vyh7HvFrCp4iyMrTkDoSF3weaYrlNj2OJe0Fq5q4QB4,45
82
+ esgpull-0.7.1.dist-info/licenses/LICENSE,sha256=lUqGPGWDHHxjkUDuYgjLLY2XQXXn_EHU7fnrQWHGugc,1540
83
+ esgpull-0.7.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: pdm-backend (2.3.2)
2
+ Generator: hatchling 1.25.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,3 +1,2 @@
1
1
  [console_scripts]
2
2
  esgpull = esgpull.cli:main
3
-