esgpull 0.9.2__tar.gz → 0.9.3__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 (170) hide show
  1. {esgpull-0.9.2 → esgpull-0.9.3}/PKG-INFO +3 -2
  2. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/configuration.md +34 -21
  3. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/__init__.py +2 -2
  4. esgpull-0.9.3/esgpull/cli/index_nodes.py +94 -0
  5. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/search.py +1 -0
  6. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/update.py +1 -0
  7. esgpull-0.9.3/esgpull/config.py +401 -0
  8. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/constants.py +1 -0
  9. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/context.py +32 -3
  10. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/esgpull.py +0 -5
  11. esgpull-0.9.3/esgpull/migrations/versions/0.9.3_update_tables.py +28 -0
  12. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/plugin.py +3 -2
  13. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/processor.py +0 -4
  14. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/tui.py +5 -5
  15. {esgpull-0.9.2 → esgpull-0.9.3}/pyproject.toml +3 -2
  16. {esgpull-0.9.2 → esgpull-0.9.3}/requirements-dev.lock +63 -50
  17. {esgpull-0.9.2 → esgpull-0.9.3}/requirements.lock +40 -27
  18. {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/test_dataset.py +9 -26
  19. {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/test_plugin_cli.py +23 -71
  20. {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/test_remove.py +9 -25
  21. {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/test_update.py +13 -20
  22. {esgpull-0.9.2 → esgpull-0.9.3}/tests/conftest.py +11 -4
  23. {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_config.py +50 -2
  24. esgpull-0.9.3/tests/test_context.py +320 -0
  25. {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_fs.py +0 -4
  26. {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_graph.py +1 -2
  27. esgpull-0.9.3/tests/test_processor.py +195 -0
  28. {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_query.py +1 -2
  29. {esgpull-0.9.2 → esgpull-0.9.3}/tests/utils.py +6 -1
  30. {esgpull-0.9.2 → esgpull-0.9.3}/uv.lock +152 -17
  31. esgpull-0.9.2/esgpull/auth.py +0 -181
  32. esgpull-0.9.2/esgpull/cli/login.py +0 -56
  33. esgpull-0.9.2/esgpull/config.py +0 -485
  34. esgpull-0.9.2/tests/test_auth.py +0 -21
  35. esgpull-0.9.2/tests/test_context.py +0 -211
  36. esgpull-0.9.2/tests/test_processor.py +0 -99
  37. {esgpull-0.9.2 → esgpull-0.9.3}/.github/workflows/ci.yml +0 -0
  38. {esgpull-0.9.2 → esgpull-0.9.3}/.github/workflows/doc.yml +0 -0
  39. {esgpull-0.9.2 → esgpull-0.9.3}/.github/workflows/pypi-publish.yml +0 -0
  40. {esgpull-0.9.2 → esgpull-0.9.3}/.gitignore +0 -0
  41. {esgpull-0.9.2 → esgpull-0.9.3}/.pre-commit-config.yaml +0 -0
  42. {esgpull-0.9.2 → esgpull-0.9.3}/CITATION.cff +0 -0
  43. {esgpull-0.9.2 → esgpull-0.9.3}/LICENSE +0 -0
  44. {esgpull-0.9.2 → esgpull-0.9.3}/README.md +0 -0
  45. {esgpull-0.9.2 → esgpull-0.9.3}/alembic.ini +0 -0
  46. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/download.md +0 -0
  47. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/glossary.md +0 -0
  48. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/download_1.svg +0 -0
  49. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/download_2.svg +0 -0
  50. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/download_3.svg +0 -0
  51. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/download_4.svg +0 -0
  52. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/download_5.svg +0 -0
  53. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/download_6.svg +0 -0
  54. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/intro_1.svg +0 -0
  55. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/intro_2.svg +0 -0
  56. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/intro_3.svg +0 -0
  57. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/intro_4.svg +0 -0
  58. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/intro_5.svg +0 -0
  59. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/intro_6.svg +0 -0
  60. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/quickstart_1.svg +0 -0
  61. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_1.svg +0 -0
  62. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_2.svg +0 -0
  63. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_3.svg +0 -0
  64. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_4.svg +0 -0
  65. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_5.svg +0 -0
  66. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_6.svg +0 -0
  67. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_7.svg +0 -0
  68. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_ignore.svg +0 -0
  69. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/index.md +0 -0
  70. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/installation.md +0 -0
  71. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/plugins.md +0 -0
  72. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/queries.md +0 -0
  73. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/quickstart.md +0 -0
  74. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/search.md +0 -0
  75. {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/stylesheets/extra.css +0 -0
  76. {esgpull-0.9.2 → esgpull-0.9.3}/docs/includes/abbreviations.md +0 -0
  77. {esgpull-0.9.2 → esgpull-0.9.3}/docs/mkdocs.yml +0 -0
  78. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/__init__.py +0 -0
  79. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/add.py +0 -0
  80. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/autoremove.py +0 -0
  81. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/config.py +0 -0
  82. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/convert.py +0 -0
  83. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/decorators.py +0 -0
  84. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/download.py +0 -0
  85. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/facet.py +0 -0
  86. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/get.py +0 -0
  87. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/install.py +0 -0
  88. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/plugins.py +0 -0
  89. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/remove.py +0 -0
  90. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/retry.py +0 -0
  91. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/self.py +0 -0
  92. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/show.py +0 -0
  93. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/status.py +0 -0
  94. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/track.py +0 -0
  95. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/utils.py +0 -0
  96. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/database.py +0 -0
  97. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/download.py +0 -0
  98. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/exceptions.py +0 -0
  99. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/fs.py +0 -0
  100. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/graph.py +0 -0
  101. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/install_config.py +0 -0
  102. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/README +0 -0
  103. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/env.py +0 -0
  104. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/script.py.mako +0 -0
  105. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.0_update_tables.py +0 -0
  106. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.1_update_tables.py +0 -0
  107. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.2_update_tables.py +0 -0
  108. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.3_update_tables.py +0 -0
  109. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.4_update_tables.py +0 -0
  110. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.5_update_tables.py +0 -0
  111. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.6_update_tables.py +0 -0
  112. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.7_update_tables.py +0 -0
  113. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.8_update_tables.py +0 -0
  114. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.4.0_update_tables.py +0 -0
  115. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.5.0_update_tables.py +0 -0
  116. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.5.1_update_tables.py +0 -0
  117. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.5.2_update_tables.py +0 -0
  118. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.5.3_update_tables.py +0 -0
  119. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.5.4_update_tables.py +0 -0
  120. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.5.5_update_tables.py +0 -0
  121. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.6.0_update_tables.py +0 -0
  122. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.6.1_update_tables.py +0 -0
  123. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.6.2_update_tables.py +0 -0
  124. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.6.3_update_tables.py +0 -0
  125. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.6.4_update_tables.py +0 -0
  126. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.6.5_update_tables.py +0 -0
  127. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.7.0_update_tables.py +0 -0
  128. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.7.1_update_tables.py +0 -0
  129. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.7.2_update_tables.py +0 -0
  130. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.7.3_update_tables.py +0 -0
  131. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.8.0_update_tables.py +0 -0
  132. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.9.0_update_tables.py +0 -0
  133. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.9.1_update_tables.py +0 -0
  134. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.9.2_update_tables.py +0 -0
  135. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/14c72daea083_query_add_column_updated_at.py +0 -0
  136. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/c7c8541fa741_query_add_column_added_at.py +0 -0
  137. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/d14f179e553c_file_add_composite_index_dataset_id_.py +0 -0
  138. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/e7edab5d4e4b_add_dataset_tracking.py +0 -0
  139. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/__init__.py +0 -0
  140. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/base.py +0 -0
  141. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/dataset.py +0 -0
  142. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/facet.py +0 -0
  143. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/file.py +0 -0
  144. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/options.py +0 -0
  145. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/query.py +0 -0
  146. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/selection.py +0 -0
  147. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/sql.py +0 -0
  148. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/synda_file.py +0 -0
  149. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/tag.py +0 -0
  150. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/utils.py +0 -0
  151. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/py.typed +0 -0
  152. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/result.py +0 -0
  153. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/utils.py +0 -0
  154. {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/version.py +0 -0
  155. {esgpull-0.9.2 → esgpull-0.9.3}/pdm.lock +0 -0
  156. {esgpull-0.9.2 → esgpull-0.9.3}/tests/__init__.py +0 -0
  157. {esgpull-0.9.2 → esgpull-0.9.3}/tests/assets/error_plugin.py +0 -0
  158. {esgpull-0.9.2 → esgpull-0.9.3}/tests/assets/incompatible_plugin.py +0 -0
  159. {esgpull-0.9.2 → esgpull-0.9.3}/tests/assets/priority_test_plugin.py +0 -0
  160. {esgpull-0.9.2 → esgpull-0.9.3}/tests/assets/sample_plugin.py +0 -0
  161. {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/__init__.py +0 -0
  162. {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/test_parse.py +0 -0
  163. {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/test_show_dates.py +0 -0
  164. {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_dataset.py +0 -0
  165. {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_db.py +0 -0
  166. {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_esgpull.py +0 -0
  167. {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_plugin.py +0 -0
  168. {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_selection.py +0 -0
  169. {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_synda.py +0 -0
  170. {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esgpull
3
- Version: 0.9.2
3
+ Version: 0.9.3
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/
@@ -21,10 +21,11 @@ Requires-Dist: cattrs>=22.2.0
21
21
  Requires-Dist: click-params>=0.4.0
22
22
  Requires-Dist: click>=8.1.3
23
23
  Requires-Dist: httpx>=0.23.0
24
- Requires-Dist: myproxyclient>=2.1.0
25
24
  Requires-Dist: nest-asyncio>=1.5.6
26
25
  Requires-Dist: packaging>=25.0
27
26
  Requires-Dist: platformdirs>=2.6.2
27
+ Requires-Dist: pydantic-settings>=2.10.1
28
+ Requires-Dist: pydantic>=2.11.7
28
29
  Requires-Dist: pyopenssl>=22.1.0
29
30
  Requires-Dist: pyparsing>=3.0.9
30
31
  Requires-Dist: pyyaml>=6.0
@@ -9,7 +9,6 @@ $ esgpull config
9
9
 
10
10
  ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /home/me/.esgpull/config.toml ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
11
11
  [paths]
12
- auth = "/home/me/.esgpull/auth"
13
12
  data = "/home/me/.esgpull/data"
14
13
  db = "/home/me/.esgpull/db"
15
14
  log = "/home/me/.esgpull/log"
@@ -98,35 +97,49 @@ $ esgpull config --generate
98
97
  ```
99
98
 
100
99
 
101
- ## Login
100
+ ## Index Node Configuration
102
101
 
103
- Although most data on ESGF can be downloaded without authentication, some datasets require a valid OpenID login and password.
102
+ The `api.index_node` setting determines which ESGF index node `esgpull` will query for dataset metadata and search results.
104
103
 
105
- The ESGF OpenID authentication system is described on the [ESGF user documentation](http://www.esgf.io/esgf-user-support/user_guide.html).
104
+ ### `esg-search` Index Nodes
106
105
 
107
-
108
- This can be provided from the command line by running the following:
106
+ `esgpull` uses a single regional ESGF index node, backed by the `esg-search` API, the endpoint can be changed through configuration:
109
107
 
110
108
  ```shell
111
- $ esgpull login
109
+ $ esgpull config api.index_node esgf-node.ipsl.upmc.fr
112
110
  ```
111
+
112
+ Other available index nodes include:
113
+
114
+ - `esgf.ceda.ac.uk` (UK)
115
+ - `esgf-data.dkrz.de` (Germany)
116
+ - `esgf-node.ornl.gov/esgf-1-5-bridge` (USA, Bridge API)
117
+ - find more at https://esgf.github.io/nodes.html
118
+
119
+ ### Bridge API
120
+
121
+ The ORNL Bridge API node provides a temporary wrapper mimicking the API surface of the legacy esg-search code. This Bridge API has replaced the regular ORNL endpoint.
122
+
123
+ To use the ORNL node, you must specify the Bridge API path:
124
+
113
125
  ```shell
114
- No credentials found.
115
- [0] esg-dn1.nsc.liu.se
116
- [1] esgf-data.dkrz.de
117
- [2] ceda.ac.uk
118
- [3] esgf-node.ipsl.upmc.fr
119
- [4] esgf-node.llnl.gov
120
- [5] esgf.nci.org.au
121
- Select a provider: 0
122
- User: MyESGFusername
123
- Password: <hidden>
124
- Certificates are missing.
125
- 👍 Renewed successfully
126
+ $ esgpull config api.index_node esgf-node.ornl.gov/esgf-1-5-bridge
126
127
  ```
127
128
 
128
- The credentials will then be saved under the ``~/.esgpull/auth`` directory, within
129
- ``credentials.toml``, which can then be used for future sessions.
129
+ !!! note "Bridge API Compatibility"
130
+
131
+ The Bridge API has some limitations compared to standard ESGF index nodes:
132
+
133
+ - The `retracted` option is not supported and will be automatically removed from queries
134
+ - The `fields` parameter is not supported
135
+ - Facet queries may behave differently than on standard nodes
136
+
137
+ `esgpull` automatically detects when you're using a Bridge API endpoint (by checking for `esgf-1-5-bridge` in the URL) and adjusts query parameters accordingly to ensure compatibility.
138
+
139
+ ## Login (deprecated)
140
+
141
+ Login is not longer required and has been removed from `esgpull`.
142
+ Existing `auth` folder in esgpull installation folder can be removed safely.
130
143
 
131
144
  ## Plugins
132
145
 
@@ -8,7 +8,7 @@ from esgpull.cli.add import add
8
8
  from esgpull.cli.config import config
9
9
  from esgpull.cli.convert import convert
10
10
  from esgpull.cli.download import download
11
- from esgpull.cli.login import login
11
+ from esgpull.cli.index_nodes import index_nodes
12
12
  from esgpull.cli.plugins import plugins
13
13
  from esgpull.cli.remove import remove
14
14
  from esgpull.cli.retry import retry
@@ -40,7 +40,6 @@ SUBCOMMANDS: list[click.Command] = [
40
40
  # get,
41
41
  self,
42
42
  # install,
43
- login,
44
43
  plugins,
45
44
  remove,
46
45
  retry,
@@ -51,6 +50,7 @@ SUBCOMMANDS: list[click.Command] = [
51
50
  status,
52
51
  # # stats,
53
52
  update,
53
+ index_nodes,
54
54
  ]
55
55
 
56
56
  CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import OrderedDict
4
+
5
+ import click
6
+ from click.exceptions import Abort, Exit
7
+
8
+ from esgpull import Context
9
+ from esgpull.cli.decorators import groups, opts
10
+ from esgpull.cli.utils import init_esgpull, totable
11
+ from esgpull.models import Query
12
+ from esgpull.tui import Verbosity, logger
13
+
14
+
15
+ def check_node(c: Context, node: str) -> bool:
16
+ try:
17
+ c.probe(index_node=node)
18
+ return True
19
+ except:
20
+ return False
21
+
22
+
23
+ def find_nodes(c: Context, node: str) -> list[str]:
24
+ try:
25
+ hints = c.hints(
26
+ Query(), file=False, index_node=node, facets=["index_node"]
27
+ )
28
+ return list(hints[0]["index_node"])
29
+ except:
30
+ return []
31
+
32
+
33
+ @click.command()
34
+ @groups.json_yaml
35
+ @opts.record
36
+ @opts.verbosity
37
+ def index_nodes(
38
+ ## json_yaml
39
+ json: bool,
40
+ yaml: bool,
41
+ record: bool,
42
+ verbosity: Verbosity,
43
+ ) -> None:
44
+ """
45
+ Test index nodes for their current status
46
+ """
47
+ esg = init_esgpull(
48
+ verbosity,
49
+ safe=False,
50
+ record=record,
51
+ )
52
+ with esg.ui.logging("scan", onraise=Abort):
53
+ node_status: dict[str, bool] = {}
54
+ nodes = [
55
+ "esgf-node.ipsl.upmc.fr",
56
+ "esgf-data.dkrz.de",
57
+ "esgf.ceda.ac.uk",
58
+ "esgf-node.ornl.gov/esgf-1-5-bridge",
59
+ ]
60
+
61
+ esg.config.api.http_timeout = 3
62
+ with esg.ui.spinner("Fetching index nodes status"):
63
+ while True:
64
+ if not nodes:
65
+ break
66
+ node = nodes.pop()
67
+ if node in node_status:
68
+ continue
69
+ logger.info(f"check: {node}")
70
+ node_status[node] = check_node(esg.context, node)
71
+ logger.info(f"{node}\tok: {node_status[node]}")
72
+ for node in find_nodes(esg.context, node):
73
+ if node not in node_status:
74
+ nodes.append(node)
75
+ logger.info(f"found index_node: {node}")
76
+ if json:
77
+ esg.ui.print(node_status, json=True)
78
+ elif yaml:
79
+ esg.ui.print(node_status, yaml=True)
80
+ else:
81
+ table = [
82
+ OrderedDict(
83
+ [
84
+ ("node", node),
85
+ (
86
+ "status",
87
+ "[green]OK" if status else "[red]no response",
88
+ ),
89
+ ]
90
+ )
91
+ for node, status in node_status.items()
92
+ ]
93
+ esg.ui.print(totable(table))
94
+ esg.ui.raise_maybe_record(Exit(0))
@@ -99,6 +99,7 @@ def search(
99
99
  esg.ui.raise_maybe_record(Exit(0))
100
100
  esg.graph.add(query, force=True)
101
101
  query = esg.graph.expand(query.sha)
102
+ esg.context.probe()
102
103
  hits = esg.context.hits(
103
104
  query,
104
105
  file=file,
@@ -78,6 +78,7 @@ def update(
78
78
  if not qfs:
79
79
  esg.ui.print(":stop_sign: Trying to update untracked queries.")
80
80
  esg.ui.raise_maybe_record(Exit(0))
81
+ esg.context.probe()
81
82
  hints = esg.context.hints(
82
83
  *[qf.expanded for qf in qfs],
83
84
  file=True,
@@ -0,0 +1,401 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from collections.abc import Iterator, Mapping
5
+ from enum import Enum, auto
6
+ from pathlib import Path
7
+ from typing import Any, TypeVar
8
+
9
+ import tomlkit
10
+ from pydantic import BaseModel, Field, PrivateAttr, field_validator
11
+ from pydantic_settings import (
12
+ BaseSettings,
13
+ PydanticBaseSettingsSource,
14
+ SettingsConfigDict,
15
+ TomlConfigSettingsSource,
16
+ )
17
+
18
+ from esgpull.constants import CONFIG_FILENAME
19
+ from esgpull.exceptions import BadConfigError, VirtualConfigError
20
+ from esgpull.install_config import InstallConfig
21
+ from esgpull.models.options import Option, Options
22
+
23
+ logger = logging.getLogger("esgpull")
24
+ T = TypeVar("T")
25
+
26
+
27
+ def _get_root() -> Path:
28
+ if InstallConfig.current is not None:
29
+ return InstallConfig.current.path
30
+ else:
31
+ return InstallConfig.default
32
+
33
+
34
+ class Paths(BaseModel, validate_assignment=True, validate_default=True):
35
+ data: Path = Path("data")
36
+ db: Path = Path("db")
37
+ log: Path = Path("log")
38
+ tmp: Path = Path("tmp")
39
+ plugins: Path = Path("plugins")
40
+
41
+ @field_validator(
42
+ "data",
43
+ "db",
44
+ "log",
45
+ "tmp",
46
+ "plugins",
47
+ mode="after",
48
+ )
49
+ @classmethod
50
+ def _set_path_from_root(cls, value: Path) -> Path:
51
+ root = _get_root()
52
+ if not value.is_absolute():
53
+ value = root / value
54
+ return value
55
+
56
+ def values(self) -> Iterator[Path]:
57
+ yield self.data
58
+ yield self.db
59
+ yield self.log
60
+ yield self.tmp
61
+ yield self.plugins
62
+
63
+
64
+ class Credentials(BaseModel, validate_assignment=True):
65
+ filename: str = "credentials.toml"
66
+
67
+
68
+ class Cli(BaseModel, validate_assignment=True):
69
+ page_size: int = 20
70
+
71
+
72
+ class Db(BaseModel, validate_assignment=True):
73
+ filename: str = "esgpull.db"
74
+
75
+
76
+ class Download(BaseModel, validate_assignment=True):
77
+ chunk_size: int = 1 << 26
78
+ http_timeout: int = 20
79
+ max_concurrent: int = 5
80
+ disable_ssl: bool = False
81
+ disable_checksum: bool = False
82
+ show_filename: bool = False
83
+
84
+
85
+ class DefaultOptions(BaseModel, validate_assignment=True):
86
+ distrib: str = Options._distrib_.name
87
+ latest: str = Options._latest_.name
88
+ replica: str = Options._replica_.name
89
+ retracted: str = Options._retracted_.name
90
+
91
+ @field_validator(
92
+ "distrib", "latest", "replica", "retracted", mode="before"
93
+ )
94
+ @classmethod
95
+ def _is_valid_option(cls, value: str | Option) -> str:
96
+ if isinstance(value, str):
97
+ return Option(value.lower()).name
98
+ else:
99
+ return value.name
100
+
101
+ def asdict(self) -> dict[str, str]:
102
+ return dict(
103
+ distrib=self.distrib,
104
+ latest=self.latest,
105
+ replica=self.replica,
106
+ retracted=self.retracted,
107
+ )
108
+
109
+
110
+ class API(BaseModel, validate_assignment=True):
111
+ index_node: str = "esgf-node.ipsl.upmc.fr"
112
+ http_timeout: int = 20
113
+ max_concurrent: int = 5
114
+ page_limit: int = 50
115
+ default_options: DefaultOptions = Field(default_factory=DefaultOptions)
116
+ default_query_id: str = ""
117
+ use_custom_distribution_algorithm: bool = False
118
+
119
+
120
+ class Plugins(BaseModel, validate_assignment=True):
121
+ enabled: bool = False
122
+
123
+
124
+ class ConfigKind(Enum):
125
+ Virtual = auto()
126
+ NoFile = auto()
127
+ Partial = auto()
128
+ Complete = auto()
129
+
130
+
131
+ class ConfigKey:
132
+ path: tuple[str, ...]
133
+
134
+ def __init__(
135
+ self,
136
+ first: str | tuple[str, ...] | list[str],
137
+ *rest: str,
138
+ ) -> None:
139
+ if isinstance(first, (tuple, list)):
140
+ self.path = tuple(first) + rest
141
+ elif "." in first:
142
+ self.path = tuple(first.split(".")) + rest
143
+ else:
144
+ self.path = (first,) + rest
145
+
146
+ def __iter__(self) -> Iterator[str]:
147
+ yield from self.path
148
+
149
+ def __hash__(self) -> int:
150
+ return hash(str(self.path))
151
+
152
+ def __eq__(self, other: object) -> bool:
153
+ match other:
154
+ case ConfigKey():
155
+ return self.path == other.path
156
+ case _:
157
+ raise TypeError(type(other))
158
+
159
+ def __repr__(self) -> str:
160
+ return ".".join(self)
161
+
162
+ def __add__(self, path: str) -> ConfigKey:
163
+ return ConfigKey(self.path, path)
164
+
165
+ def __len__(self) -> int:
166
+ return len(self.path)
167
+
168
+ def exists_in(self, source: Mapping | None) -> bool:
169
+ if source is None:
170
+ return False
171
+ doc = source
172
+ for key in self:
173
+ if key in doc:
174
+ doc = doc[key]
175
+ else:
176
+ return False
177
+ return True
178
+
179
+ def value_of(self, source: Any) -> Any:
180
+ doc = source
181
+ for key in self:
182
+ try:
183
+ doc = doc[key]
184
+ except TypeError:
185
+ doc = getattr(doc, key)
186
+ return doc
187
+
188
+
189
+ def fix_rename_search_api(doc: dict) -> dict:
190
+ if "api" in doc and "search" in doc:
191
+ raise KeyError(
192
+ "Both 'api' and 'search' (deprecated) are used in your "
193
+ "config, please use 'api' only."
194
+ )
195
+ elif "search" in doc:
196
+ logger.warn(
197
+ "Deprecated key 'search' is used in your config, "
198
+ "please use 'api' instead."
199
+ )
200
+ doc["api"] = doc.pop("search")
201
+ return doc
202
+
203
+
204
+ def fix_remove_auth(doc: dict) -> dict:
205
+ if "paths" in doc and "auth" in doc["paths"]:
206
+ logger.warn(
207
+ "Deprecated 'paths.auth' is present in your config, "
208
+ "you can remove it safely."
209
+ )
210
+ doc["paths"].pop("auth")
211
+ return doc
212
+
213
+
214
+ def iter_keys(
215
+ source: Mapping,
216
+ path: ConfigKey | None = None,
217
+ ) -> Iterator[ConfigKey]:
218
+ for key in source.keys():
219
+ if path is None:
220
+ local_path = ConfigKey(key)
221
+ else:
222
+ local_path = path + key
223
+ if isinstance(source[key], Mapping):
224
+ yield from iter_keys(source[key], local_path)
225
+ else:
226
+ yield local_path
227
+
228
+
229
+ def pop_and_clear_empty_parents(source: Mapping, ckey: ConfigKey):
230
+ *parent_path, last_key = ckey.path
231
+ parent_ckey = ConfigKey(parent_path)
232
+ parent_ckey.value_of(source).pop(last_key)
233
+
234
+ for i in range(len(parent_path), 0, -1):
235
+ parent_ckey = ConfigKey(parent_path[: i - 1])
236
+ container_ckey = ConfigKey(parent_path[:i])
237
+ parent = parent_ckey.value_of(source)
238
+ container = container_ckey.value_of(source)
239
+ if isinstance(container, dict) and len(container) == 0:
240
+ parent.pop(container_ckey.path[-1])
241
+ else:
242
+ break # Stop if we hit a non-empty container
243
+
244
+
245
+ config_fixers = [fix_rename_search_api, fix_remove_auth]
246
+
247
+
248
+ class TomlKitConfigSettingsSource(TomlConfigSettingsSource):
249
+ def _read_file(self, file_path: Path) -> dict[str, Any]:
250
+ with open(file_path, mode="rb") as toml_file:
251
+ doc = tomlkit.load(toml_file)
252
+ for fixer in config_fixers:
253
+ try:
254
+ doc = fixer(doc)
255
+ except Exception:
256
+ raise BadConfigError(file_path)
257
+ return dict(doc)
258
+
259
+
260
+ class Config(BaseSettings):
261
+ # TODO: set in a load method instead
262
+ # model_config = SettingsConfigDict(toml_file=_get_root() / "config.toml")
263
+ model_config = SettingsConfigDict(toml_file=None)
264
+
265
+ paths: Paths = Field(default_factory=Paths)
266
+ credentials: Credentials = Field(default_factory=Credentials)
267
+ cli: Cli = Field(default_factory=Cli)
268
+ db: Db = Field(default_factory=Db)
269
+ download: Download = Field(default_factory=Download)
270
+ api: API = Field(default_factory=API)
271
+ plugins: Plugins = Field(default_factory=Plugins)
272
+ _raw: dict[str, Any] | None = PrivateAttr(default=None)
273
+ _config_file: Path | None = PrivateAttr(default=None)
274
+
275
+ @classmethod
276
+ def settings_customise_sources(
277
+ cls,
278
+ settings_cls: type[BaseSettings],
279
+ init_settings: PydanticBaseSettingsSource,
280
+ env_settings: PydanticBaseSettingsSource,
281
+ dotenv_settings: PydanticBaseSettingsSource,
282
+ file_secret_settings: PydanticBaseSettingsSource,
283
+ ) -> tuple[PydanticBaseSettingsSource, ...]:
284
+ return (
285
+ init_settings,
286
+ TomlKitConfigSettingsSource(settings_cls),
287
+ )
288
+
289
+ @classmethod
290
+ def load(cls, path: Path) -> Config:
291
+ try:
292
+ file_path = path / CONFIG_FILENAME
293
+ cls.model_config["toml_file"] = file_path
294
+ instance = cls()
295
+ finally:
296
+ cls.model_config["toml_file"] = None
297
+ instance._config_file = file_path
298
+ if file_path.is_file():
299
+ with file_path.open("rb") as toml_file:
300
+ instance._raw = tomlkit.load(toml_file)
301
+ else:
302
+ instance._raw = None
303
+ return instance
304
+
305
+ @classmethod
306
+ def default(cls) -> Config:
307
+ ## TODO: rename+deprecate
308
+ ## very bad name since this is loading from the **default** config
309
+ ## file path, as set in InstallConfig
310
+ return cls.load(path=_get_root())
311
+
312
+ @property
313
+ def kind(self) -> ConfigKind:
314
+ if self._config_file is None:
315
+ return ConfigKind.Virtual
316
+ elif not self._config_file.is_file():
317
+ return ConfigKind.NoFile
318
+ elif self.unset_options():
319
+ return ConfigKind.Partial
320
+ else:
321
+ return ConfigKind.Complete
322
+
323
+ def dump(self, with_defaults: bool = True) -> dict:
324
+ result = self.model_dump(mode="json")
325
+ if not with_defaults:
326
+ unset = set(self.unset_options())
327
+ for ckey in iter_keys(self.model_dump()):
328
+ if ckey in unset:
329
+ pop_and_clear_empty_parents(result, ckey)
330
+ return result
331
+
332
+ def unset_options(self) -> list[ConfigKey]:
333
+ result: list[ConfigKey] = []
334
+ dump = self.model_dump()
335
+ for ckey in iter_keys(dump):
336
+ if not ckey.exists_in(self._raw):
337
+ result.append(ckey)
338
+ return result
339
+
340
+ def update_item(
341
+ self,
342
+ key: str,
343
+ value: T,
344
+ empty_ok: bool = False,
345
+ ) -> T:
346
+ if self._raw is None and empty_ok:
347
+ self._raw = {}
348
+ elif self._raw is None:
349
+ raise VirtualConfigError
350
+ doc = self._raw
351
+ obj = self
352
+ ckey = ConfigKey(key)
353
+ *parts, last = ckey
354
+ for part in parts:
355
+ doc.setdefault(part, {})
356
+ doc = doc[part]
357
+ obj = getattr(obj, part)
358
+ old_value = getattr(obj, last)
359
+ setattr(obj, last, value)
360
+ doc[last] = value
361
+ return old_value
362
+
363
+ def set_default(self, key: str) -> Any:
364
+ ckey = ConfigKey(key)
365
+ if self._raw is None:
366
+ raise VirtualConfigError()
367
+ elif not ckey.exists_in(self._raw):
368
+ return None
369
+ default_config = Config()
370
+ default_value: Any = ckey.value_of(default_config)
371
+ old_value: Any = ckey.value_of(self)
372
+
373
+ *parent_path, last_key = ckey.path
374
+ parent_ckey = ConfigKey(parent_path)
375
+ obj = parent_ckey.value_of(self)
376
+ setattr(obj, last_key, default_value)
377
+ pop_and_clear_empty_parents(self._raw, ckey)
378
+ return old_value
379
+
380
+ def generate(self, overwrite: bool = False) -> None:
381
+ match (self.kind, overwrite):
382
+ case (ConfigKind.Virtual, _):
383
+ raise VirtualConfigError
384
+ case (ConfigKind.Partial, overwrite):
385
+ defaults = self.model_dump()
386
+ for ckey in self.unset_options():
387
+ self.update_item(str(ckey), ckey.value_of(defaults))
388
+ case (ConfigKind.Partial | ConfigKind.Complete, _):
389
+ raise FileExistsError(self._config_file)
390
+ case (ConfigKind.NoFile, _):
391
+ self._raw = self.model_dump(mode="json")
392
+ case _:
393
+ raise ValueError(self.kind)
394
+ self.write()
395
+
396
+ def write(self) -> None:
397
+ if self._config_file is None or self._raw is None:
398
+ raise VirtualConfigError
399
+ self._config_file.parent.mkdir(parents=True, exist_ok=True)
400
+ with self._config_file.open("w") as f:
401
+ tomlkit.dump(self._raw, f)
@@ -4,6 +4,7 @@ CONFIG_FILENAME = "config.toml"
4
4
  INSTALLS_PATH_ENV = "ESGPULL_INSTALLS_PATH"
5
5
  ROOT_ENV = "ESGPULL_CURRENT"
6
6
  ESGPULL_DEBUG = os.environ.get("ESGPULL_DEBUG", "0") == "1"
7
+ ESGPULL_DEBUG_LOCALS = os.environ.get("ESGPULL_DEBUG_LOCALS", "0") == "1"
7
8
 
8
9
  IDP = "/esgf-idp/openid/"
9
10
  CEDA_IDP = "/OpenID/Provider/server/"