esgpull 0.9.3__tar.gz → 0.9.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. {esgpull-0.9.3 → esgpull-0.9.5}/.github/workflows/ci.yml +4 -0
  2. {esgpull-0.9.3 → esgpull-0.9.5}/PKG-INFO +3 -3
  3. {esgpull-0.9.3 → esgpull-0.9.5}/README.md +1 -1
  4. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/context.py +45 -12
  5. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/esgpull.py +10 -5
  6. esgpull-0.9.5/esgpull/migrations/versions/0.9.4_update_tables.py +28 -0
  7. esgpull-0.9.5/esgpull/migrations/versions/0.9.5_update_tables.py +28 -0
  8. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/tui.py +11 -0
  9. {esgpull-0.9.3 → esgpull-0.9.5}/pyproject.toml +2 -2
  10. {esgpull-0.9.3 → esgpull-0.9.5}/requirements-dev.lock +36 -36
  11. {esgpull-0.9.3 → esgpull-0.9.5}/requirements.lock +17 -19
  12. {esgpull-0.9.3 → esgpull-0.9.5}/tests/cli/test_dataset.py +1 -1
  13. esgpull-0.9.5/tests/cli/test_download.py +50 -0
  14. {esgpull-0.9.3 → esgpull-0.9.5}/tests/cli/test_remove.py +2 -2
  15. {esgpull-0.9.3 → esgpull-0.9.5}/tests/cli/test_update.py +1 -1
  16. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_context.py +189 -1
  17. {esgpull-0.9.3 → esgpull-0.9.5}/uv.lock +2 -7
  18. {esgpull-0.9.3 → esgpull-0.9.5}/.github/workflows/doc.yml +0 -0
  19. {esgpull-0.9.3 → esgpull-0.9.5}/.github/workflows/pypi-publish.yml +0 -0
  20. {esgpull-0.9.3 → esgpull-0.9.5}/.gitignore +0 -0
  21. {esgpull-0.9.3 → esgpull-0.9.5}/.pre-commit-config.yaml +0 -0
  22. {esgpull-0.9.3 → esgpull-0.9.5}/CITATION.cff +0 -0
  23. {esgpull-0.9.3 → esgpull-0.9.5}/LICENSE +0 -0
  24. {esgpull-0.9.3 → esgpull-0.9.5}/alembic.ini +0 -0
  25. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/configuration.md +0 -0
  26. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/download.md +0 -0
  27. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/glossary.md +0 -0
  28. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/download_1.svg +0 -0
  29. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/download_2.svg +0 -0
  30. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/download_3.svg +0 -0
  31. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/download_4.svg +0 -0
  32. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/download_5.svg +0 -0
  33. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/download_6.svg +0 -0
  34. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/intro_1.svg +0 -0
  35. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/intro_2.svg +0 -0
  36. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/intro_3.svg +0 -0
  37. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/intro_4.svg +0 -0
  38. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/intro_5.svg +0 -0
  39. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/intro_6.svg +0 -0
  40. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/quickstart_1.svg +0 -0
  41. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/search_1.svg +0 -0
  42. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/search_2.svg +0 -0
  43. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/search_3.svg +0 -0
  44. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/search_4.svg +0 -0
  45. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/search_5.svg +0 -0
  46. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/search_6.svg +0 -0
  47. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/search_7.svg +0 -0
  48. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/images/search_ignore.svg +0 -0
  49. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/index.md +0 -0
  50. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/installation.md +0 -0
  51. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/plugins.md +0 -0
  52. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/queries.md +0 -0
  53. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/quickstart.md +0 -0
  54. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/search.md +0 -0
  55. {esgpull-0.9.3 → esgpull-0.9.5}/docs/docs/stylesheets/extra.css +0 -0
  56. {esgpull-0.9.3 → esgpull-0.9.5}/docs/includes/abbreviations.md +0 -0
  57. {esgpull-0.9.3 → esgpull-0.9.5}/docs/mkdocs.yml +0 -0
  58. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/__init__.py +0 -0
  59. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/__init__.py +0 -0
  60. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/add.py +0 -0
  61. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/autoremove.py +0 -0
  62. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/config.py +0 -0
  63. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/convert.py +0 -0
  64. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/decorators.py +0 -0
  65. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/download.py +0 -0
  66. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/facet.py +0 -0
  67. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/get.py +0 -0
  68. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/index_nodes.py +0 -0
  69. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/install.py +0 -0
  70. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/plugins.py +0 -0
  71. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/remove.py +0 -0
  72. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/retry.py +0 -0
  73. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/search.py +0 -0
  74. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/self.py +0 -0
  75. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/show.py +0 -0
  76. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/status.py +0 -0
  77. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/track.py +0 -0
  78. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/update.py +0 -0
  79. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/cli/utils.py +0 -0
  80. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/config.py +0 -0
  81. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/constants.py +0 -0
  82. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/database.py +0 -0
  83. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/download.py +0 -0
  84. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/exceptions.py +0 -0
  85. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/fs.py +0 -0
  86. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/graph.py +0 -0
  87. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/install_config.py +0 -0
  88. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/README +0 -0
  89. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/env.py +0 -0
  90. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/script.py.mako +0 -0
  91. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.3.0_update_tables.py +0 -0
  92. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.3.1_update_tables.py +0 -0
  93. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.3.2_update_tables.py +0 -0
  94. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.3.3_update_tables.py +0 -0
  95. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.3.4_update_tables.py +0 -0
  96. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.3.5_update_tables.py +0 -0
  97. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.3.6_update_tables.py +0 -0
  98. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.3.7_update_tables.py +0 -0
  99. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.3.8_update_tables.py +0 -0
  100. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.4.0_update_tables.py +0 -0
  101. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.5.0_update_tables.py +0 -0
  102. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.5.1_update_tables.py +0 -0
  103. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.5.2_update_tables.py +0 -0
  104. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.5.3_update_tables.py +0 -0
  105. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.5.4_update_tables.py +0 -0
  106. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.5.5_update_tables.py +0 -0
  107. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.6.0_update_tables.py +0 -0
  108. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.6.1_update_tables.py +0 -0
  109. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.6.2_update_tables.py +0 -0
  110. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.6.3_update_tables.py +0 -0
  111. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.6.4_update_tables.py +0 -0
  112. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.6.5_update_tables.py +0 -0
  113. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.7.0_update_tables.py +0 -0
  114. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.7.1_update_tables.py +0 -0
  115. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.7.2_update_tables.py +0 -0
  116. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.7.3_update_tables.py +0 -0
  117. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.8.0_update_tables.py +0 -0
  118. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.9.0_update_tables.py +0 -0
  119. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.9.1_update_tables.py +0 -0
  120. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.9.2_update_tables.py +0 -0
  121. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/0.9.3_update_tables.py +0 -0
  122. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/14c72daea083_query_add_column_updated_at.py +0 -0
  123. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/c7c8541fa741_query_add_column_added_at.py +0 -0
  124. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/d14f179e553c_file_add_composite_index_dataset_id_.py +0 -0
  125. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/migrations/versions/e7edab5d4e4b_add_dataset_tracking.py +0 -0
  126. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/models/__init__.py +0 -0
  127. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/models/base.py +0 -0
  128. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/models/dataset.py +0 -0
  129. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/models/facet.py +0 -0
  130. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/models/file.py +0 -0
  131. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/models/options.py +0 -0
  132. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/models/query.py +0 -0
  133. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/models/selection.py +0 -0
  134. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/models/sql.py +0 -0
  135. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/models/synda_file.py +0 -0
  136. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/models/tag.py +0 -0
  137. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/models/utils.py +0 -0
  138. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/plugin.py +0 -0
  139. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/processor.py +0 -0
  140. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/py.typed +0 -0
  141. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/result.py +0 -0
  142. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/utils.py +0 -0
  143. {esgpull-0.9.3 → esgpull-0.9.5}/esgpull/version.py +0 -0
  144. {esgpull-0.9.3 → esgpull-0.9.5}/pdm.lock +0 -0
  145. {esgpull-0.9.3 → esgpull-0.9.5}/tests/__init__.py +0 -0
  146. {esgpull-0.9.3 → esgpull-0.9.5}/tests/assets/error_plugin.py +0 -0
  147. {esgpull-0.9.3 → esgpull-0.9.5}/tests/assets/incompatible_plugin.py +0 -0
  148. {esgpull-0.9.3 → esgpull-0.9.5}/tests/assets/priority_test_plugin.py +0 -0
  149. {esgpull-0.9.3 → esgpull-0.9.5}/tests/assets/sample_plugin.py +0 -0
  150. {esgpull-0.9.3 → esgpull-0.9.5}/tests/cli/__init__.py +0 -0
  151. {esgpull-0.9.3 → esgpull-0.9.5}/tests/cli/test_parse.py +0 -0
  152. {esgpull-0.9.3 → esgpull-0.9.5}/tests/cli/test_plugin_cli.py +0 -0
  153. {esgpull-0.9.3 → esgpull-0.9.5}/tests/cli/test_show_dates.py +0 -0
  154. {esgpull-0.9.3 → esgpull-0.9.5}/tests/conftest.py +0 -0
  155. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_config.py +0 -0
  156. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_dataset.py +0 -0
  157. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_db.py +0 -0
  158. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_esgpull.py +0 -0
  159. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_fs.py +0 -0
  160. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_graph.py +0 -0
  161. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_plugin.py +0 -0
  162. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_processor.py +0 -0
  163. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_query.py +0 -0
  164. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_selection.py +0 -0
  165. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_synda.py +0 -0
  166. {esgpull-0.9.3 → esgpull-0.9.5}/tests/test_utils.py +0 -0
  167. {esgpull-0.9.3 → esgpull-0.9.5}/tests/utils.py +0 -0
@@ -9,6 +9,8 @@ on:
9
9
  - esgpull/**
10
10
  - tests/**
11
11
  - .github/workflows/ci.yml
12
+ - pyproject.toml
13
+ - uv.lock
12
14
  pull_request:
13
15
  branches:
14
16
  - main
@@ -17,6 +19,8 @@ on:
17
19
  - esgpull/**
18
20
  - tests/**
19
21
  - .github/workflows/ci.yml
22
+ - pyproject.toml
23
+ - uv.lock
20
24
 
21
25
  jobs:
22
26
  tests:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esgpull
3
- Version: 0.9.3
3
+ Version: 0.9.5
4
4
  Summary: ESGF data discovery, download, replication tool
5
5
  Project-URL: Repository, https://github.com/ESGF/esgf-download
6
6
  Project-URL: Documentation, https://esgf.github.io/esgf-download/
@@ -31,13 +31,13 @@ Requires-Dist: pyparsing>=3.0.9
31
31
  Requires-Dist: pyyaml>=6.0
32
32
  Requires-Dist: rich>=12.6.0
33
33
  Requires-Dist: setuptools>=65.4.1
34
- Requires-Dist: sqlalchemy>=2.0.0b2
34
+ Requires-Dist: sqlalchemy<2.1,>=2.0.0b2
35
35
  Requires-Dist: tomlkit>=0.11.5
36
36
  Description-Content-Type: text/markdown
37
37
 
38
38
  # esgpull - ESGF data management utility
39
39
 
40
- [![Rye](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/rye/main/artwork/badge.json)](https://rye.astral.sh)
40
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
41
41
 
42
42
  `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.
43
43
 
@@ -1,6 +1,6 @@
1
1
  # esgpull - ESGF data management utility
2
2
 
3
- [![Rye](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/rye/main/artwork/badge.json)](https://rye.astral.sh)
3
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
4
4
 
5
5
  `esgpull` is a tool that simplifies usage of the [ESGF Search API](https://esgf.github.io/esg-search/ESGF_Search_RESTful_API.html) for data discovery, and manages procedures related to downloading and storing files from ESGF.
6
6
 
@@ -134,18 +134,46 @@ class Result:
134
134
  solr_terms: list[str] = []
135
135
  for name, values in self.query.selection.items():
136
136
  if index.is_bridge():
137
- value_term = " ".join(quote_str(v) for v in values)
137
+ if name == "query":
138
+ solr_terms.append(" ".join(values))
139
+ elif name.startswith("!"):
140
+ value_term = " ".join(quote_str(v) for v in values)
141
+ if len(values) > 1:
142
+ value_term = f"({value_term})"
143
+ solr_terms.append(f"NOT ({name[1:]}:{value_term})")
144
+ else:
145
+ has_wildcard = any("*" in v for v in values)
146
+ no_wildcard = any("*" not in v for v in values)
147
+
148
+ if has_wildcard and no_wildcard:
149
+ logger.warning(
150
+ (
151
+ f"Facet {name} has mixed wildcard/non-wildcard values. "
152
+ "Non-wildcard values may match partially."
153
+ )
154
+ )
155
+ value_term = " ".join(quote_str(v) for v in values)
156
+ if len(values) > 1:
157
+ value_term = f"({value_term})"
158
+ solr_terms.append(f"{name}:{value_term}")
159
+ elif has_wildcard:
160
+ value_term = " ".join(quote_str(v) for v in values)
161
+ if len(values) > 1:
162
+ value_term = f"({value_term})"
163
+ solr_terms.append(f"{name}:{value_term}")
164
+ else:
165
+ params[name] = ",".join(values)
138
166
  else:
139
167
  value_term = " ".join(values)
140
- if name == "query": # freetext case
141
- solr_terms.append(value_term)
142
- else:
143
- if len(values) > 1:
144
- value_term = f"({value_term})"
145
- if name.startswith("!"):
146
- solr_terms.append(f"NOT ({name[1:]}:{value_term})")
168
+ if name == "query":
169
+ solr_terms.append(value_term)
147
170
  else:
148
- solr_terms.append(f"{name}:{value_term}")
171
+ if len(values) > 1:
172
+ value_term = f"({value_term})"
173
+ if name.startswith("!"):
174
+ solr_terms.append(f"NOT ({name[1:]}:{value_term})")
175
+ else:
176
+ solr_terms.append(f"{name}:{value_term}")
149
177
  if solr_terms:
150
178
  params["query"] = " AND ".join(solr_terms)
151
179
  for name, option in self.query.options.items(use_default=True):
@@ -562,6 +590,7 @@ class Context:
562
590
  ) -> list[File]:
563
591
  files: list[File] = []
564
592
  shas: set[str] = set()
593
+ file_ids: set[str] = set()
565
594
  async for result in self._fetch(*results):
566
595
  files_result = result.to(ResultFiles)
567
596
  files_result.process()
@@ -569,9 +598,13 @@ class Context:
569
598
  for file in files_result.data:
570
599
  if not keep_duplicates and file.sha in shas:
571
600
  logger.debug(f"Duplicate file {file.file_id}")
572
- else:
573
- files.append(file)
574
- shas.add(file.sha)
601
+ continue
602
+ if not keep_duplicates and file.file_id in file_ids:
603
+ logger.debug(f"Duplicate file_id {file.file_id}")
604
+ continue
605
+ files.append(file)
606
+ shas.add(file.sha)
607
+ file_ids.add(file.file_id)
575
608
  return files
576
609
 
577
610
  async def _search_as_queries(
@@ -54,7 +54,7 @@ from esgpull.plugin import (
54
54
  )
55
55
  from esgpull.processor import Processor
56
56
  from esgpull.result import Err, Ok, Result
57
- from esgpull.tui import UI, DummyLive, Verbosity, logger
57
+ from esgpull.tui import UI, DummyLive, ErrorCountColumn, Verbosity, logger
58
58
  from esgpull.utils import format_size
59
59
 
60
60
 
@@ -387,6 +387,7 @@ class Esgpull:
387
387
  SpinnerColumn(),
388
388
  MofNCompleteColumn(),
389
389
  TimeRemainingColumn(compact=True, elapsed_when_finished=True),
390
+ ErrorCountColumn(),
390
391
  )
391
392
  file_columns: list[str | ProgressColumn] = [
392
393
  TextColumn("[cyan][{task.id}] [b blue]{task.fields[sha]}"),
@@ -434,7 +435,9 @@ class Esgpull:
434
435
  if use_db:
435
436
  self.db.add(*processor.files)
436
437
  queue_size = len(processor.tasks)
437
- main_task_id = main_progress.add_task("", total=queue_size)
438
+ main_task_id = main_progress.add_task(
439
+ "", total=queue_size, nb_errors=0
440
+ )
438
441
  # TODO: rename ? installed/downloaded/completed/...
439
442
  files: list[File] = []
440
443
  errors: list[Err] = []
@@ -475,11 +478,13 @@ class Esgpull:
475
478
  )
476
479
  case Err(_, err):
477
480
  queue_size -= 1
478
- main_progress.update(
479
- main_task_id, total=queue_size
480
- )
481
481
  result.data.file.status = FileStatus.Error
482
482
  errors.append(result)
483
+ main_progress.update(
484
+ main_task_id,
485
+ total=queue_size,
486
+ nb_errors=len(errors),
487
+ )
483
488
  emit(
484
489
  Event.file_error,
485
490
  file=result.data.file,
@@ -0,0 +1,28 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.9.4
4
+ Revises: 0.9.3
5
+ Create Date: 2026-01-14 10:49:04.536961
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '0.9.4'
14
+ down_revision = '0.9.3'
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 ###
@@ -0,0 +1,28 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.9.5
4
+ Revises: 0.9.4
5
+ Create Date: 2026-02-04 15:52:02.010431
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '0.9.5'
14
+ down_revision = '0.9.4'
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 ###
@@ -101,6 +101,17 @@ class ExceptionToDebugFilter(logging.Filter):
101
101
  return True
102
102
 
103
103
 
104
+ class ErrorCountColumn(ProgressColumn):
105
+ def render(self, task):
106
+ nb_errors = task.fields.get("nb_errors", 0)
107
+
108
+ if nb_errors == 0:
109
+ return Text("")
110
+
111
+ suffix = "" if nb_errors == 1 else "s"
112
+ return Text(f"({nb_errors} download{suffix} failed)", style="red")
113
+
114
+
104
115
  @define
105
116
  class UI:
106
117
  path: Path = field(converter=Path)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "esgpull"
7
- version = "0.9.3"
7
+ version = "0.9.5"
8
8
  classifiers = [
9
9
  "License :: OSI Approved :: BSD License",
10
10
  "Programming Language :: Python :: 3",
@@ -27,7 +27,7 @@ dependencies = [
27
27
  "pyyaml>=6.0",
28
28
  "tomlkit>=0.11.5",
29
29
  "rich>=12.6.0",
30
- "sqlalchemy>=2.0.0b2",
30
+ "sqlalchemy>=2.0.0b2,<2.1",
31
31
  "setuptools>=65.4.1",
32
32
  "aiostream>=0.4.5",
33
33
  "attrs>=22.1.0",
@@ -14,24 +14,24 @@ aiofiles==25.1.0
14
14
  # via esgpull
15
15
  aiostream==0.7.1
16
16
  # via esgpull
17
- alembic==1.17.2
17
+ alembic==1.18.3
18
18
  # via esgpull
19
19
  annotated-types==0.7.0
20
20
  # via pydantic
21
- anyio==4.11.0
21
+ anyio==4.12.1
22
22
  # via httpx
23
23
  asttokens==3.0.1
24
24
  # via stack-data
25
25
  attrs==25.4.0
26
26
  # via cattrs
27
27
  # via esgpull
28
- babel==2.17.0
28
+ babel==2.18.0
29
29
  # via mkdocs-material
30
30
  backrefs==6.1
31
31
  # via mkdocs-material
32
32
  cattrs==25.3.0
33
33
  # via esgpull
34
- certifi==2025.11.12
34
+ certifi==2026.1.4
35
35
  # via httpcore
36
36
  # via httpx
37
37
  # via requests
@@ -49,18 +49,18 @@ colorama==0.4.6
49
49
  # via mkdocs-material
50
50
  comm==0.2.3
51
51
  # via ipykernel
52
- coverage==7.12.0
52
+ coverage==7.13.3
53
53
  # via pytest-cov
54
- cryptography==46.0.3
54
+ cryptography==46.0.4
55
55
  # via pyopenssl
56
- debugpy==1.8.17
56
+ debugpy==1.8.20
57
57
  # via ipykernel
58
58
  decorator==5.2.1
59
59
  # via ipdb
60
60
  # via ipython
61
61
  deprecated==1.3.1
62
62
  # via click-params
63
- exceptiongroup==1.3.0
63
+ exceptiongroup==1.3.1
64
64
  # via anyio
65
65
  # via cattrs
66
66
  # via ipython
@@ -69,11 +69,11 @@ execnet==2.1.2
69
69
  # via pytest-xdist
70
70
  executing==2.2.1
71
71
  # via stack-data
72
- filelock==3.20.0
72
+ filelock==3.20.3
73
73
  # via pytest-mypy
74
74
  ghp-import==2.1.0
75
75
  # via mkdocs
76
- greenlet==3.2.4
76
+ greenlet==3.3.1
77
77
  # via sqlalchemy
78
78
  h11==0.16.0
79
79
  # via httpcore
@@ -90,7 +90,7 @@ iniconfig==2.3.0
90
90
  ipdb==0.13.13
91
91
  ipykernel==7.1.0
92
92
  # via jupyter-console
93
- ipython==8.37.0
93
+ ipython==8.38.0
94
94
  # via ipdb
95
95
  # via ipykernel
96
96
  # via jupyter-console
@@ -99,7 +99,7 @@ jedi==0.19.2
99
99
  jinja2==3.1.6
100
100
  # via mkdocs
101
101
  # via mkdocs-material
102
- jupyter-client==8.6.3
102
+ jupyter-client==8.8.0
103
103
  # via ipykernel
104
104
  # via jupyter-console
105
105
  jupyter-console==6.6.3
@@ -107,9 +107,11 @@ jupyter-core==5.9.1
107
107
  # via ipykernel
108
108
  # via jupyter-client
109
109
  # via jupyter-console
110
+ librt==0.7.8
111
+ # via mypy
110
112
  mako==1.3.10
111
113
  # via alembic
112
- markdown==3.10
114
+ markdown==3.10.1
113
115
  # via mkdocs
114
116
  # via mkdocs-material
115
117
  # via pymdown-extensions
@@ -131,18 +133,18 @@ mkdocs==1.6.1
131
133
  # via mkdocs-material
132
134
  mkdocs-get-deps==0.2.0
133
135
  # via mkdocs
134
- mkdocs-material==9.7.0
136
+ mkdocs-material==9.7.1
135
137
  mkdocs-material-extensions==1.3.1
136
138
  # via mkdocs-material
137
- mypy==1.18.2
139
+ mypy==1.19.1
138
140
  # via pytest-mypy
139
141
  mypy-extensions==1.1.0
140
142
  # via mypy
141
143
  nest-asyncio==1.6.0
142
144
  # via esgpull
143
145
  # via ipykernel
144
- orjson==3.11.4
145
- packaging==25.0
146
+ orjson==3.11.7
147
+ packaging==26.0
146
148
  # via esgpull
147
149
  # via ipykernel
148
150
  # via mkdocs
@@ -151,12 +153,12 @@ paginate==0.5.7
151
153
  # via mkdocs-material
152
154
  parso==0.8.5
153
155
  # via jedi
154
- pathspec==0.12.1
156
+ pathspec==1.0.4
155
157
  # via mkdocs
156
158
  # via mypy
157
159
  pexpect==4.9.0
158
160
  # via ipython
159
- platformdirs==4.5.0
161
+ platformdirs==4.5.1
160
162
  # via esgpull
161
163
  # via jupyter-core
162
164
  # via mkdocs-get-deps
@@ -166,15 +168,15 @@ pluggy==1.6.0
166
168
  prompt-toolkit==3.0.52
167
169
  # via ipython
168
170
  # via jupyter-console
169
- psutil==7.1.3
171
+ psutil==7.2.2
170
172
  # via ipykernel
171
173
  ptyprocess==0.7.0
172
174
  # via pexpect
173
175
  pure-eval==0.2.3
174
176
  # via stack-data
175
- pycparser==2.23
177
+ pycparser==3.0
176
178
  # via cffi
177
- pydantic==2.12.4
179
+ pydantic==2.12.5
178
180
  # via esgpull
179
181
  # via pydantic-settings
180
182
  pydantic-core==2.41.5
@@ -187,13 +189,13 @@ pygments==2.19.2
187
189
  # via mkdocs-material
188
190
  # via pytest
189
191
  # via rich
190
- pymdown-extensions==10.17.1
192
+ pymdown-extensions==10.20.1
191
193
  # via mkdocs-material
192
194
  pyopenssl==25.3.0
193
195
  # via esgpull
194
- pyparsing==3.2.5
196
+ pyparsing==3.3.2
195
197
  # via esgpull
196
- pytest==9.0.1
198
+ pytest==9.0.2
197
199
  # via pytest-cov
198
200
  # via pytest-mypy
199
201
  # via pytest-xdist
@@ -219,28 +221,26 @@ pyzmq==27.1.0
219
221
  # via jupyter-console
220
222
  requests==2.32.5
221
223
  # via mkdocs-material
222
- rich==14.2.0
224
+ rich==14.3.2
223
225
  # via esgpull
224
- setuptools==80.9.0
226
+ setuptools==80.10.2
225
227
  # via esgpull
226
228
  six==1.17.0
227
229
  # via python-dateutil
228
- sniffio==1.3.1
229
- # via anyio
230
- sqlalchemy==2.0.44
230
+ sqlalchemy==2.0.46
231
231
  # via alembic
232
232
  # via esgpull
233
233
  stack-data==0.6.3
234
234
  # via ipython
235
- tomli==2.3.0
235
+ tomli==2.4.0
236
236
  # via alembic
237
237
  # via coverage
238
238
  # via ipdb
239
239
  # via mypy
240
240
  # via pytest
241
- tomlkit==0.13.3
241
+ tomlkit==0.14.0
242
242
  # via esgpull
243
- tornado==6.5.2
243
+ tornado==6.5.4
244
244
  # via ipykernel
245
245
  # via jupyter-client
246
246
  traitlets==5.14.3
@@ -269,13 +269,13 @@ typing-extensions==4.15.0
269
269
  typing-inspection==0.4.2
270
270
  # via pydantic
271
271
  # via pydantic-settings
272
- urllib3==2.5.0
272
+ urllib3==2.6.3
273
273
  # via requests
274
274
  validators==0.22.0
275
275
  # via click-params
276
276
  watchdog==6.0.0
277
277
  # via mkdocs
278
- wcwidth==0.2.14
278
+ wcwidth==0.5.3
279
279
  # via prompt-toolkit
280
- wrapt==2.0.1
280
+ wrapt==2.1.1
281
281
  # via deprecated
@@ -14,18 +14,18 @@ aiofiles==25.1.0
14
14
  # via esgpull
15
15
  aiostream==0.7.1
16
16
  # via esgpull
17
- alembic==1.17.2
17
+ alembic==1.18.3
18
18
  # via esgpull
19
19
  annotated-types==0.7.0
20
20
  # via pydantic
21
- anyio==4.11.0
21
+ anyio==4.12.1
22
22
  # via httpx
23
23
  attrs==25.4.0
24
24
  # via cattrs
25
25
  # via esgpull
26
26
  cattrs==25.3.0
27
27
  # via esgpull
28
- certifi==2025.11.12
28
+ certifi==2026.1.4
29
29
  # via httpcore
30
30
  # via httpx
31
31
  cffi==2.0.0
@@ -35,14 +35,14 @@ click==8.3.1
35
35
  # via esgpull
36
36
  click-params==0.5.0
37
37
  # via esgpull
38
- cryptography==46.0.3
38
+ cryptography==46.0.4
39
39
  # via pyopenssl
40
40
  deprecated==1.3.1
41
41
  # via click-params
42
- exceptiongroup==1.3.0
42
+ exceptiongroup==1.3.1
43
43
  # via anyio
44
44
  # via cattrs
45
- greenlet==3.2.4
45
+ greenlet==3.3.1
46
46
  # via sqlalchemy
47
47
  h11==0.16.0
48
48
  # via httpcore
@@ -63,13 +63,13 @@ mdurl==0.1.2
63
63
  # via markdown-it-py
64
64
  nest-asyncio==1.6.0
65
65
  # via esgpull
66
- packaging==25.0
66
+ packaging==26.0
67
67
  # via esgpull
68
- platformdirs==4.5.0
68
+ platformdirs==4.5.1
69
69
  # via esgpull
70
- pycparser==2.23
70
+ pycparser==3.0
71
71
  # via cffi
72
- pydantic==2.12.4
72
+ pydantic==2.12.5
73
73
  # via esgpull
74
74
  # via pydantic-settings
75
75
  pydantic-core==2.41.5
@@ -80,24 +80,22 @@ pygments==2.19.2
80
80
  # via rich
81
81
  pyopenssl==25.3.0
82
82
  # via esgpull
83
- pyparsing==3.2.5
83
+ pyparsing==3.3.2
84
84
  # via esgpull
85
85
  python-dotenv==1.2.1
86
86
  # via pydantic-settings
87
87
  pyyaml==6.0.3
88
88
  # via esgpull
89
- rich==14.2.0
89
+ rich==14.3.2
90
90
  # via esgpull
91
- setuptools==80.9.0
91
+ setuptools==80.10.2
92
92
  # via esgpull
93
- sniffio==1.3.1
94
- # via anyio
95
- sqlalchemy==2.0.44
93
+ sqlalchemy==2.0.46
96
94
  # via alembic
97
95
  # via esgpull
98
- tomli==2.3.0
96
+ tomli==2.4.0
99
97
  # via alembic
100
- tomlkit==0.13.3
98
+ tomlkit==0.14.0
101
99
  # via esgpull
102
100
  typing-extensions==4.15.0
103
101
  # via aiostream
@@ -116,5 +114,5 @@ typing-inspection==0.4.2
116
114
  # via pydantic-settings
117
115
  validators==0.22.0
118
116
  # via click-params
119
- wrapt==2.0.1
117
+ wrapt==2.1.1
120
118
  # via deprecated
@@ -30,7 +30,7 @@ def test_update_displays_dataset_completion(root: Path, config: Config):
30
30
  "variable_id:areacella",
31
31
  "experiment_id:1pctCO2",
32
32
  "--distrib",
33
- "false",
33
+ "true",
34
34
  "--track",
35
35
  ],
36
36
  )
@@ -0,0 +1,50 @@
1
+ from pathlib import Path
2
+
3
+ from click.testing import CliRunner
4
+
5
+ from esgpull import Esgpull
6
+ from esgpull.cli.download import download
7
+ from esgpull.config import Config
8
+ from esgpull.models import File, FileStatus, Query
9
+
10
+
11
+ def test_download_errors_in_progress(root: Path, config: Config):
12
+ # Configure the environment
13
+ config.generate(overwrite=True)
14
+ runner = CliRunner()
15
+ esg = Esgpull(root)
16
+
17
+ # Create a query without updating (no datasets will be created)
18
+ query = Query()
19
+ query.compute_sha()
20
+ query.track(query.options.default())
21
+
22
+ # Manually create files
23
+ failing_file = File(
24
+ file_id="failing_file",
25
+ dataset_id="test_dataset_id",
26
+ master_id="test_master_id",
27
+ url="https://nonexistent.path/to/file.nc",
28
+ version="1.0",
29
+ filename="failing.nc",
30
+ local_path="test/failing",
31
+ data_node="test_node",
32
+ checksum="12345",
33
+ checksum_type="SHA256",
34
+ size=1000,
35
+ status=FileStatus.Queued,
36
+ )
37
+ failing_file.compute_sha()
38
+
39
+ # Add files to query
40
+ query.files.append(failing_file)
41
+
42
+ # Add query to database
43
+ esg.db.add(query)
44
+
45
+ # Run the download instruction in the CLI
46
+ result_download = runner.invoke(download)
47
+
48
+ # Parse the output to extract
49
+ output = result_download.output
50
+ assert "download failed" in output
@@ -22,7 +22,7 @@ def test_update_after_remove(root: Path, config: Config):
22
22
  "experiment_id:1pctCO2",
23
23
  "source_id:CNRM-ESM2-1",
24
24
  "--distrib",
25
- "false",
25
+ "true",
26
26
  "--track",
27
27
  ],
28
28
  )
@@ -47,7 +47,7 @@ def test_update_after_remove(root: Path, config: Config):
47
47
  "experiment_id:1pctCO2",
48
48
  "source_id:CNRM-ESM2-1",
49
49
  "--distrib",
50
- "false",
50
+ "true",
51
51
  "--track",
52
52
  ],
53
53
  )
@@ -48,7 +48,7 @@ def test_update_updates_timestamp(root: Path, config: Config):
48
48
  "variable_id:areacella",
49
49
  "experiment_id:1pctCO2",
50
50
  "--distrib",
51
- "false",
51
+ "true",
52
52
  "--track",
53
53
  ],
54
54
  )