esgpull 0.7.2__tar.gz → 0.8.0__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 (152) hide show
  1. esgpull-0.8.0/CITATION.cff +33 -0
  2. {esgpull-0.7.2 → esgpull-0.8.0}/PKG-INFO +22 -2
  3. {esgpull-0.7.2 → esgpull-0.8.0}/README.md +20 -0
  4. {esgpull-0.7.2 → esgpull-0.8.0}/alembic.ini +0 -6
  5. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/show.py +29 -0
  6. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/status.py +6 -4
  7. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/update.py +17 -6
  8. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/utils.py +18 -24
  9. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/config.py +1 -0
  10. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/context.py +6 -6
  11. esgpull-0.8.0/esgpull/migrations/versions/0.7.3_update_tables.py +28 -0
  12. esgpull-0.8.0/esgpull/migrations/versions/0.8.0_update_tables.py +28 -0
  13. esgpull-0.8.0/esgpull/migrations/versions/14c72daea083_query_add_column_updated_at.py +36 -0
  14. esgpull-0.8.0/esgpull/migrations/versions/c7c8541fa741_query_add_column_added_at.py +37 -0
  15. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/models/options.py +1 -1
  16. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/models/query.py +40 -1
  17. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/utils.py +14 -2
  18. {esgpull-0.7.2 → esgpull-0.8.0}/pyproject.toml +7 -2
  19. {esgpull-0.7.2 → esgpull-0.8.0}/requirements-dev.lock +68 -63
  20. {esgpull-0.7.2 → esgpull-0.8.0}/requirements.lock +29 -27
  21. esgpull-0.8.0/tests/cli/test_parse.py +40 -0
  22. esgpull-0.8.0/tests/cli/test_show_dates.py +61 -0
  23. esgpull-0.8.0/tests/cli/test_update.py +104 -0
  24. {esgpull-0.7.2 → esgpull-0.8.0}/tests/test_context.py +1 -0
  25. {esgpull-0.7.2 → esgpull-0.8.0}/tests/test_db.py +2 -1
  26. {esgpull-0.7.2 → esgpull-0.8.0}/tests/test_graph.py +70 -30
  27. {esgpull-0.7.2 → esgpull-0.8.0}/tests/test_query.py +36 -7
  28. esgpull-0.8.0/tests/test_utils.py +47 -0
  29. esgpull-0.8.0/tests/utils.py +11 -0
  30. esgpull-0.7.2/tests/cli/test_update.py +0 -36
  31. esgpull-0.7.2/tests/test_utils.py +0 -23
  32. {esgpull-0.7.2 → esgpull-0.8.0}/.github/workflows/ci.yml +0 -0
  33. {esgpull-0.7.2 → esgpull-0.8.0}/.github/workflows/doc.yml +0 -0
  34. {esgpull-0.7.2 → esgpull-0.8.0}/.github/workflows/pypi-publish.yml +0 -0
  35. {esgpull-0.7.2 → esgpull-0.8.0}/.gitignore +0 -0
  36. {esgpull-0.7.2 → esgpull-0.8.0}/.pre-commit-config.yaml +0 -0
  37. {esgpull-0.7.2 → esgpull-0.8.0}/LICENSE +0 -0
  38. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/configuration.md +0 -0
  39. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/download.md +0 -0
  40. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/glossary.md +0 -0
  41. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/download_1.svg +0 -0
  42. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/download_2.svg +0 -0
  43. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/download_3.svg +0 -0
  44. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/download_4.svg +0 -0
  45. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/download_5.svg +0 -0
  46. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/download_6.svg +0 -0
  47. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/intro_1.svg +0 -0
  48. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/intro_2.svg +0 -0
  49. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/intro_3.svg +0 -0
  50. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/intro_4.svg +0 -0
  51. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/intro_5.svg +0 -0
  52. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/intro_6.svg +0 -0
  53. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/quickstart_1.svg +0 -0
  54. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/search_1.svg +0 -0
  55. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/search_2.svg +0 -0
  56. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/search_3.svg +0 -0
  57. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/search_4.svg +0 -0
  58. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/search_5.svg +0 -0
  59. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/search_6.svg +0 -0
  60. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/search_7.svg +0 -0
  61. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/images/search_ignore.svg +0 -0
  62. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/index.md +0 -0
  63. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/installation.md +0 -0
  64. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/queries.md +0 -0
  65. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/quickstart.md +0 -0
  66. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/search.md +0 -0
  67. {esgpull-0.7.2 → esgpull-0.8.0}/docs/docs/stylesheets/extra.css +0 -0
  68. {esgpull-0.7.2 → esgpull-0.8.0}/docs/includes/abbreviations.md +0 -0
  69. {esgpull-0.7.2 → esgpull-0.8.0}/docs/mkdocs.yml +0 -0
  70. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/__init__.py +0 -0
  71. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/auth.py +0 -0
  72. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/__init__.py +0 -0
  73. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/add.py +0 -0
  74. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/autoremove.py +0 -0
  75. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/config.py +0 -0
  76. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/convert.py +0 -0
  77. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/datasets.py +0 -0
  78. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/decorators.py +0 -0
  79. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/download.py +0 -0
  80. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/facet.py +0 -0
  81. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/get.py +0 -0
  82. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/install.py +0 -0
  83. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/login.py +0 -0
  84. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/remove.py +0 -0
  85. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/retry.py +0 -0
  86. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/search.py +0 -0
  87. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/self.py +0 -0
  88. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/cli/track.py +0 -0
  89. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/constants.py +0 -0
  90. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/database.py +0 -0
  91. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/download.py +0 -0
  92. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/esgpull.py +0 -0
  93. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/exceptions.py +0 -0
  94. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/fs.py +0 -0
  95. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/graph.py +0 -0
  96. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/install_config.py +0 -0
  97. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/README +0 -0
  98. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/env.py +0 -0
  99. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/script.py.mako +0 -0
  100. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.3.0_update_tables.py +0 -0
  101. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.3.1_update_tables.py +0 -0
  102. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.3.2_update_tables.py +0 -0
  103. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.3.3_update_tables.py +0 -0
  104. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.3.4_update_tables.py +0 -0
  105. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.3.5_update_tables.py +0 -0
  106. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.3.6_update_tables.py +0 -0
  107. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.3.7_update_tables.py +0 -0
  108. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.3.8_update_tables.py +0 -0
  109. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.4.0_update_tables.py +0 -0
  110. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.5.0_update_tables.py +0 -0
  111. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.5.1_update_tables.py +0 -0
  112. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.5.2_update_tables.py +0 -0
  113. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.5.3_update_tables.py +0 -0
  114. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.5.4_update_tables.py +0 -0
  115. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.5.5_update_tables.py +0 -0
  116. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.6.0_update_tables.py +0 -0
  117. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.6.1_update_tables.py +0 -0
  118. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.6.2_update_tables.py +0 -0
  119. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.6.3_update_tables.py +0 -0
  120. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.6.4_update_tables.py +0 -0
  121. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.6.5_update_tables.py +0 -0
  122. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.7.0_update_tables.py +0 -0
  123. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.7.1_update_tables.py +0 -0
  124. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/migrations/versions/0.7.2_update_tables.py +0 -0
  125. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/models/__init__.py +0 -0
  126. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/models/base.py +0 -0
  127. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/models/dataset.py +0 -0
  128. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/models/facet.py +0 -0
  129. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/models/file.py +0 -0
  130. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/models/selection.py +0 -0
  131. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/models/sql.py +0 -0
  132. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/models/synda_file.py +0 -0
  133. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/models/tag.py +0 -0
  134. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/models/utils.py +0 -0
  135. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/processor.py +0 -0
  136. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/py.typed +0 -0
  137. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/result.py +0 -0
  138. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/tui.py +0 -0
  139. {esgpull-0.7.2 → esgpull-0.8.0}/esgpull/version.py +0 -0
  140. {esgpull-0.7.2 → esgpull-0.8.0}/pdm.lock +0 -0
  141. {esgpull-0.7.2 → esgpull-0.8.0}/recipe/meta.yaml +0 -0
  142. {esgpull-0.7.2 → esgpull-0.8.0}/recipe/recipe.yaml +0 -0
  143. {esgpull-0.7.2 → esgpull-0.8.0}/tests/__init__.py +0 -0
  144. {esgpull-0.7.2 → esgpull-0.8.0}/tests/cli/__init__.py +0 -0
  145. {esgpull-0.7.2 → esgpull-0.8.0}/tests/conftest.py +0 -0
  146. {esgpull-0.7.2 → esgpull-0.8.0}/tests/test_auth.py +0 -0
  147. {esgpull-0.7.2 → esgpull-0.8.0}/tests/test_config.py +0 -0
  148. {esgpull-0.7.2 → esgpull-0.8.0}/tests/test_esgpull.py +0 -0
  149. {esgpull-0.7.2 → esgpull-0.8.0}/tests/test_fs.py +0 -0
  150. {esgpull-0.7.2 → esgpull-0.8.0}/tests/test_processor.py +0 -0
  151. {esgpull-0.7.2 → esgpull-0.8.0}/tests/test_selection.py +0 -0
  152. {esgpull-0.7.2 → esgpull-0.8.0}/tests/test_synda.py +0 -0
@@ -0,0 +1,33 @@
1
+ # This CITATION.cff file was generated with cffinit.
2
+
3
+ cff-version: 1.2.0
4
+ title: esgpull
5
+ message: ESGF command line download tool esgpull
6
+ type: software
7
+ authors:
8
+ - given-names: Sven
9
+ family-names: Rodriguez
10
+ email: sven.rodriguez@ipsl.fr
11
+ affiliation: Sorbonne University
12
+ - given-names: Atef
13
+ family-names: Ben Nasser
14
+ email: abennasser@ipsl.fr
15
+ affiliation: Centre National de Recherche Scientifique
16
+ orcid: 'https://orcid.org/0000-0001-6948-8735'
17
+ - given-names: Guillaume
18
+ family-names: Levavasseur
19
+ email: glipsl@ipsl.fr
20
+ affiliation: Sorbonne University
21
+ orcid: 'https://orcid.org/0000-0002-0801-0890'
22
+ repository-code: 'https://github.com/ESGF/esgf-download.git'
23
+ url: 'https://esgf.github.io/esgf-download/'
24
+ abstract: >-
25
+ esgpull is a modern ESGF data management tool, bundled
26
+ with a custom asynchronous interface with the ESGF Search
27
+ API. It handles scanning, downloading and updating
28
+ datasets, files and queries from ESGF.
29
+ license: BSD-3-Clause
30
+ commit: 762494e
31
+ version: 0.7.4
32
+ doi: 10.5281/zenodo.14228984
33
+ date-released: '2024-11-27'
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: esgpull
3
- Version: 0.7.2
3
+ Version: 0.8.0
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/
@@ -60,6 +60,18 @@ for dataset in datasets:
60
60
  - Command-line interface
61
61
  - HTTP download (async multi-file)
62
62
 
63
+ ## Installation
64
+
65
+ Install `esgpull` using pip or conda:
66
+
67
+ ```shell
68
+ pip install esgpull
69
+ ```
70
+
71
+ ```shell
72
+ conda install -c conda-forge ipsl::esgpull
73
+ ```
74
+
63
75
  ## Usage
64
76
 
65
77
  ```console
@@ -87,3 +99,11 @@ Commands:
87
99
  untrack Untrack queries
88
100
  update Fetch files, link files <-> queries, send files to download...
89
101
  ```
102
+
103
+ ## Useful links
104
+ * [ESGF Webinar: An Introduction to esgpull, A Replacement for Synda](https://www.youtube.com/watch?v=xv2RVMd1iCA)
105
+
106
+
107
+ ## Contributions
108
+
109
+ You can use the common github workflow (through pull requests and issues) to contribute.
@@ -25,6 +25,18 @@ for dataset in datasets:
25
25
  - Command-line interface
26
26
  - HTTP download (async multi-file)
27
27
 
28
+ ## Installation
29
+
30
+ Install `esgpull` using pip or conda:
31
+
32
+ ```shell
33
+ pip install esgpull
34
+ ```
35
+
36
+ ```shell
37
+ conda install -c conda-forge ipsl::esgpull
38
+ ```
39
+
28
40
  ## Usage
29
41
 
30
42
  ```console
@@ -52,3 +64,11 @@ Commands:
52
64
  untrack Untrack queries
53
65
  update Fetch files, link files <-> queries, send files to download...
54
66
  ```
67
+
68
+ ## Useful links
69
+ * [ESGF Webinar: An Introduction to esgpull, A Replacement for Synda](https://www.youtube.com/watch?v=xv2RVMd1iCA)
70
+
71
+
72
+ ## Contributions
73
+
74
+ You can use the common github workflow (through pull requests and issues) to contribute.
@@ -3,12 +3,6 @@ script_location = esgpull/migrations
3
3
  prepend_sys_path = .
4
4
  version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
5
5
 
6
- [post_write_hooks]
7
- hooks = black
8
- black.type = console_scripts
9
- black.entrypoint = black
10
- black.options = -l 79 REVISION_SCRIPT_FILENAME
11
-
12
6
  # Logging configuration
13
7
  [loggers]
14
8
  keys = root,sqlalchemy,alembic
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from datetime import datetime
4
+
3
5
  import click
4
6
  from click.exceptions import Abort, BadArgumentUsage, Exit
5
7
 
@@ -15,6 +17,18 @@ from esgpull.tui import Verbosity
15
17
  @groups.json_yaml
16
18
  @opts.files
17
19
  @opts.shas
20
+ @click.option(
21
+ "--after",
22
+ type=click.DateTime(["%Y-%m-%d"]),
23
+ default=None,
24
+ help="Filter queries added after this date (YYYY-MM-DD)",
25
+ )
26
+ @click.option(
27
+ "--before",
28
+ type=click.DateTime(["%Y-%m-%d"]),
29
+ default=None,
30
+ help="Filter queries added before this date (YYYY-MM-DD)",
31
+ )
18
32
  @opts.verbosity
19
33
  def show(
20
34
  query_id: str | None,
@@ -26,6 +40,8 @@ def show(
26
40
  json: bool,
27
41
  yaml: bool,
28
42
  shas: bool,
43
+ after: datetime | None,
44
+ before: datetime | None,
29
45
  verbosity: Verbosity,
30
46
  ) -> None:
31
47
  """
@@ -49,6 +65,19 @@ def show(
49
65
  parents=parents,
50
66
  keep_db=True,
51
67
  )
68
+
69
+ # Apply date filters if provided
70
+ if after or before:
71
+ filtered_queries = {}
72
+ for sha, query in graph.queries.items():
73
+ if after and query.added_at < after:
74
+ continue
75
+ if before and query.added_at > before:
76
+ continue
77
+ filtered_queries[sha] = query
78
+ graph.queries = filtered_queries
79
+ # Update the shas set to match the filtered queries
80
+ graph._shas = set(filtered_queries.keys())
52
81
  if tag is not None:
53
82
  tag_db = esg.graph.get_tag(tag)
54
83
  if tag_db is not None and tag_db.description is not None:
@@ -44,8 +44,10 @@ def status(
44
44
  for status, count, total_size in status_count_size:
45
45
  first_row = True
46
46
  for query in esg.graph.queries.values():
47
- files = [f for f in query.files if f.status == status]
48
- if files:
47
+ if not query.tracked:
48
+ continue
49
+ st_count, st_size = query.files_count_size(status)
50
+ if st_count:
49
51
  if first_row:
50
52
  if first_line:
51
53
  first_line = False
@@ -61,7 +63,7 @@ def status(
61
63
  first_row = False
62
64
  table.add_row(
63
65
  query.rich_name,
64
- str(len(files)),
65
- format_size(sum([f.size for f in files])),
66
+ str(st_count),
67
+ format_size(st_size),
66
68
  )
67
69
  esg.ui.print(table)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
4
5
 
5
6
  import click
6
7
  from click.exceptions import Abort, Exit
@@ -100,12 +101,20 @@ def update(
100
101
  # It might be interesting for the special case where all files already
101
102
  # exist in db, then the detailed fetch could be skipped.
102
103
  for qf in qfs:
103
- qf_results = esg.context.prepare_search_distributed(
104
- qf.expanded,
105
- file=True,
106
- hints=[qf.hints],
107
- max_hits=None,
108
- )
104
+ if esg.config.api.use_custom_distribution_algorithm:
105
+ qf_results = esg.context.prepare_search_distributed(
106
+ qf.expanded,
107
+ file=True,
108
+ hints=[qf.hints],
109
+ max_hits=None,
110
+ )
111
+ else:
112
+ qf_results = esg.context.prepare_search(
113
+ qf.expanded,
114
+ file=True,
115
+ hits=[qf.hits],
116
+ max_hits=None,
117
+ )
109
118
  nb_req = len(qf_results)
110
119
  if nb_req > 50:
111
120
  msg = (
@@ -184,4 +193,6 @@ def update(
184
193
  elif has_legacy and legacy in file_db.queries:
185
194
  esg.db.unlink(query=legacy, file=file_db)
186
195
  esg.db.link(query=qf.query, file=file)
196
+ qf.query.updated_at = datetime.now(timezone.utc)
197
+ esg.db.session.add(qf.query)
187
198
  esg.ui.raise_maybe_record(Exit(0))
@@ -15,7 +15,7 @@ from rich.text import Text
15
15
  from esgpull import Esgpull
16
16
  from esgpull.graph import Graph
17
17
  from esgpull.models import Dataset, File, Option, Options, Query, Selection
18
- from esgpull.tui import UI, TempUI, Verbosity
18
+ from esgpull.tui import UI, TempUI, Verbosity, logger
19
19
  from esgpull.utils import format_size
20
20
 
21
21
 
@@ -121,9 +121,15 @@ def totable(docs: list[OrderedDict[str, Any]]) -> Table:
121
121
  return table
122
122
 
123
123
 
124
+ def safe_value(value: str) -> str:
125
+ if " " in value:
126
+ return f'"{value}"'
127
+ else:
128
+ return value
129
+
130
+
124
131
  def parse_facets(facets: list[str]) -> Selection:
125
132
  facet_dict: dict[str, list[str]] = {}
126
- exact_terms: list[str] | None = None
127
133
  for facet in facets:
128
134
  match facet.split(":"):
129
135
  case [value]:
@@ -132,28 +138,9 @@ def parse_facets(facets: list[str]) -> Selection:
132
138
  ...
133
139
  case _:
134
140
  raise BadArgumentUsage(f"{facet!r} is not valid syntax.")
135
- if value.startswith("/"):
136
- if exact_terms is not None:
137
- raise BadArgumentUsage("Nested exact string is forbidden.")
138
- exact_terms = []
139
- if exact_terms is not None:
140
- if name != "query":
141
- raise BadArgumentUsage(
142
- "Cannot use facet term inside an exact string."
143
- )
144
- exact_terms.append(value)
145
- if value.endswith("/"):
146
- final_exact_str = " ".join(exact_terms)
147
- value = '"' + final_exact_str.strip("/") + '"'
148
- exact_terms = None
149
- else:
150
- continue
151
- facet_dict.setdefault(name, [])
152
- facet_dict[name].append(value)
153
- else:
154
- values = value.split(",")
155
- facet_dict.setdefault(name, [])
156
- facet_dict[name].extend(values)
141
+ values = list(map(safe_value, value.split(",")))
142
+ facet_dict.setdefault(name, [])
143
+ facet_dict[name].extend(values)
157
144
  selection = Selection()
158
145
  for name, values in facet_dict.items():
159
146
  selection[name] = values
@@ -170,6 +157,13 @@ def parse_query(
170
157
  replica: str | None,
171
158
  retracted: str | None,
172
159
  ) -> Query:
160
+ logger.info(f"{facets=}")
161
+ logger.info(f"{tags=}")
162
+ logger.info(f"{require=}")
163
+ logger.info(f"{distrib=}")
164
+ logger.info(f"{latest=}")
165
+ logger.info(f"{replica=}")
166
+ logger.info(f"{retracted=}")
173
167
  options = Options(
174
168
  distrib=distrib or Option.notset,
175
169
  latest=latest or Option.notset,
@@ -126,6 +126,7 @@ class API:
126
126
  page_limit: int = 50
127
127
  default_options: DefaultOptions = Factory(DefaultOptions)
128
128
  default_query_id: str = ""
129
+ use_custom_distribution_algorithm: bool = False
129
130
 
130
131
 
131
132
  def fix_rename_search_api(doc: TOMLDocument) -> TOMLDocument:
@@ -18,7 +18,7 @@ from esgpull.config import Config
18
18
  from esgpull.exceptions import SolrUnstableQueryError
19
19
  from esgpull.models import Dataset, File, Query
20
20
  from esgpull.tui import logger
21
- from esgpull.utils import format_date, index2url, sync
21
+ from esgpull.utils import format_date_iso, index2url, sync
22
22
 
23
23
  # workaround for notebooks with running event loop
24
24
  if asyncio.get_event_loop().is_running():
@@ -77,9 +77,9 @@ class Result:
77
77
  else:
78
78
  params["fields"] = "instance_id"
79
79
  if date_from is not None:
80
- params["from"] = format_date(date_from)
80
+ params["from"] = format_date_iso(date_from)
81
81
  if date_to is not None:
82
- params["to"] = format_date(date_to)
82
+ params["to"] = format_date_iso(date_to)
83
83
  if facets_param is not None:
84
84
  if len(set(facets_param) & DangerousFacets) > 0:
85
85
  raise SolrUnstableQueryError(pretty_repr(self.query))
@@ -90,9 +90,9 @@ class Result:
90
90
  facets_star = False
91
91
  # [?]TODO: add nominal temporal constraints `to`
92
92
  # if "start" in facets:
93
- # query["start"] = format_date(str(facets.pop("start")))
93
+ # query["start"] = format_date_iso(str(facets.pop("start")))
94
94
  # if "end" in facets:
95
- # query["end"] = format_date(str(facets.pop("end")))
95
+ # query["end"] = format_date_iso(str(facets.pop("end")))
96
96
  solr_terms: list[str] = []
97
97
  for name, values in self.query.selection.items():
98
98
  value_term = " ".join(values)
@@ -282,7 +282,7 @@ class Context:
282
282
  # # if since is None:
283
283
  # # self.since = since
284
284
  # # else:
285
- # # self.since = format_date(since)
285
+ # # self.since = format_date_iso(since)
286
286
 
287
287
  async def __aenter__(self) -> Context:
288
288
  if hasattr(self, "client"):
@@ -0,0 +1,28 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.7.3
4
+ Revises: 0.7.2
5
+ Create Date: 2024-09-20 12:22:11.989996
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '0.7.3'
14
+ down_revision = '0.7.2'
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.8.0
4
+ Revises: 14c72daea083
5
+ Create Date: 2025-05-15 11:28:10.755003
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '0.8.0'
14
+ down_revision = '14c72daea083'
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,36 @@
1
+ """query_add_column_updated_at
2
+
3
+ Revision ID: 14c72daea083
4
+ Revises: c7c8541fa741
5
+ Create Date: 2025-05-07 14:49:43.993125
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '14c72daea083'
14
+ down_revision = 'c7c8541fa741'
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ with op.batch_alter_table('query', schema=None) as batch_op:
21
+ batch_op.add_column(sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True))
22
+
23
+ # Backfill nulls
24
+ op.execute('UPDATE query SET updated_at = CURRENT_TIMESTAMP WHERE updated_at IS NULL')
25
+
26
+ # Make non-nullable
27
+ with op.batch_alter_table('query', schema=None) as batch_op:
28
+ batch_op.alter_column('updated_at', nullable=False)
29
+
30
+
31
+ def downgrade() -> None:
32
+ # ### commands auto generated by Alembic - please adjust! ###
33
+ with op.batch_alter_table('query', schema=None) as batch_op:
34
+ batch_op.drop_column('updated_at')
35
+
36
+ # ### end Alembic commands ###
@@ -0,0 +1,37 @@
1
+ """query_add_column_added_at
2
+
3
+ Revision ID: c7c8541fa741
4
+ Revises: 0.7.3
5
+ Create Date: 2025-05-05 16:14:57.140262
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = 'c7c8541fa741'
14
+ down_revision = '0.7.3'
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # Add as nullable first
21
+ with op.batch_alter_table('query', schema=None) as batch_op:
22
+ batch_op.add_column(sa.Column('added_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True))
23
+
24
+ # Backfill nulls
25
+ op.execute('UPDATE query SET added_at = CURRENT_TIMESTAMP WHERE added_at IS NULL')
26
+
27
+ # Make non-nullable
28
+ with op.batch_alter_table('query', schema=None) as batch_op:
29
+ batch_op.alter_column('added_at', nullable=False)
30
+
31
+
32
+ def downgrade() -> None:
33
+ # ### commands auto generated by Alembic - please adjust! ###
34
+ with op.batch_alter_table('query', schema=None) as batch_op:
35
+ batch_op.drop_column('added_at')
36
+
37
+ # ### end Alembic commands ###
@@ -53,7 +53,7 @@ class Options(Base):
53
53
  replica: Mapped[Option] = mapped_column(sa.Enum(Option))
54
54
  retracted: Mapped[Option] = mapped_column(sa.Enum(Option))
55
55
 
56
- _distrib_ = Option(False)
56
+ _distrib_ = Option(True)
57
57
  _latest_ = Option(True)
58
58
  _replica_ = Option(None)
59
59
  _retracted_ = Option(False)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Iterator, MutableMapping, Sequence
4
+ from datetime import datetime, timezone
4
5
  from typing import Any, Literal
5
6
 
6
7
  import sqlalchemy as sa
@@ -11,6 +12,7 @@ from rich.tree import Tree
11
12
  from sqlalchemy.orm import Mapped, mapped_column, object_session, relationship
12
13
  from typing_extensions import NotRequired, TypedDict
13
14
 
15
+ from esgpull import utils
14
16
  from esgpull.exceptions import UntrackableQuery
15
17
  from esgpull.models.base import Base, Sha
16
18
  from esgpull.models.file import FileDict, FileStatus
@@ -24,7 +26,18 @@ from esgpull.models.utils import (
24
26
  rich_measure_impl,
25
27
  short_sha,
26
28
  )
27
- from esgpull.utils import format_size
29
+ from esgpull.utils import format_date_iso, format_size
30
+
31
+ QUERY_DATE_FMT = "%Y-%m-%d %H:%M:%S"
32
+
33
+
34
+ def parse_date(d: datetime | str) -> datetime:
35
+ return utils.parse_date(d, fmt=QUERY_DATE_FMT)
36
+
37
+
38
+ def format_date(d: datetime | str) -> str:
39
+ return utils.format_date(d, fmt=QUERY_DATE_FMT)
40
+
28
41
 
29
42
  query_file_proxy = sa.Table(
30
43
  "query_file",
@@ -152,6 +165,8 @@ class QueryDict(TypedDict):
152
165
  options: NotRequired[MutableMapping[str, bool | None]]
153
166
  selection: NotRequired[MutableMapping[str, FacetValues]]
154
167
  files: NotRequired[list[FileDict]]
168
+ added_at: NotRequired[str]
169
+ updated_at: NotRequired[str]
155
170
 
156
171
 
157
172
  class Query(Base):
@@ -181,6 +196,14 @@ class Query(Base):
181
196
  back_populates="queries",
182
197
  repr=False,
183
198
  )
199
+ added_at: Mapped[datetime] = mapped_column(
200
+ server_default=sa.func.now(),
201
+ default_factory=lambda: datetime.now(timezone.utc),
202
+ )
203
+ updated_at: Mapped[datetime] = mapped_column(
204
+ server_default=sa.func.now(),
205
+ default_factory=lambda: datetime.now(timezone.utc),
206
+ )
184
207
 
185
208
  def __init__(
186
209
  self,
@@ -191,6 +214,8 @@ class Query(Base):
191
214
  options: Options | MutableMapping[str, bool | None] | None = None,
192
215
  selection: Selection | MutableMapping[str, FacetValues] | None = None,
193
216
  files: list[FileDict] | None = None,
217
+ added_at: datetime | str | None = None,
218
+ updated_at: datetime | str | None = None,
194
219
  ) -> None:
195
220
  self.tracked = tracked
196
221
  self.require = require
@@ -219,6 +244,14 @@ class Query(Base):
219
244
  if files is not None:
220
245
  for file in files:
221
246
  self.files.append(File.fromdict(file))
247
+ if added_at is not None:
248
+ self.added_at = parse_date(added_at)
249
+ else:
250
+ self.added_at = datetime.now(timezone.utc)
251
+ if updated_at is not None:
252
+ self.updated_at = parse_date(updated_at)
253
+ else:
254
+ self.updated_at = datetime.now(timezone.utc)
222
255
 
223
256
  @property
224
257
  def has_files(self) -> bool:
@@ -313,6 +346,8 @@ class Query(Base):
313
346
  result["options"] = self.options.asdict()
314
347
  if self.selection:
315
348
  result["selection"] = self.selection.asdict()
349
+ result["added_at"] = format_date(self.added_at)
350
+ result["updated_at"] = format_date(self.updated_at)
316
351
  return result
317
352
 
318
353
  def clone(self, compute_sha: bool = True) -> Query:
@@ -409,6 +444,10 @@ class Query(Base):
409
444
  title = Text.from_markup(self.rich_name)
410
445
  if not self.tracked:
411
446
  title.append(" untracked", style="i red")
447
+ title.append(
448
+ f"\n│ added {format_date_iso(self.added_at)}"
449
+ f"\n│ updated {format_date_iso(self.updated_at)}"
450
+ )
412
451
  contents = Table.grid(padding=(0, 1))
413
452
  if not hasattr(self, "_rich_no_require") and self.require is not None:
414
453
  if len(self.require) == 40:
@@ -31,7 +31,9 @@ def format_size(size: int) -> str:
31
31
  )
32
32
 
33
33
 
34
- def format_date(date: str | datetime.datetime, fmt: str = "%Y-%m-%d") -> str:
34
+ def parse_date(
35
+ date: str | datetime.datetime, fmt: str = "%Y-%m-%d"
36
+ ) -> datetime.datetime:
35
37
  match date:
36
38
  case datetime.datetime():
37
39
  ...
@@ -39,7 +41,17 @@ def format_date(date: str | datetime.datetime, fmt: str = "%Y-%m-%d") -> str:
39
41
  date = datetime.datetime.strptime(date, fmt)
40
42
  case _:
41
43
  raise ValueError(date)
42
- return date.replace(microsecond=0).isoformat() + "Z"
44
+ return date
45
+
46
+
47
+ def format_date(date: str | datetime.datetime, fmt: str = "%Y-%m-%d") -> str:
48
+ return parse_date(date, fmt).strftime(fmt)
49
+
50
+
51
+ def format_date_iso(
52
+ date: str | datetime.datetime, fmt: str = "%Y-%m-%d"
53
+ ) -> str:
54
+ return parse_date(date, fmt).replace(microsecond=0).isoformat() + "Z"
43
55
 
44
56
 
45
57
  def url2index(url: str) -> str:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "esgpull"
7
- version = "0.7.2"
7
+ version = "0.8.0"
8
8
  classifiers = [
9
9
  "License :: OSI Approved :: BSD License",
10
10
  "Programming Language :: Python :: 3",
@@ -102,5 +102,10 @@ dev-dependencies = [
102
102
  "types-pyyaml>=6.0.12.20240808",
103
103
  "types-aiofiles>=24.1.0.20240626",
104
104
  "pytest-mypy>=0.10.3",
105
- "pytest-xdist>=3.6.1"
105
+ "pytest-xdist>=3.6.1",
106
+ "ipdb>=0.13.13",
107
+ "orjson>=3.10.7"
106
108
  ]
109
+
110
+ [tool.rye.scripts]
111
+ esg = {cmd = "esgpull", env-file = ".env"}