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.
- {esgpull-0.9.2 → esgpull-0.9.3}/PKG-INFO +3 -2
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/configuration.md +34 -21
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/__init__.py +2 -2
- esgpull-0.9.3/esgpull/cli/index_nodes.py +94 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/search.py +1 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/update.py +1 -0
- esgpull-0.9.3/esgpull/config.py +401 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/constants.py +1 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/context.py +32 -3
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/esgpull.py +0 -5
- esgpull-0.9.3/esgpull/migrations/versions/0.9.3_update_tables.py +28 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/plugin.py +3 -2
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/processor.py +0 -4
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/tui.py +5 -5
- {esgpull-0.9.2 → esgpull-0.9.3}/pyproject.toml +3 -2
- {esgpull-0.9.2 → esgpull-0.9.3}/requirements-dev.lock +63 -50
- {esgpull-0.9.2 → esgpull-0.9.3}/requirements.lock +40 -27
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/test_dataset.py +9 -26
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/test_plugin_cli.py +23 -71
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/test_remove.py +9 -25
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/test_update.py +13 -20
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/conftest.py +11 -4
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_config.py +50 -2
- esgpull-0.9.3/tests/test_context.py +320 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_fs.py +0 -4
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_graph.py +1 -2
- esgpull-0.9.3/tests/test_processor.py +195 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_query.py +1 -2
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/utils.py +6 -1
- {esgpull-0.9.2 → esgpull-0.9.3}/uv.lock +152 -17
- esgpull-0.9.2/esgpull/auth.py +0 -181
- esgpull-0.9.2/esgpull/cli/login.py +0 -56
- esgpull-0.9.2/esgpull/config.py +0 -485
- esgpull-0.9.2/tests/test_auth.py +0 -21
- esgpull-0.9.2/tests/test_context.py +0 -211
- esgpull-0.9.2/tests/test_processor.py +0 -99
- {esgpull-0.9.2 → esgpull-0.9.3}/.github/workflows/ci.yml +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/.github/workflows/doc.yml +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/.github/workflows/pypi-publish.yml +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/.gitignore +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/.pre-commit-config.yaml +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/CITATION.cff +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/LICENSE +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/README.md +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/alembic.ini +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/download.md +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/glossary.md +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/download_1.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/download_2.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/download_3.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/download_4.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/download_5.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/download_6.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/intro_1.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/intro_2.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/intro_3.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/intro_4.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/intro_5.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/intro_6.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/quickstart_1.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_1.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_2.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_3.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_4.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_5.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_6.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_7.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/images/search_ignore.svg +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/index.md +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/installation.md +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/plugins.md +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/queries.md +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/quickstart.md +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/search.md +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/docs/stylesheets/extra.css +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/includes/abbreviations.md +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/docs/mkdocs.yml +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/__init__.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/add.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/autoremove.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/config.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/convert.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/decorators.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/download.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/facet.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/get.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/install.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/plugins.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/remove.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/retry.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/self.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/show.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/status.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/track.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/cli/utils.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/database.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/download.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/exceptions.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/fs.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/graph.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/install_config.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/README +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/env.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/script.py.mako +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.0_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.1_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.2_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.3_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.4_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.5_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.6_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.7_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.3.8_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.4.0_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.5.0_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.5.1_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.5.2_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.5.3_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.5.4_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.5.5_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.6.0_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.6.1_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.6.2_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.6.3_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.6.4_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.6.5_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.7.0_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.7.1_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.7.2_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.7.3_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.8.0_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.9.0_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.9.1_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/0.9.2_update_tables.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/14c72daea083_query_add_column_updated_at.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/c7c8541fa741_query_add_column_added_at.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/d14f179e553c_file_add_composite_index_dataset_id_.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/migrations/versions/e7edab5d4e4b_add_dataset_tracking.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/__init__.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/base.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/dataset.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/facet.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/file.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/options.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/query.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/selection.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/sql.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/synda_file.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/tag.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/models/utils.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/py.typed +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/result.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/utils.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/esgpull/version.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/pdm.lock +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/__init__.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/assets/error_plugin.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/assets/incompatible_plugin.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/assets/priority_test_plugin.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/assets/sample_plugin.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/__init__.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/test_parse.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/cli/test_show_dates.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_dataset.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_db.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_esgpull.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_plugin.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_selection.py +0 -0
- {esgpull-0.9.2 → esgpull-0.9.3}/tests/test_synda.py +0 -0
- {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.
|
|
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
|
-
##
|
|
100
|
+
## Index Node Configuration
|
|
102
101
|
|
|
103
|
-
|
|
102
|
+
The `api.index_node` setting determines which ESGF index node `esgpull` will query for dataset metadata and search results.
|
|
104
103
|
|
|
105
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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.
|
|
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))
|
|
@@ -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/"
|