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.
@@ -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
+ [![Documentation Status](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://esgf-pyclient.readthedocs.io/en/latest/?badge=latest)
58
+ [![Build Status](https://github.com/ESGF/esgf-pyclient/workflows/build%20%E2%9A%99%EF%B8%8F/badge.svg)](https://github.com/ESGF/esgf-pyclient/actions)
59
+ [![GitHub license](https://img.shields.io/github/license/ESGF/esgf-pyclient.svg)](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
+ [![Binder Launcher](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/ESGF/esgf-pyclient.git/master?filepath=notebooks)
89
+ [![NBViewer](https://raw.githubusercontent.com/jupyter/design/master/logos/Badges/nbviewer_badge.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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
@@ -0,0 +1,6 @@
1
+ """
2
+ Top-level package for Python interface to ESGF services.
3
+
4
+ """
5
+
6
+ __version__ = '0.3.2'
pyesgf/exceptions.py ADDED
@@ -0,0 +1,16 @@
1
+ """
2
+ Exceptions for the package go here
3
+
4
+ """
5
+
6
+
7
+ class Error(Exception):
8
+ pass
9
+
10
+
11
+ class OpenidResolutionError(Error):
12
+ pass
13
+
14
+
15
+ class DuplicateHashError(Error):
16
+ pass
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.