esgf-pyclient 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- esgf_pyclient-0.3.2.dist-info/METADATA +119 -0
- esgf_pyclient-0.3.2.dist-info/RECORD +18 -0
- esgf_pyclient-0.3.2.dist-info/WHEEL +5 -0
- esgf_pyclient-0.3.2.dist-info/licenses/LICENSE +29 -0
- esgf_pyclient-0.3.2.dist-info/top_level.txt +1 -0
- pyesgf/__init__.py +6 -0
- pyesgf/exceptions.py +16 -0
- pyesgf/logon.py +300 -0
- pyesgf/search/__init__.py +17 -0
- pyesgf/search/connection.py +379 -0
- pyesgf/search/constraints.py +32 -0
- pyesgf/search/consts.py +14 -0
- pyesgf/search/context.py +363 -0
- pyesgf/search/exceptions.py +12 -0
- pyesgf/search/results.py +329 -0
- pyesgf/security/__init__.py +13 -0
- pyesgf/security/ats.py +101 -0
- pyesgf/util.py +136 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: esgf-pyclient
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: A library interacting with ESGF services within Python
|
|
5
|
+
Home-page: http://esgf-pyclient.readthedocs.org
|
|
6
|
+
Download-URL: https://github.com/ESGF/esgf-pyclient
|
|
7
|
+
Author: Ag Stephens
|
|
8
|
+
Author-email: Ag.Stephens@stfc.ac.uk
|
|
9
|
+
License: BSD
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.11.0
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: defusedxml
|
|
23
|
+
Requires-Dist: jinja2
|
|
24
|
+
Requires-Dist: requests
|
|
25
|
+
Requires-Dist: requests_cache
|
|
26
|
+
Requires-Dist: webob
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest; extra == "dev"
|
|
29
|
+
Requires-Dist: flake8; extra == "dev"
|
|
30
|
+
Requires-Dist: defusedxml; extra == "dev"
|
|
31
|
+
Requires-Dist: jinja2; extra == "dev"
|
|
32
|
+
Requires-Dist: requests; extra == "dev"
|
|
33
|
+
Requires-Dist: requests_cache; extra == "dev"
|
|
34
|
+
Requires-Dist: webob; extra == "dev"
|
|
35
|
+
Requires-Dist: myproxyclient>=2.1.1; extra == "dev"
|
|
36
|
+
Requires-Dist: nbval; extra == "dev"
|
|
37
|
+
Requires-Dist: sphinx; extra == "dev"
|
|
38
|
+
Requires-Dist: sphinx_mdinclude; extra == "dev"
|
|
39
|
+
Requires-Dist: nbsphinx; extra == "dev"
|
|
40
|
+
Requires-Dist: pandoc; extra == "dev"
|
|
41
|
+
Requires-Dist: nbconvert; extra == "dev"
|
|
42
|
+
Requires-Dist: ipykernel; extra == "dev"
|
|
43
|
+
Dynamic: author
|
|
44
|
+
Dynamic: author-email
|
|
45
|
+
Dynamic: classifier
|
|
46
|
+
Dynamic: description
|
|
47
|
+
Dynamic: description-content-type
|
|
48
|
+
Dynamic: download-url
|
|
49
|
+
Dynamic: home-page
|
|
50
|
+
Dynamic: license
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
Dynamic: provides-extra
|
|
53
|
+
Dynamic: requires-dist
|
|
54
|
+
Dynamic: requires-python
|
|
55
|
+
Dynamic: summary
|
|
56
|
+
|
|
57
|
+
[](https://esgf-pyclient.readthedocs.io/en/latest/?badge=latest)
|
|
58
|
+
[](https://github.com/ESGF/esgf-pyclient/actions)
|
|
59
|
+
[](https://github.com/ESGF/esgf-pyclient/blob/master/LICENSE)
|
|
60
|
+
|
|
61
|
+
# esgf-pyclient
|
|
62
|
+
|
|
63
|
+
ESGF PyClient is a Python package designed for interacting with the [Earth System Grid Federation](https://esgf.llnl.gov/) system.
|
|
64
|
+
|
|
65
|
+
> [!WARNING]
|
|
66
|
+
> This package interacts with the
|
|
67
|
+
> [legacy ESGF Search API](https://esgf.github.io/esg-search/ESGF_Search_RESTful_API.html),
|
|
68
|
+
> which is deprecated and servers may be shut down permanently.
|
|
69
|
+
> Users are encouraged to switch to
|
|
70
|
+
> [intake-esgf](https://intake-esgf.readthedocs.io/), or use
|
|
71
|
+
> [pystac-client](https://pystac-client.readthedocs.io/) to interact with the
|
|
72
|
+
> new STAC-based search API. The new STAC servers are available at:
|
|
73
|
+
>
|
|
74
|
+
> - https://discovery.east.esgf.io
|
|
75
|
+
> - https://discovery.west.esgf.io
|
|
76
|
+
>
|
|
77
|
+
> Progress reports on the development of the new ESGF infrastructure can
|
|
78
|
+
> be found on the [ESGF Roadmap GitHub
|
|
79
|
+
> repository](https://github.com/ESGF/esgf-roadmap/blob/main/status/README.md).
|
|
80
|
+
|
|
81
|
+
This package contains API code for calling the
|
|
82
|
+
[legacy ESGF Search API](https://esgf.github.io/esg-search/ESGF_Search_RESTful_API.html)
|
|
83
|
+
from Python code. It may be used to access data that is only findable
|
|
84
|
+
through the legacy ESGF Search API, such as CMIP5, CORDEX, and older
|
|
85
|
+
obs4MIPs data.
|
|
86
|
+
|
|
87
|
+
You can try it online using Binder, or view the notebooks on NBViewer:
|
|
88
|
+
[](https://mybinder.org/v2/gh/ESGF/esgf-pyclient.git/master?filepath=notebooks)
|
|
89
|
+
[](https://nbviewer.jupyter.org/github/ESGF/esgf-pyclient/tree/master/notebooks/)
|
|
90
|
+
|
|
91
|
+
Please submit bugs and feature requests through the bug tracker on
|
|
92
|
+
[GitHub](https://github.com/ESGF/esgf-pyclient). Pull requests are
|
|
93
|
+
always welcome.
|
|
94
|
+
|
|
95
|
+
Full [documentation](http://esgf-pyclient.readthedocs.org) is available
|
|
96
|
+
on ReadTheDocs or in the docs directory.
|
|
97
|
+
|
|
98
|
+
## Available servers
|
|
99
|
+
|
|
100
|
+
At the time of writing (September 9, 2026), servers providing search
|
|
101
|
+
using this API may be found at the following URLs:
|
|
102
|
+
|
|
103
|
+
- <https://esgf-node.ornl.gov/esgf-1-5-bridge> (partially supports the legacy ESGF Search API, issues can be reported [here](https://github.com/esgf2-us/esg_fastapi/issues))
|
|
104
|
+
- <https://esgf.ceda.ac.uk/esg-search/search>
|
|
105
|
+
- <https://esgf-data.dkrz.de/esg-search/search>
|
|
106
|
+
- <https://esg-dn1.nsc.liu.se/esg-search/search>
|
|
107
|
+
|
|
108
|
+
while the following servers appear to be online, but return an error
|
|
109
|
+
page:
|
|
110
|
+
|
|
111
|
+
- <https://esgf-node.ipsl.upmc.fr/esg-search/search>
|
|
112
|
+
- <https://esgf.nci.org.au/esg-search/search>
|
|
113
|
+
- <https://esgf.nccs.nasa.gov/esg-search/search>
|
|
114
|
+
- <https://esgdata.gfdl.noaa.gov/esg-search/search>
|
|
115
|
+
|
|
116
|
+
Note that logging in using OpenID with the
|
|
117
|
+
[`pyesgf.logon`](https://esgf-pyclient.readthedocs.io/en/latest/api.html#module-pyesgf-logon)
|
|
118
|
+
module is no longer required to access ESGF data.
|
|
119
|
+
All servers supporting this feature have been taken offline.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
esgf_pyclient-0.3.2.dist-info/licenses/LICENSE,sha256=3iMAbo0g_5511YQjYqcO1AFcen0KUvm9PPCT9mIsm_c,1554
|
|
2
|
+
pyesgf/__init__.py,sha256=bdeax8a2O8eHF1QvGk9c7Y6BTm-Xl6pAz6FUJNCj8wo,89
|
|
3
|
+
pyesgf/exceptions.py,sha256=KtFtJFRK86Un8LnZlpzHl_n5uDeeUKQ86d18vHXCX-8,170
|
|
4
|
+
pyesgf/logon.py,sha256=CoIDmY491Y6yUiCI0TfHC2rarAyXsFhP8IGMiDCPx1E,10667
|
|
5
|
+
pyesgf/util.py,sha256=nR_i7VJRw6BtVMf_wKI1UVSlNEwbamwi88m_RWKqS98,4150
|
|
6
|
+
pyesgf/search/__init__.py,sha256=QVAj8jIIvpApQii8XPU7MssG7010BlAS_Z_YokRirRo,574
|
|
7
|
+
pyesgf/search/connection.py,sha256=PjsG0ABB2YK4e_Ve3ARO0ikLkdnO6uZ5i97BFNTNetU,13226
|
|
8
|
+
pyesgf/search/constraints.py,sha256=F5r4SewiVT5ZUzV7wH10AiWfUi1tyiLgJWvzxe_XyRU,778
|
|
9
|
+
pyesgf/search/consts.py,sha256=ocFMwTwNtwjRoD-qCUsfc3-cK24aFSXK-5KusGr3FK4,450
|
|
10
|
+
pyesgf/search/context.py,sha256=ZQqVJNNICflOo2ozPfdV3UcYNLpLQhTpXWHAGeMIZ6k,13546
|
|
11
|
+
pyesgf/search/exceptions.py,sha256=obhqko0Yy2f4dY-iuszyHmeBrG7_TjsIxoMKe2iruYk,183
|
|
12
|
+
pyesgf/search/results.py,sha256=HvDQB7bPpcJgyDgPeGwFoBs_LP89dvTrNgLYE0wiVao,9618
|
|
13
|
+
pyesgf/security/__init__.py,sha256=aBtm4Xxk_30gc8766VbAaVUrw1RprPl_smwhNjgOB64,379
|
|
14
|
+
pyesgf/security/ats.py,sha256=PgrPNtybXREXoq6cgZzyWkwK_xYGqck6SvpXjOidzro,3195
|
|
15
|
+
esgf_pyclient-0.3.2.dist-info/METADATA,sha256=Y-lknpSx3bhErZ5UuGlJfQFWkA7TIdfwNm0PSDDudTQ,5085
|
|
16
|
+
esgf_pyclient-0.3.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
17
|
+
esgf_pyclient-0.3.2.dist-info/top_level.txt,sha256=XGYAbIglPZB5md4Z0gEUAQLkbU2zXrNUQLvvyakPjKo,7
|
|
18
|
+
esgf_pyclient-0.3.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2012, Science & Technology Facilities Council (STFC)
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyesgf
|
pyesgf/__init__.py
ADDED
pyesgf/exceptions.py
ADDED
pyesgf/logon.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Module :mod:`pyesgf.logon`
|
|
4
|
+
==========================
|
|
5
|
+
|
|
6
|
+
Manage the client's interaction with ESGF's security system. Using this
|
|
7
|
+
module requires installing the MyProxyClient_ library.
|
|
8
|
+
|
|
9
|
+
.. _MyProxyClient: http://pypi.python.org/pypi/MyProxyClient
|
|
10
|
+
|
|
11
|
+
To obtain ESGF credentials create a :class:`LogonManager` instance and supply
|
|
12
|
+
it with logon details::
|
|
13
|
+
|
|
14
|
+
>>> lm = LogonManager()
|
|
15
|
+
>>> lm.is_logged_on()
|
|
16
|
+
False
|
|
17
|
+
>>> lm.logon(username, password, myproxy_hostname, bootstrap=True)
|
|
18
|
+
>>> lm.is_logged_on()
|
|
19
|
+
True
|
|
20
|
+
|
|
21
|
+
Logon parameters that aren't specified will be prompted for at the terminal
|
|
22
|
+
by default. The :class:`LogonManager` object also writes a ``.httprc`` file
|
|
23
|
+
configuring OPeNDAP access through the NetCDF API.
|
|
24
|
+
|
|
25
|
+
The option ``bootstrap=True`` is needed on the first run.
|
|
26
|
+
|
|
27
|
+
You can use your OpenID to logon instead. The logon details will be deduced
|
|
28
|
+
from the OpenID where possible::
|
|
29
|
+
|
|
30
|
+
>>> lm.logoff()
|
|
31
|
+
>>> lm.is_logged_on()
|
|
32
|
+
False
|
|
33
|
+
>>> lm.logon_with_openid(openid, password, bootstrap=True)
|
|
34
|
+
>>> lm.is_logged_on()
|
|
35
|
+
True
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import os
|
|
40
|
+
import os.path as op
|
|
41
|
+
import shutil
|
|
42
|
+
from defusedxml import ElementTree
|
|
43
|
+
import requests
|
|
44
|
+
import re
|
|
45
|
+
from getpass import getpass
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from myproxy.client import MyProxyClient
|
|
49
|
+
import OpenSSL
|
|
50
|
+
_has_myproxy = True
|
|
51
|
+
except (ImportError, SyntaxError):
|
|
52
|
+
_has_myproxy = False
|
|
53
|
+
|
|
54
|
+
from .exceptions import OpenidResolutionError
|
|
55
|
+
|
|
56
|
+
# -----------------------------------------------------------------------------
|
|
57
|
+
# Constants
|
|
58
|
+
|
|
59
|
+
ESGF_DIR = op.join(os.path.expanduser('~'), '.esg')
|
|
60
|
+
ESGF_CERTS_DIR = 'certificates'
|
|
61
|
+
ESGF_CREDENTIALS = 'credentials.pem'
|
|
62
|
+
DAP_CONFIG = op.join(os.path.expanduser('~'), '.dodsrc')
|
|
63
|
+
DAP_CONFIG_MARKER = '<<< Managed by esgf-pyclient >>>'
|
|
64
|
+
|
|
65
|
+
XRI_NS = 'xri://$xrd*($v*2.0)'
|
|
66
|
+
MYPROXY_URN = 'urn:esg:security:myproxy-service'
|
|
67
|
+
ESGF_OPENID_REXP = r'https://.*/esgf-idp/openid/(.*)'
|
|
68
|
+
MYPROXY_URI_REXP = r'socket://([^:]*):?(\d+)?'
|
|
69
|
+
|
|
70
|
+
# -----------------------------------------------------------------------------
|
|
71
|
+
# classes
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class LogonManager(object):
|
|
75
|
+
"""
|
|
76
|
+
Manages ESGF crendentials and security configuration files.
|
|
77
|
+
|
|
78
|
+
Also integrates with NetCDF's secure OPeNDAP configuration.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
STATE_LOGGED_ON = 0
|
|
82
|
+
STATE_NO_CREDENTIALS = 1
|
|
83
|
+
STATE_EXPIRED_CREDENTIALS = 2
|
|
84
|
+
STATE_INVALID_CREDENTIALS = 3
|
|
85
|
+
|
|
86
|
+
def __init__(self, esgf_dir=ESGF_DIR, dap_config=DAP_CONFIG,
|
|
87
|
+
verify=True):
|
|
88
|
+
"""
|
|
89
|
+
:param esgf_dir: Root directory of ESGF state. Default ~/.esg
|
|
90
|
+
:param dap_config: Set the location of .httprc. Defaults to ~/.httprc
|
|
91
|
+
:param verify: SSL verification option. Default ``True``.
|
|
92
|
+
|
|
93
|
+
See the ``requests`` documenation to configure the
|
|
94
|
+
``verify`` option:
|
|
95
|
+
|
|
96
|
+
http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification
|
|
97
|
+
|
|
98
|
+
Note if dap_config is defined your current working directory must be
|
|
99
|
+
the same as the location as the dap_config file when OPeNDAP is
|
|
100
|
+
initialised.
|
|
101
|
+
"""
|
|
102
|
+
if not _has_myproxy:
|
|
103
|
+
raise ImportError('pyesgf.logon requires MyProxyClient')
|
|
104
|
+
|
|
105
|
+
self.esgf_dir = esgf_dir
|
|
106
|
+
self.esgf_credentials = op.join(self.esgf_dir, ESGF_CREDENTIALS)
|
|
107
|
+
self.esgf_certs_dir = op.join(self.esgf_dir, ESGF_CERTS_DIR)
|
|
108
|
+
self.dap_config = dap_config
|
|
109
|
+
self.verify = verify
|
|
110
|
+
|
|
111
|
+
self._write_dap_config()
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def state(self):
|
|
115
|
+
if not op.exists(self.esgf_credentials):
|
|
116
|
+
return self.STATE_NO_CREDENTIALS
|
|
117
|
+
else:
|
|
118
|
+
with open(self.esgf_credentials) as fh:
|
|
119
|
+
data = fh.read()
|
|
120
|
+
cert = OpenSSL.crypto.load_certificate(
|
|
121
|
+
OpenSSL.SSL.FILETYPE_PEM, data)
|
|
122
|
+
|
|
123
|
+
if cert.has_expired():
|
|
124
|
+
return self.STATE_EXPIRED_CREDENTIALS
|
|
125
|
+
|
|
126
|
+
# !TODO: check credentials against certificates
|
|
127
|
+
|
|
128
|
+
return self.STATE_LOGGED_ON
|
|
129
|
+
|
|
130
|
+
def is_logged_on(self):
|
|
131
|
+
return self.state == self.STATE_LOGGED_ON
|
|
132
|
+
|
|
133
|
+
def logon_with_openid(self, openid, password=None,
|
|
134
|
+
bootstrap=False, update_trustroots=True,
|
|
135
|
+
interactive=True):
|
|
136
|
+
"""
|
|
137
|
+
Obtains ESGF credentials by detecting the MyProxy parameters from
|
|
138
|
+
the users OpenID. Some ESGF compatible OpenIDs do not contain enough
|
|
139
|
+
information to obtain credentials. In this case the user is prompted
|
|
140
|
+
for missing information if ``interactive == True``, otherwise an
|
|
141
|
+
exception is raised.
|
|
142
|
+
|
|
143
|
+
:param openid: OpenID to login with See :meth:`logon` for parameters
|
|
144
|
+
``interactive``, ``bootstrap`` and ``update_trustroots``.
|
|
145
|
+
"""
|
|
146
|
+
username, myproxy = self._get_logon_details(openid)
|
|
147
|
+
return self.logon(username, password, myproxy,
|
|
148
|
+
bootstrap=bootstrap,
|
|
149
|
+
update_trustroots=update_trustroots,
|
|
150
|
+
interactive=interactive)
|
|
151
|
+
|
|
152
|
+
def logon(self, username=None, password=None, hostname=None,
|
|
153
|
+
bootstrap=False, update_trustroots=True,
|
|
154
|
+
interactive=True):
|
|
155
|
+
"""
|
|
156
|
+
Obtain ESGF credentials from the specified MyProxy service.
|
|
157
|
+
|
|
158
|
+
If ``interactive == True`` then any missing parameters of ``password``,
|
|
159
|
+
``username`` or ``hostname`` will be prompted for at the terminal.
|
|
160
|
+
|
|
161
|
+
:param interactive: Whether to ask for input at the terminal for
|
|
162
|
+
any missing information. I.e. username, password or hostname.
|
|
163
|
+
:param bootstrap: Whether to bootstrap the trustroots for this
|
|
164
|
+
MyProxy service.
|
|
165
|
+
:param update_trustroots: Whether to update the trustroots for this
|
|
166
|
+
MyProxy service.
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
if interactive:
|
|
170
|
+
if hostname is None:
|
|
171
|
+
print('Enter myproxy hostname:', end=' ')
|
|
172
|
+
hostname = input()
|
|
173
|
+
if username is None:
|
|
174
|
+
print('Enter myproxy username:', end=' ')
|
|
175
|
+
username = input()
|
|
176
|
+
if password is None:
|
|
177
|
+
password = getpass('Enter password for %s: ' % username)
|
|
178
|
+
|
|
179
|
+
if None in (hostname, username, password):
|
|
180
|
+
raise OpenidResolutionError('Full logon details not available')
|
|
181
|
+
|
|
182
|
+
c = MyProxyClient(hostname=hostname, caCertDir=self.esgf_certs_dir)
|
|
183
|
+
|
|
184
|
+
creds = c.logon(username, password,
|
|
185
|
+
bootstrap=bootstrap,
|
|
186
|
+
updateTrustRoots=update_trustroots)
|
|
187
|
+
with open(self.esgf_credentials, 'wb') as fh:
|
|
188
|
+
for cred in creds:
|
|
189
|
+
fh.write(cred)
|
|
190
|
+
|
|
191
|
+
def logoff(self, clear_trustroots=False):
|
|
192
|
+
"""
|
|
193
|
+
Remove any obtained credentials from the ESGF environment.
|
|
194
|
+
|
|
195
|
+
:param clear_trustroots: If True also remove trustroots.
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
if op.exists(self.esgf_credentials):
|
|
199
|
+
os.remove(self.esgf_credentials)
|
|
200
|
+
|
|
201
|
+
if clear_trustroots:
|
|
202
|
+
shutil.rmtree(self.esgf_certs_dir)
|
|
203
|
+
|
|
204
|
+
def _get_logon_details(self, openid):
|
|
205
|
+
response = requests.get(openid, verify=self.verify)
|
|
206
|
+
xml = ElementTree.fromstring(response.content)
|
|
207
|
+
|
|
208
|
+
hostname = None
|
|
209
|
+
port = None
|
|
210
|
+
username = None
|
|
211
|
+
|
|
212
|
+
services = xml.findall('.//{%s}Service' % XRI_NS)
|
|
213
|
+
for service in services:
|
|
214
|
+
try:
|
|
215
|
+
service_type = service.find('{%s}Type' % XRI_NS).text
|
|
216
|
+
except AttributeError:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# Detect myproxy hostname and port
|
|
220
|
+
if service_type == MYPROXY_URN:
|
|
221
|
+
myproxy_uri = service.find('{%s}URI' % XRI_NS).text
|
|
222
|
+
mo = re.match(MYPROXY_URI_REXP, myproxy_uri)
|
|
223
|
+
if mo:
|
|
224
|
+
hostname, port = mo.groups()
|
|
225
|
+
|
|
226
|
+
# If the OpenID matches the standard ESGF pattern assume it contains
|
|
227
|
+
# the username, otherwise prompt or raise an exception
|
|
228
|
+
mo = re.match(ESGF_OPENID_REXP, openid)
|
|
229
|
+
if mo:
|
|
230
|
+
username = mo.group(1)
|
|
231
|
+
|
|
232
|
+
# !TODO maybe support different myproxy port
|
|
233
|
+
if port is not None:
|
|
234
|
+
if not int(port) == 7512:
|
|
235
|
+
raise AssertionError()
|
|
236
|
+
|
|
237
|
+
return username, hostname
|
|
238
|
+
|
|
239
|
+
def _write_dap_config(self, verbose=False, validate=False):
|
|
240
|
+
preamble, managed, postamble = self._parse_dap_config()
|
|
241
|
+
|
|
242
|
+
with open(self.dap_config, 'w') as fh:
|
|
243
|
+
fh.write(('{preamble}\n'
|
|
244
|
+
'# BEGIN {marker}\n'
|
|
245
|
+
'HTTP.VERBOSE={verbose}\n'
|
|
246
|
+
'HTTP.COOKIEJAR={esgf_dir}/.dods_cookies\n'
|
|
247
|
+
'HTTP.SSL.VALIDATE=0\n'
|
|
248
|
+
'HTTP.SSL.CERTIFICATE={esgf_dir}/credentials.pem\n'
|
|
249
|
+
'HTTP.SSL.KEY={esgf_dir}/credentials.pem\n'
|
|
250
|
+
'HTTP.SSL.CAPATH={esgf_certs_dir}\n'
|
|
251
|
+
'# END {marker}\n'
|
|
252
|
+
'{postamble}\n')
|
|
253
|
+
.format(verbose=1 if verbose else 0,
|
|
254
|
+
# validate=1 if validate else 0,
|
|
255
|
+
esgf_certs_dir=self.esgf_certs_dir,
|
|
256
|
+
esgf_dir=self.esgf_dir,
|
|
257
|
+
marker=DAP_CONFIG_MARKER,
|
|
258
|
+
preamble=preamble,
|
|
259
|
+
postamble=postamble))
|
|
260
|
+
|
|
261
|
+
def _parse_dap_config(self, config_str=None):
|
|
262
|
+
"""
|
|
263
|
+
Read the DAP_CONFIG file and extract the parts not controlled
|
|
264
|
+
by esgf-pyclient.
|
|
265
|
+
|
|
266
|
+
:return: (preamble, managed, postamble), three strings of
|
|
267
|
+
configuration lines before, within and after the esgf-pyclient
|
|
268
|
+
controlled block.
|
|
269
|
+
|
|
270
|
+
"""
|
|
271
|
+
if config_str is None:
|
|
272
|
+
if not op.exists(self.dap_config):
|
|
273
|
+
return ('', '', '')
|
|
274
|
+
config_str = open(self.dap_config).read()
|
|
275
|
+
|
|
276
|
+
sections = re.split(r'^# (?:BEGIN|END) {0}$\n'
|
|
277
|
+
.format(DAP_CONFIG_MARKER),
|
|
278
|
+
config_str, flags=re.M)
|
|
279
|
+
|
|
280
|
+
if len(sections) < 2:
|
|
281
|
+
preamble, managed, postamble = sections[0], '', ''
|
|
282
|
+
elif len(sections) == 2:
|
|
283
|
+
preamble, managed, postamble = sections + ['']
|
|
284
|
+
elif len(sections) == 3:
|
|
285
|
+
preamble, managed, postamble = sections
|
|
286
|
+
else:
|
|
287
|
+
# In odd circumstances there might be more than 3 parts of the
|
|
288
|
+
# config so assume the final config is the one to keep
|
|
289
|
+
managed, unmanaged = [], []
|
|
290
|
+
sections.reverse()
|
|
291
|
+
while sections:
|
|
292
|
+
unmanaged.append(sections.pop())
|
|
293
|
+
if sections:
|
|
294
|
+
managed.append(sections.pop())
|
|
295
|
+
|
|
296
|
+
preamble = '\n'.join(unmanaged[:-1])
|
|
297
|
+
postamble = unmanaged[-1]
|
|
298
|
+
managed = managed[-1]
|
|
299
|
+
|
|
300
|
+
return preamble.strip(), managed.strip(), postamble.strip()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
An interface to the `ESGF Search API`_
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
.. _`ESGF Search API`: http://esgf.org/wiki/ESGF_Search_API
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .connection import SearchConnection # noqa: F401
|
|
10
|
+
from .context import SearchContext # noqa: F401
|
|
11
|
+
from .constraints import GeospatialConstraint, any_of, not_equals # noqa: F401
|
|
12
|
+
from .results import ResultSet # noqa: F401
|
|
13
|
+
from .consts import TYPE_DATASET, TYPE_FILE # noqa: F401
|
|
14
|
+
|
|
15
|
+
# !TODO: ResultFormatter class. process response json to specialise the result
|
|
16
|
+
# json. Default is None
|
|
17
|
+
# !TODO: pipe results to new process. Command-line interface.
|