├── .gitignore ├── .travis.yml ├── HISTORY.md ├── LICENSE.txt ├── README.md ├── erddapClient ├── __init__.py ├── erddap_constants.py ├── erddap_dataset.py ├── erddap_griddap.py ├── erddap_griddap_dimensions.py ├── erddap_server.py ├── erddap_tabledap.py ├── formatting.py ├── parse_utils.py ├── remote_requests.py └── url_operations.py ├── notebooks ├── ERDDAP-Server-Search-Operations.ipynb ├── ERDDAP-client-demo.ipynb ├── ERDDAP-status-page-metrics.ipynb ├── ERDDAP_Griddap-DEMO.ipynb ├── ERDDAP_Tabledap-DEMO.ipynb ├── Griddap-demo.ipynb ├── NDBC-Analysis.ipynb └── roi-masking-griddap.ipynb ├── requirements.txt ├── setup.py └── tests ├── cassettes ├── test_griddap_dimensions.yaml ├── test_griddap_dimensions_creation.yaml ├── test_griddap_setSubset_query.yaml ├── test_griddap_subset_parsing.yaml ├── test_parsestatus.yaml └── test_remote_connection.yaml ├── test_erddap_dataset.py ├── test_erddap_griddap.py ├── test_erddap_griddap_dimensions.py ├── test_erddap_server.py └── test_parsing_functions.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg* 2 | erddapClient/__pycache__ 3 | tests/__pycache__ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | jobs: 3 | include: 4 | - name: "Python 3.6 Linux" 5 | python: "3.6" 6 | # command to install dependencies 7 | install: 8 | - pip install -r requirements.txt 9 | - pip install pytest-vcr 10 | - python setup.py install 11 | # command to run tests 12 | script: 13 | - pytest tests 14 | - name: "Python 3.6 in Windows" 15 | os: windows 16 | language: shell 17 | before_install: 18 | - choco install python --version 3.6 19 | env: PATH=/c/Python36:/c/Python36/Scripts:$PATH 20 | install: 21 | - python -m pip install --upgrade pip 22 | - python -m pip install -r requirements.txt 23 | - python -m pip install pytest-vcr 24 | - python setup.py install 25 | script: 26 | - pytest tests 27 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Package history 2 | 3 | ## Version new 4 | 5 | - Bug fix: The search method could not receive a phrase to search, using double quotes in phrase, i.e. "phrase to search" 6 | 7 | ## Version 1.0.0 8 | 9 | - Releasing a stable version of erddap-python, growing community backed. 10 | - Added slicing capabilities to ERDDAP_Griddap_Dimension classes. 11 | - Added special methods and properties to ERDDAP_Griddap_Dimension type 'time' 12 | - Validation of opendap request of griddap datasets. 13 | - Added a new way to request griddap subsets using regular slice objects, using integer indexing or real dimensions values, The methods `setSubsetI`, `setSubset`. 14 | 15 | ## Version 0.0.10b 16 | 17 | - Bug fix: MLD timeseries parsing for ERDDAP version 2.12 wasn't working. 18 | 19 | ## Version 0.0.9b 20 | 21 | - Added parse status support for ERDDAP version 2.12, new metrics: nunique_users_since_startup. New columns for MLD timeseries: R_toomany and open_files_percent. 22 | - Included `__version__` number property in erddapClient class 23 | 24 | ## Version 0.0.8b 25 | 26 | - Added methods to parse ERDDAP status.html page, results are stored in DataFrames and scalars. Method is available in erddapClient.ERDDAP_Server.parseStatusPage() 27 | - Bug fix method setConstraints() 28 | 29 | ## Version 0.0.7b 30 | 31 | - Added the methods getxArray and getncDataset that can get subsets of ERDDAP griddap datasets and return xarray or netCDF4.Dataset objects 32 | - Parsing methods for griddap querys of extended opendap format, to integer indexes equivalents. 33 | 34 | ## Version 0.0.6b 35 | 36 | - Removed os.path.join usage to build url strings, that caused the search operations to fail on windows. 37 | - Added test on windows os. 38 | 39 | ## Version 0.0.5b 40 | 41 | - pip package issues fix. 42 | 43 | ## Version 0.0.4b 44 | 45 | - Relocation of getDataFrame method, to base class erddap_dataset, this can be used by griddap_dataset also. 46 | 47 | ## Version 0.0.3b 48 | 49 | - Refactor of private methods 50 | - Improved docstring using pdoc 51 | - Published API reference in [https://hmedrano.github.io/erddap-python/](https://hmedrano.github.io/erddap-python/) 52 | 53 | ## Version 0.0.2b 54 | 55 | - Pytests 56 | - First pip package -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 erddap-python 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ERDDAP python library 2 | 3 | [![Anaconda-Server Badge](https://anaconda.org/conda-forge/erddap-python/badges/version.svg)](https://anaconda.org/conda-forge/erddap-python) 4 | [![image](https://img.shields.io/pypi/v/erddap-python.svg)](https://pypi.python.org/pypi/erddap-python) 5 | [![image](https://pepy.tech/badge/erddap-python)](https://pepy.tech/project/erddap-python) 6 | [![Build Status](https://travis-ci.com/hmedrano/erddap-python.svg?branch=main)](https://travis-ci.com/hmedrano/erddap-python) 7 | [![image](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | 9 | ## About 10 | 11 | [ERDDAP](https://coastwatch.pfeg.noaa.gov/erddap/information.html) is a data server that gives you a simple, consistent way to download subsets of gridded and tabular scientific datasets in common file formats and make graphs and maps. 12 | 13 | erddap-python is a python client for the ERDDAP Restful API, it can obtain server status metrics, provides search methods, gives tabledap and griddap class objects for metadata and data access. 14 | 15 | This library was initially built for [CICESE](https://cicese.edu.mx), [CIGOM](https://cigom.org), [OORCO](https://oorco.org), and [CEMIEOceano](https://cemieoceano.mx/) projects for the automation of reports, interactive custom visualizations and data analysis. Most of the functionality was inspired on the work of [erddapy](https://github.com/ioos/erddapy) library, but designed more for a more flexible backend service construction in mind. 16 | 17 | 18 | Full API reference can be found [here](https://hmedrano.github.io/erddap-python/). 19 | 20 | ## Projects using erddap-python 21 | 22 | - [ERDDAP server's status metrics dashboard using Streamlit](https://share.streamlit.io/hmedrano/erddap-status-dashboard/main/dashboard_streamlit_app.py) 23 | - [Module for Ocean Observatory Data Analysis library](https://github.com/rbardaji/mooda) 24 | 25 | ## Requirements 26 | 27 | - python 3 28 | - python libraries numpy, pandas, xarray, netCDF4 29 | 30 | ## Installation 31 | 32 | Using pip: 33 | 34 | ``` 35 | $ pip install erddap-python 36 | ``` 37 | 38 | Also you can use `conda` package manager, from the `conda-forge` channel: 39 | 40 | ``` 41 | $ conda install -c conda-forge erddap-python 42 | ``` 43 | 44 | ## Usage 45 | 46 | ### Explore a ERDDAP Server 47 | 48 | Connect to a ERDDAP Server, and get results from a basic search. 49 | 50 | ```python 51 | >>> from erddapClient import ERDDAP_Server 52 | >>> 53 | >>> remoteServer = ERDDAP_Server('https://coastwatch.pfeg.noaa.gov/erddap') 54 | >>> remoteServer 55 | 56 | Server version: ERDDAP_version=2.11 57 | ``` 58 | 59 | [search](https://hmedrano.github.io/erddap-python/#ERDDAP_Server.search) and [advancedSerch](https://hmedrano.github.io/erddap-python/#ERDDAP_Server.advancedSearch) methods are available, it builds the search request URL and also can 60 | make the request to the ERDDAP restful services to obtain results. 61 | 62 | ```python 63 | >>> searchRequest = remoteServer.search(searchFor="gliders") 64 | >>> searchRequest 65 | 66 | Results: 1 67 | [ 68 | 0 - scrippsGliders , "Gliders, Scripps Institution of Oceanography, 2014-present" 69 | ] 70 | ``` 71 | 72 | The methods returns an object with a list of the [ERDDAP_Tabledap](https://hmedrano.github.io/erddap-python/#ERDDAP_Tabledap) or [ERDDAP_Griddap](https://hmedrano.github.io/erddap-python/#ERDDAP_Griddap) objects that matched the search criteria. 73 | 74 | ### Connect to Tabledap datasets 75 | 76 | 77 | Using the [ERDDAP_Tabledap](https://hmedrano.github.io/erddap-python/#ERDDAP_Tabledap) class you can construct ERDDAP data request URL's 78 | 79 | ```python 80 | 81 | >>> from erddapClient import ERDDAP_Tabledap 82 | >>> 83 | >>> remote = ERDDAP_Tabledap('https://coastwatch.pfeg.noaa.gov/erddap', 'cwwcNDBCMet') 84 | >>> 85 | >>> remote.setResultVariables(['station','time','atmp']) 86 | >>> print (remote.getURL('htmlTable')) 87 | 88 | 'https://coastwatch.pfeg.noaa.gov/erddap/tabledap/cwwcNDBCMet.htmlTable?station%2Ctime%2Catmp' 89 | 90 | ``` 91 | 92 | The tabledap object internally stores a stack for the result variables, constrainsts and server side operations. You 93 | can keep adding them and get the different urls. 94 | 95 | ```python 96 | >>> import datetime as dt 97 | >>> 98 | >>> remote.addConstraint('time>=2020-12-29T00:00:00Z') \ 99 | ..: .addConstraint({ 'time<=' : dt.datetime(2020,12,31) }) 100 | >>> remote.getURL() 101 | 102 | 'https://coastwatch.pfeg.noaa.gov/erddap/tabledap/cwwcNDBCMet.csvp?station%2Ctime%2Catmp&time%3E=2020-12-29T00%3A00%3A00Z&time%3C=2020-12-31T00%3A00%3A00Z' 103 | 104 | >>> 105 | >>> remote.orderByClosest(['station','time/1day']) 106 | >>> remote.getURL() 107 | 108 | 'https://coastwatch.pfeg.noaa.gov/erddap/tabledap/cwwcNDBCMet.csvp?station%2Ctime%2Catmp&time%3E=2020-12-29T00%3A00%3A00Z&time%3C=2020-12-31T00%3A00%3A00Z&orderByClosest(%22station%2Ctime/1day%22)' 109 | 110 | >>> 111 | ``` 112 | 113 | The class has methods to clear the result variables, the constraints, and the server side operations that are added in the stack: [clearConstraints()](https://hmedrano.github.io/erddap-python/#ERDDAP_Dataset.clearConstraints), [clearResultVariable()](https://hmedrano.github.io/erddap-python/#ERDDAP_Dataset.clearResultVariables), [clearServerSideFunctions()](https://hmedrano.github.io/erddap-python/#ERDDAP_Dataset.clearServerSideFunctions) or [clearQuery()](https://hmedrano.github.io/erddap-python/#ERDDAP_Dataset.clearQuery). 114 | 115 | #### Tabledap data subset request 116 | 117 | An user can build the data request query by chaining the result variables, constraints and server side adding methods. And at the end you can make the data request in all the available formats that ERDDAP provides (csv, mat, json, nc, etc). 118 | 119 | ```python 120 | >>> 121 | >>> remote.clearQuery() 122 | >>> 123 | >>> responseCSV = ( 124 | ..: remote.setResultVariables(['station','time','atmp']) 125 | ..: .addConstraint('time>=2020-12-29T00:00:00Z') 126 | ..: .addConstraint('time<=2020-12-31T00:00:00Z') 127 | ..: .orderByClosest(['station','time/1day']) 128 | ..: .getData('csvp') 129 | ..: ) 130 | >>> 131 | >>> print(responseCSV) 132 | 133 | station,time (UTC),atmp (degree_C) 134 | 41001,2020-12-29T00:00:00Z,17.3 135 | 41001,2020-12-30T00:00:00Z,13.7 136 | 41001,2020-12-31T00:00:00Z,15.9 137 | 41004,2020-12-29T00:10:00Z,18.1 138 | 41004,2020-12-30T00:00:00Z,17.1 139 | 41004,2020-12-31T00:00:00Z,21.2 140 | 41008,2020-12-29T00:50:00Z,14.8 141 | ... 142 | . 143 | 144 | >>> 145 | >>> remote.clearQuery() 146 | >>> 147 | >>> responsePandas = ( 148 | ..: remote.setResultVariables(['station','time','atmp']) 149 | ..: .addConstraint('time>=2020-12-29T00:00:00Z') 150 | ..: .addConstraint('time<=2020-12-31T00:00:00Z') 151 | ..: .orderByClosest(['station','time/1day']) 152 | ..: .getDataFrame() 153 | ..: ) 154 | >>> 155 | >>> responsePandas 156 | 157 | station time (UTC) atmp (degree_C) 158 | 0 41001 2020-12-29T00:00:00Z 17.3 159 | 1 41001 2020-12-30T00:00:00Z 13.7 160 | 2 41001 2020-12-31T00:00:00Z 15.9 161 | 3 41004 2020-12-29T00:00:00Z 18.2 162 | 4 41004 2020-12-30T00:00:00Z 17.1 163 | ... ... ... ... 164 | 2006 YKRV2 2020-12-30T00:00:00Z NaN 165 | 2007 YKRV2 2020-12-31T00:00:00Z 8.1 166 | 2008 YKTV2 2020-12-29T00:00:00Z 11.3 167 | 2009 YKTV2 2020-12-30T00:00:00Z NaN 168 | 2010 YKTV2 2020-12-31T00:00:00Z 7.1 169 | 170 | [2011 rows x 3 columns] 171 | 172 | 173 | ``` 174 | 175 | 176 | ### Griddap datasets 177 | 178 | All the url building functions, and data request functionality is available in the [ERDDAP_Griddap](https://hmedrano.github.io/erddap-python/#ERDDAP_Griddap) class. 179 | 180 | With this class you can download data subsets in all the available ERDDAP data formats, plus the posibility to request a fully described xarray.DataArrays objects. 181 | 182 | This class can parse the griddap query, and detect if the query is malformed before requesting data from the 183 | ERDDAP server. 184 | 185 | Usage sample: 186 | 187 | ```python 188 | >>> from erddapClient import ERDDAP_Griddap 189 | >>> 190 | >>> remote = ERDDAP_Griddap('https://coastwatch.pfeg.noaa.gov/erddap', 'hycom_gom310D') 191 | >>> 192 | >>> print(remote) 193 | 194 | 195 | Title: NRL HYCOM 1/25 deg model output, Gulf of Mexico, 10.04 Expt 31.0, 2009-2014, At Depths 196 | Server URL: https://coastwatch.pfeg.noaa.gov/erddap 197 | Dataset ID: hycom_gom310D 198 | Dimensions: 199 | time (double) range=(cftime.DatetimeGregorian(2009, 4, 2, 0, 0, 0, 0), cftime.DatetimeGregorian(2014, 8, 30, 0, 0, 0, 0)) 200 | Standard name: time 201 | Units: seconds since 1970-01-01T00:00:00Z 202 | depth (float) range=(0.0, 5500.0) 203 | Standard name: depth 204 | Units: m 205 | latitude (float) range=(18.09165, 31.96065) 206 | Standard name: latitude 207 | Units: degrees_north 208 | longitude (float) range=(-98.0, -76.40002) 209 | Standard name: longitude 210 | Units: degrees_east 211 | Variables: 212 | temperature (float) 213 | Standard name: sea_water_potential_temperature 214 | Units: degC 215 | salinity (float) 216 | Standard name: sea_water_practical_salinity 217 | Units: psu 218 | u (float) 219 | Standard name: eastward_sea_water_velocity 220 | Units: m/s 221 | v (float) 222 | Standard name: northward_sea_water_velocity 223 | Units: m/s 224 | w_velocity (float) 225 | Standard name: upward_sea_water_velocity 226 | Units: m/s 227 | ``` 228 | 229 | Right after creating the griddap object you can explore the dimensions information. 230 | 231 | ```python 232 | >>> print(remote.dimensions) 233 | 234 | 235 | Dimensions: 236 | - time (nValues=1977) 1238630400 .. 1409356800 237 | - depth (nValues=40) 0.0 .. 5500.0 238 | - latitude (nValues=385) 18.091648 .. 31.960648 239 | - longitude (nValues=541) -98.0 .. -76.400024 240 | 241 | >>> print(remote.dimensions['time']) 242 | 243 | 244 | Dimension: time 245 | _nValues : 1977 246 | _evenlySpaced : True 247 | _averageSpacing : 1 day 248 | _dataType : double 249 | _CoordinateAxisType : Time 250 | actual_range : (cftime.DatetimeGregorian(2009, 4, 2, 0, 0, 0, 0), cftime.DatetimeGregorian(2014, 8, 30, 0, 0, 0, 0)) 251 | axis : T 252 | calendar : standard 253 | ioos_category : Time 254 | long_name : Time 255 | standard_name : time 256 | time_origin : 01-JAN-1970 00:00:00 257 | units : seconds since 1970-01-01T00:00:00Z 258 | ``` 259 | 260 | #### Griddap data request in a xarray.DataArray 261 | 262 | Request a data subset and store it in a fully described xarray.DataArray object. 263 | 264 | ```python 265 | 266 | >>> xSubset = ( remote.setResultVariables('temperature') 267 | ..: .setSubset(time="2012-01-13", 268 | ..: depth=slice(0,2000), 269 | ..: latitude=slice(18.09165, 31.96065), 270 | ..: longitude=slice(-98.0,-76.40002)) 271 | ..: .getxArray() ) 272 | 273 | >>> xSubset 274 | 275 | 276 | Dimensions: (depth: 33, latitude: 385, longitude: 541, time: 1) 277 | Coordinates: 278 | * time (time) object 2012-01-13 00:00:00 279 | * depth (depth) float64 0.0 5.0 10.0 15.0 ... 1.5e+03 1.75e+03 2e+03 280 | * latitude (latitude) float64 18.09 18.13 18.17 ... 31.89 31.93 31.96 281 | * longitude (longitude) float64 -98.0 -97.96 -97.92 ... -76.48 -76.44 -76.4 282 | Data variables: 283 | temperature (time, depth, latitude, longitude) float32 ... 284 | Attributes: (12/32) 285 | cdm_data_type: Grid 286 | Conventions: COARDS, CF-1.0, ACDD-1.3 287 | creator_email: hycomdata@coaps.fsu.edu 288 | creator_name: Naval Research Laboratory 289 | creator_type: institution 290 | creator_url: https://www.hycom.org 291 | ... ... 292 | standard_name_vocabulary: CF Standard Name Table v70 293 | summary: NRL HYCOM 1/25 deg model output, Gulf of Mexi... 294 | time_coverage_end: 2014-08-30T00:00:00Z 295 | time_coverage_start: 2009-04-02T00:00:00Z 296 | title: NRL HYCOM 1/25 deg model output, Gulf of Mexi... 297 | Westernmost_Easting: -98.0 298 | 299 | ``` 300 | 301 | The above data request can also be done using the ERDDAP opendap extended query format, by example : 302 | 303 | ```python 304 | >>> xSubset = ( remote.setResultVariables('temperature[(2012-01-13)][(0):(2000)][(18.09165):(31.96065)][(-98.0):(-76.40002)]') 305 | ..: .getxArray() 306 | ``` 307 | 308 | #### Make request for subsets in different formats. 309 | 310 | Request a location timeseires and store it in a pandas dataframe, using the [getDataFrame](https://hmedrano.github.io/erddap-python/#ERDDAP_Dataset.getDataFrame) method. 311 | 312 | ```python 313 | >>> # 314 | >>> 315 | >>> remote.clearQuery() 316 | >>> dfSubset = ( remote.setResultVariables(['temperature','salinity']) 317 | ..: .setSubset(time=slice("2009-04-02","2014-8-30"), 318 | ..: depth=0, 319 | ..: latitude=22.5, 320 | ..: longitude=-95.5) 321 | ..: .getDataFrame(header=0, 322 | ..: names=['time','depth','latitude','longitude', 'temperature', 'salinity'], 323 | ..: parse_dates=['time'], 324 | ..: index_col='time') ) 325 | 326 | >>> dfSubset 327 | 328 | depth latitude longitude temperature salinity 329 | time 330 | 2009-04-02 00:00:00+00:00 0.0 22.51696 -95.47998 24.801798 36.167076 331 | 2009-04-03 00:00:00+00:00 0.0 22.51696 -95.47998 24.605570 36.256450 332 | 2009-04-04 00:00:00+00:00 0.0 22.51696 -95.47998 24.477884 36.086346 333 | 2009-04-05 00:00:00+00:00 0.0 22.51696 -95.47998 24.552357 36.133224 334 | 2009-04-06 00:00:00+00:00 0.0 22.51696 -95.47998 25.761946 36.179676 335 | ... ... ... ... ... ... 336 | 2014-08-26 00:00:00+00:00 0.0 22.51696 -95.47998 30.277546 36.440037 337 | 2014-08-27 00:00:00+00:00 0.0 22.51696 -95.47998 30.258907 36.485844 338 | 2014-08-28 00:00:00+00:00 0.0 22.51696 -95.47998 30.298597 36.507530 339 | 2014-08-29 00:00:00+00:00 0.0 22.51696 -95.47998 30.246874 36.493400 340 | 2014-08-30 00:00:00+00:00 0.0 22.51696 -95.47998 30.387840 36.487934 341 | 342 | [1977 rows x 5 columns] 343 | 344 | >>> 345 | 346 | ``` 347 | 348 | ---- 349 | 350 | ## Sample notebooks 351 | 352 | Check the demostration [notebooks folder](https://github.com/hmedrano/erddap-python/tree/main/notebooks) for more usage examples of the library classes. 353 | -------------------------------------------------------------------------------- /erddapClient/__init__.py: -------------------------------------------------------------------------------- 1 | from erddapClient.erddap_server import ERDDAP_Server 2 | from erddapClient.erddap_dataset import ERDDAP_Dataset 3 | from erddapClient.erddap_tabledap import ERDDAP_Tabledap 4 | from erddapClient.erddap_griddap import ERDDAP_Griddap 5 | from erddapClient.erddap_griddap_dimensions import ERDDAP_Griddap_dimensions, ERDDAP_Griddap_dimension 6 | 7 | __all__ = ["ERDDAP_Server", "ERDDAP_Dataset", "ERDDAP_Tabledap", "ERDDAP_Griddap", "ERDDAP_Griddap_dimensions", "ERDDAP_Griddap_dimension"] 8 | 9 | __version__ = "1.0.0" -------------------------------------------------------------------------------- /erddapClient/erddap_constants.py: -------------------------------------------------------------------------------- 1 | 2 | ERDDAP_TIME_UNITS = 'seconds since 1970-01-01T00:00:00Z' 3 | 4 | ERDDAP_DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ' 5 | 6 | class ERDDAP_Metadata_Rows: 7 | ROW_TYPE = 0 8 | VARIABLE_NAME = 1 9 | ATTRIBUTE_NAME = 2 10 | DATA_TYPE = 3 11 | VALUE = 4 12 | 13 | class ERDDAP_Search_Results_Rows: 14 | GRIDDAP = 0 15 | SUBSET = 1 16 | TABLEDAP = 2 17 | MAKEAGRAPH = 3 18 | WMS = 4 19 | FILES = 5 20 | ACCESIBLE = 6 21 | TITLE = 7 22 | SUMMARY = 8 23 | FGDC = 9 24 | ISO19115 = 10 25 | INFO = 11 26 | BACKINFO = 12 27 | RSS = 13 28 | EMAIL = 14 29 | INSTITUTION = 15 30 | DATASETID = 16 -------------------------------------------------------------------------------- /erddapClient/erddap_dataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | from erddapClient import url_operations 3 | from erddapClient.remote_requests import urlread 4 | from erddapClient.parse_utils import parseDictMetadata, parseConstraintValue 5 | from erddapClient.formatting import dataset_str, simple_dataset_repr 6 | import datetime as dt 7 | import pandas as pd 8 | from io import StringIO 9 | 10 | 11 | class ERDDAP_Dataset: 12 | """ 13 | Class to represent the shared attributes and methods of a ERDDAP Dataset 14 | either a griddap or tabledap. 15 | 16 | """ 17 | 18 | DEFAULT_FILETYPE = 'csvp' 19 | BINARY_FILETYPES = [ 'dods', 'mat', 'nc', 'ncCF', 'ncCFMA', 'wav', 20 | 'smallPdf', 'pdf', 'largePdf', 21 | 'smallPng', 'png', 'largePng', 'transparentPng'] 22 | 23 | def __init__(self, erddapurl, datasetid, protocol='tabledap', auth=None, lazyload=True): 24 | self.erddapurl = erddapurl 25 | self.datasetid = datasetid 26 | self.protocol = protocol 27 | self.erddapauth = auth 28 | 29 | self.__metadata = None 30 | 31 | self.resultVariables = [] 32 | self.constraints = [] 33 | self.serverSideFunctions = [] 34 | 35 | if not lazyload: 36 | self.loadMetadata() 37 | 38 | 39 | def __str__(self): 40 | return dataset_str(self) 41 | 42 | def __simple_repr__(self): 43 | return simple_dataset_repr(self) 44 | 45 | 46 | def setResultVariables(self, variables): 47 | """ 48 | This function sets the optional comma-separated list of variables 49 | called "resultsVariables" as part of the query for data request. 50 | (for example: longitude,latitude,time,station,wmo_platform_code,T_25). 51 | For each variable in resultsVariables, there will be a column in the 52 | results table, in the same order. If you don't specify any results 53 | variables, the results table will include columns for all of the 54 | variables in the dataset. 55 | 56 | Arguments 57 | 58 | `variables` : The list of variables, this can be a string with the 59 | comma separated variables, or a list. 60 | 61 | Returns the current object allowing chaining functions. 62 | 63 | """ 64 | if type(variables) is list: 65 | self.resultVariables = variables 66 | elif type(variables) is str: 67 | self.resultVariables = variables.split(',') 68 | self.resultVariables = [ rv.strip() for rv in self.resultVariables] 69 | else: 70 | raise Exception("variables argument must be list, or comma separated list of variables") 71 | 72 | return self 73 | 74 | 75 | def addResultVariable(self, variable): 76 | """ 77 | Adds a variable to the data request query element "resultsVariables" 78 | 79 | Arguments 80 | 81 | `variable` : A string with the variable name add. 82 | """ 83 | self.resultVariables.append(variable) 84 | return self 85 | 86 | 87 | def setConstraints(self, constraintListOrDict): 88 | """ 89 | This functions sets the constraints for the data request query. 90 | 91 | More on ERDDAP constraints: https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#query 92 | 93 | """ 94 | self.clearConstraints() 95 | self.addConstraints(constraintListOrDict) 96 | return self 97 | 98 | 99 | def addConstraints(self, constraintListOrDict): 100 | if isinstance(constraintListOrDict,dict): 101 | for k,v in constraintListOrDict.items(): 102 | self.addConstraint({k:v}) 103 | elif isinstance(constraintListOrDict,list): 104 | for constraint in constraintListOrDict: 105 | self.addConstraint(constraint) 106 | else: 107 | raise Exception("Constraints argument must be either dictionary or list") 108 | return self 109 | 110 | 111 | def addConstraint(self, constraint): 112 | """ 113 | This adds a constraint to the data request query. 114 | 115 | Arguments 116 | 117 | `constraint` : This can be a string with the constraint, or a dictionary 118 | element, being the key the first part of the constraint, and 119 | the dict value the constraint value. 120 | 121 | Example: 122 | ``` 123 | >>> dataset.addConstraint('time>=2020-12-29T00:00:00Z') 124 | >>> dataset.addConstraint({ 'time>=' : dt.datetime(2020,12,29) }) 125 | ``` 126 | 127 | More on ERDDAP constraints: https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#query 128 | """ 129 | if isinstance(constraint,dict): 130 | self._addConstraintDict(constraint) 131 | elif isinstance(constraint,str): 132 | self._addConstraintStr(constraint) 133 | else: 134 | raise Exception("constraint argument must be either string or a dictionary") 135 | return self 136 | 137 | 138 | def _addConstraintStr(self, constraint): 139 | self.constraints.append(constraint) 140 | 141 | 142 | def _addConstraintDict(self, constraintDict): 143 | constraintKey = next(iter(constraintDict)) 144 | self._addConstraintStr( 145 | "{key_plus_conditional}{value}".format( 146 | key_plus_conditional=constraintKey, 147 | value=parseConstraintValue(constraintDict[constraintKey]) 148 | ) 149 | ) 150 | 151 | 152 | def getDataRequestURL(self, filetype=DEFAULT_FILETYPE, useSafeURL=True): 153 | requestURL = self.getBaseURL(filetype) 154 | query = "" 155 | 156 | if len(self.resultVariables) > 0: 157 | query += url_operations.parseQueryItems(self.resultVariables, useSafeURL, safe='', item_separator=',') 158 | 159 | if len(self.constraints) > 0: 160 | query += '&' + url_operations.parseQueryItems(self.constraints, useSafeURL, safe='=!()&') 161 | 162 | if len(self.serverSideFunctions) > 0: 163 | query += '&' + url_operations.parseQueryItems(self.serverSideFunctions, useSafeURL, safe='=!()&/') 164 | 165 | requestURL = url_operations.joinURLElements(requestURL, query) 166 | 167 | self.lastRequestURL = requestURL 168 | return self.lastRequestURL 169 | 170 | 171 | def getURL(self, filetype=DEFAULT_FILETYPE, useSafeURL=True): 172 | """ 173 | Buils and returns a strint with the data request query, with the available 174 | resultsVariables, constraints and operations provided. 175 | """ 176 | return self.getDataRequestURL(filetype, useSafeURL) 177 | 178 | 179 | def getBaseURL(self, filetype=DEFAULT_FILETYPE): 180 | if filetype.lower() == 'opendap': 181 | return url_operations.url_join(self.erddapurl, self.protocol, self.datasetid ) 182 | else: 183 | return url_operations.url_join(self.erddapurl, self.protocol, self.datasetid + "." + filetype ) 184 | 185 | 186 | def getAttribute(self, attribute, variableName='NC_GLOBAL'): 187 | """ 188 | Returns the value for a attribute name in the dataset. If the metadata of the 189 | dataset is not already in memory, this functions will load it, calling the 190 | function `erddapClient.ERDDAP_Dataset.loadMetadata` 191 | 192 | """ 193 | self.loadMetadata() 194 | if variableName == 'NC_GLOBAL': 195 | if attribute in self.__metadata['global'].keys(): 196 | return self.__metadata['global'][attribute] 197 | else: 198 | for vd in [ self.__metadata['dimensions'], self.__metadata['variables'] ]: 199 | if variableName in vd.keys(): 200 | if attribute in vd[variableName].keys(): 201 | return vd[variableName][attribute] 202 | 203 | 204 | def loadMetadata(self, force=False): 205 | """ 206 | Loads in to memory the metadata atributes and values available in the info 207 | page of the dataset. 208 | 209 | Arguments: 210 | 211 | `force` : If true, this method will reload the metadata attributes 212 | even if the information where already downloaded. 213 | """ 214 | if self.__metadata is None or force: 215 | rawRequest = urlread(self.getMetadataURL(), auth=self.erddapauth) 216 | rawRequestJson = rawRequest.json() 217 | self.__metadata = parseDictMetadata(rawRequestJson) 218 | return True 219 | 220 | @property 221 | def variables(self): 222 | """ 223 | Returns list of variables of this dataset, and it's associate metadata 224 | """ 225 | self.loadMetadata() 226 | return self.__metadata['variables'] 227 | 228 | @property 229 | def dimensions(self): 230 | """ 231 | Returns a list of the dataset dimensions, and it's associate metadata 232 | """ 233 | self.loadMetadata() 234 | return self.__metadata['dimensions'] 235 | 236 | @property 237 | def info(self): 238 | """ 239 | Returns a list of the global metadata of this dataset. 240 | """ 241 | self.loadMetadata() 242 | return self.__metadata['global'] 243 | 244 | 245 | def getMetadataURL(self, filetype='json'): 246 | """ 247 | Returns a string with the url to request the metadata for the dataset. 248 | 249 | Arguments 250 | 251 | `filetype` : The filetype for the metadata request, defaults to 'json' 252 | 253 | """ 254 | return url_operations.url_join(self.erddapurl, "info", self.datasetid , "index." + filetype ) 255 | 256 | 257 | def clearConstraints(self): 258 | """ 259 | Clears from the constrains stack all the constraints provided by the 260 | `erddapClient.ERDDAP_Dataset.setConstraints`, `erddapClient.ERDDAP_Dataset.addConstraint` 261 | methods. 262 | 263 | """ 264 | self.constraints = [] 265 | 266 | 267 | def clearServerSideFunctions(self): 268 | """ 269 | Clears from the server side functions stack all the functions provided by the 270 | methods available, like `erddapClient.ERDDAP_Tabledap.orderBy`, 271 | `erddapClient.ERDDAP_Tabledap.orderByClosest`, etc. 272 | 273 | """ 274 | self.serverSideFunctions = [] 275 | 276 | 277 | def clearResultVariables(self): 278 | """ 279 | Clears from the results variables stack all the variabkles provided by the 280 | `erddapClient.ERDDAP_Dataset.setResultsVariables` and 281 | `erddapClient.ERDDAP_Dataset.addResultVariable` methods 282 | 283 | """ 284 | self.resultVariables = [] 285 | 286 | 287 | def clearQuery(self): 288 | """ 289 | Clears all the query elements of the stack, variables, constraints and 290 | server side variables. 291 | 292 | """ 293 | self.clearConstraints() 294 | self.clearServerSideFunctions() 295 | self.clearResultVariables() 296 | 297 | 298 | def getData(self, filetype=DEFAULT_FILETYPE, request_kwargs={}): 299 | """ 300 | Makes a data request to the ERDDAP server, the request url is build 301 | using the `erddapClient.ERDDAP_Dataset.getURL` function. 302 | 303 | Aditional request arguments for the urlread function can be provided 304 | as kwargs in this function. 305 | 306 | Returns either a string or a binary format (`erddapClient.ERDDAP_Dataset.BINARY_FILETYPES`) 307 | depending on the filetype specified in the query. 308 | 309 | """ 310 | rawRequest = urlread(self.getDataRequestURL(filetype), auth=self.erddapauth, **request_kwargs) 311 | if filetype in self.BINARY_FILETYPES: 312 | return rawRequest.content 313 | else: 314 | return rawRequest.text 315 | 316 | def getDataFrame(self, request_kwargs={}, **kwargs): 317 | """ 318 | This method makes a data request to the ERDDAP server in csv format 319 | then convert it to a pandas object. 320 | 321 | The pandas object is created using the read_csv method, and 322 | additional arguments for this method can be provided as kwargs in this 323 | method. 324 | 325 | Returns the pandas DataFrame object. 326 | """ 327 | csvpdata = self.getData('csvp', **request_kwargs) 328 | return pd.read_csv(StringIO(csvpdata), **kwargs) 329 | -------------------------------------------------------------------------------- /erddapClient/erddap_griddap.py: -------------------------------------------------------------------------------- 1 | from erddapClient.erddap_dataset import ERDDAP_Dataset 2 | from erddapClient.erddap_griddap_dimensions import ERDDAP_Griddap_dimensions, ERDDAP_Griddap_dimension 3 | from erddapClient import url_operations 4 | from erddapClient.formatting import griddap_str 5 | from erddapClient.parse_utils import parseTimeRangeAttributes, parse_griddap_resultvariables_slices, is_slice_element_opendap_extended, get_value_from_opendap_extended_slice_element, validate_iso8601, validate_float, validate_int, validate_last_keyword, iso8601STRtoNum, extractVariableName 6 | from erddapClient.erddap_constants import ERDDAP_TIME_UNITS, ERDDAP_DATETIME_FORMAT 7 | from collections import OrderedDict 8 | from netCDF4 import Dataset, date2num 9 | import datetime as dt 10 | import numpy as np 11 | import pandas as pd 12 | import xarray as xr 13 | import requests 14 | 15 | 16 | class ERDDAP_Griddap(ERDDAP_Dataset): 17 | """ 18 | Class with the representation and methods for a ERDDAP Griddap Dataset 19 | 20 | """ 21 | 22 | DEFAULT_FILETYPE = 'nc' 23 | 24 | def __init__(self, url, datasetid, auth=None, lazyload=True): 25 | super().__init__(url, datasetid, 'griddap', auth, lazyload=lazyload) 26 | self.__dimensions = None 27 | self.__positional_indexes = None 28 | """ 29 | This property stores the last dimensions slices that builds the subset query. Its used to build opendap 30 | compatible queryes, and to get the dimensions values of the subset. 31 | """ 32 | 33 | def __str__(self): 34 | dst_repr_ = super().__str__() 35 | return dst_repr_ + griddap_str(self) 36 | 37 | 38 | def loadMetadata(self, force=False): 39 | """ 40 | Loads in to memory the metadata atributes and values available in the info 41 | page of the dataset. 42 | 43 | Arguments: 44 | 45 | `force` : If true, this method will reload the metadata attributes 46 | even if the information where already downloaded. 47 | """ 48 | if super().loadMetadata(force): 49 | parseTimeRangeAttributes(self._ERDDAP_Dataset__metadata['dimensions'].items()) 50 | 51 | 52 | @property 53 | def dimensions(self): 54 | self.loadDimensionValues() 55 | return self.__dimensions 56 | 57 | 58 | def clearQuery(self): 59 | super().clearQuery() 60 | self.__positional_indexes = None 61 | 62 | 63 | def loadDimensionValues(self, force=False): 64 | """ 65 | This methods loads from the ERDDAP Server the dimension values 66 | for the current griddap dataset. This values will be used to 67 | calculate integer indexes for opendap requests. 68 | 69 | Arguments: 70 | 71 | `force` : If true, this method will reload the dimensions values 72 | even if the values where already downloaded. 73 | """ 74 | 75 | if self.__dimensions is None or force: 76 | self.loadMetadata() 77 | dimensionVariableNames = list(self._ERDDAP_Dataset__metadata['dimensions'].keys()) 78 | 79 | _resultVars = self.resultVariables 80 | dimensionsData = ( self.setResultVariables(dimensionVariableNames) 81 | .getDataFrame(header=0, names=dimensionVariableNames) ) 82 | self.resultVariables = _resultVars 83 | 84 | self.__dimensions = ERDDAP_Griddap_dimensions() 85 | for dimName in dimensionVariableNames: 86 | 87 | dimDatadroppedNaNs = dimensionsData[dimName].dropna() 88 | if dimName == 'time': 89 | numericDates = np.array([ date2num(dt.datetime.strptime(_dt, ERDDAP_DATETIME_FORMAT), ERDDAP_TIME_UNITS) if (isinstance(_dt,str)) else _dt for _dt in dimDatadroppedNaNs] ) 90 | dimensionSeries = pd.Series( data = np.arange(numericDates.size), index = numericDates) 91 | else: 92 | dimensionSeries = pd.Series( data = dimDatadroppedNaNs.index.values, index = dimDatadroppedNaNs.values) 93 | 94 | dimMeta = self._ERDDAP_Dataset__metadata['dimensions'][dimName] 95 | self.__dimensions[dimName] = ERDDAP_Griddap_dimension(dimName, dimensionSeries, metadata=dimMeta) 96 | 97 | 98 | def getxArray(self, **kwargs_od): 99 | """ 100 | Returns an xarray object subset of the ERDDAP dataset current selection query 101 | 102 | Arguments: 103 | 104 | This method will pass all kwargs to the xarray.open_dataset method. 105 | """ 106 | open_dataset_kwparams = { 'mask_and_scale' : True } # Accept _FillValue, scale_value and add_offset attribute functionality 107 | open_dataset_kwparams.update(kwargs_od) 108 | subsetURL = self.getDataRequestURL(filetype='opendap', useSafeURL=False) 109 | if self.erddapauth: 110 | session = requests.Session() 111 | session.auth = self.erddapauth 112 | store = xr.backends.PydapDataStore.open(subsetURL, 113 | session=session) 114 | _xarray = xr.open_dataset(store, **open_dataset_kwparams) 115 | else: 116 | _xarray = xr.open_dataset(subsetURL, **open_dataset_kwparams) 117 | 118 | # Add extra information to the xarray object, the dimension information. 119 | # Add the subset of the dimensions values to the xarray object 120 | _subset_coords = { dimName : dObj.data[self.__positional_indexes[dimName]] for dimName, dObj in self.dimensions.items() } 121 | if self.dimensions.timeDimension: 122 | _subset_coords[self.dimensions.timeDimension.name] = self.dimensions.timeDimension.timeData[ self.__positional_indexes[self.dimensions.timeDimension.name] ] 123 | _xarray = _xarray.assign_coords(_subset_coords) 124 | # Add attributes to the coordinates 125 | for dimName, dObj in self.dimensions.items(): 126 | _xarray.coords[dimName].attrs = dObj.metadata 127 | 128 | return _xarray 129 | 130 | 131 | def getncDataset(self, **kwargs): 132 | """ 133 | Returns an netCDF4.Dataset object subset of the ERDDAP dataset 134 | 135 | Arguments: 136 | 137 | This method will pass all kwargs to the netCDF4.Dataset method. 138 | """ 139 | subsetURL = (self.getDataRequestURL(filetype='opendap', useSafeURL=False)) 140 | if self.erddapauth: 141 | # TODO Add user, password in URL 142 | _netcdf4Dataset = Dataset(subsetURL, **kwargs) 143 | else: 144 | _netcdf4Dataset = Dataset(subsetURL, **kwargs) 145 | return _netcdf4Dataset 146 | 147 | 148 | def getDataRequestURL(self, filetype=DEFAULT_FILETYPE, useSafeURL=True, resultVariables=None): 149 | """ 150 | Returns the fully built ERDDAP data request url with the available components. 151 | 152 | Arguments: 153 | 154 | `filetype` : The request download format 155 | 156 | `useSafeURL` : If True the query part of the url will be encoded 157 | 158 | `resultVariables` : If None, the self.resultVariables will be used. 159 | 160 | """ 161 | requestURL = self.getBaseURL(filetype) 162 | query = "" 163 | 164 | if resultVariables is None: 165 | resultVariables = self.resultVariables 166 | 167 | if filetype == 'opendap': 168 | self.loadDimensionValues() 169 | if self.__positional_indexes: 170 | resultVariables = self._resultVariablesWithValidDapIndexing() 171 | else: 172 | # resultVariables = self._convertERDDAPSubset2OpendapRegular(resultVariables) 173 | # 174 | resultVariables = self._parseResultVariablesExtendedDapQueryToValidDap(resultVariables) 175 | else: 176 | if self.__positional_indexes: 177 | resultVariables = self._resultVariablesWithValidDapIndexing() 178 | 179 | if len(self.resultVariables) > 0: 180 | query += url_operations.parseQueryItems(resultVariables, useSafeURL, safe='', item_separator=',') 181 | 182 | requestURL = url_operations.joinURLElements(requestURL, query) 183 | 184 | self.lastRequestURL = requestURL 185 | return self.lastRequestURL 186 | 187 | 188 | def setSubset(self, *pdims, **kwdims): 189 | """ 190 | Sets a query subset for griddap request, by using dimension names 191 | 192 | Usage example: 193 | 194 | ``` 195 | dsub = ( remote.setResultVariables(['temperature','salinity']) 196 | .setSubset( time=slice(dt.datetime(2014,6,15), dt.datetime(2014,7,15)), 197 | depth=0, 198 | latitude=slice(18.10, 31.96), 199 | longitude=slice(-98, -76.41)) 200 | .getxArray() ) 201 | ``` 202 | """ 203 | self.__positional_indexes = self.dimensions.subset(*pdims, **kwdims) 204 | return self 205 | 206 | 207 | def setSubsetI(self, *pdims, **kwdims): 208 | """ 209 | Sets a query subset for griddap request, by using its positional 210 | integer index. 211 | 212 | Usage example: 213 | 214 | ``` 215 | dsub = ( remote.setResultVariables(['temperature','salinity']) 216 | .setSubsetI( time=slice(-10,-1), 217 | depth=0, 218 | latitude=slice(10, 150), 219 | longitude=slice(20, 100) ) 220 | .getxArray() ) 221 | ``` 222 | """ 223 | 224 | self.__positional_indexes = self.dimensions.subsetI(*pdims, **kwdims) 225 | return self 226 | 227 | def _parseResultVariablesExtendedDapQueryToValidDap(self, resultVariables): 228 | 229 | """ 230 | This method will receive a string from the resultVariables part of the 231 | ERDDAP griddap request like: 232 | ssh[(2001-06-01T09:00:00Z):(2002-06-01T09:00:00Z)][0:(last-20.3)][last-20:1:last] 233 | 234 | And will return the each result varibel with valid opendap integer indexing query, 235 | ssh[10:20][0:70.7][300:359] 236 | 237 | This operation is done by parsing the subset, obtaining the elements of the 238 | slice that are erddap opendap extended format, the ones between ( ), and converting 239 | the nearest integer index. 240 | 241 | By parsing the subset, this function returns also error messages on a bad formed 242 | query. 243 | 244 | """ 245 | queryComponents = parse_griddap_resultvariables_slices(resultVariables) 246 | 247 | parsedResultVariables = [] 248 | _parsed_positional_indexes = OrderedDict({ dimname : None for dimname, dobj in self.dimensions.items() }) 249 | for qIdx, queryComponent in enumerate(queryComponents): 250 | 251 | if len(queryComponent['sliceComponents']) != 0 and len(self.dimensions) != len(queryComponent['sliceComponents']): 252 | raise Exception('The subset request ({}) must match the number of dimensions ({})'.format(resultVariables[qIdx], self.dimensions.ndims)) 253 | 254 | indexSlice = {'start' : None, 'stop' : None} 255 | indexSliceStr = "" 256 | 257 | for dimOrder, (dimensionName, sliceComponent) in enumerate(zip(self.dimensions.keys(), queryComponent['sliceComponents'])): 258 | 259 | # Check if start of stop components of the slice is opendap extended format, 260 | # The way to detect them is if they are between (dimValue) 261 | for slicePart in ['start', 'stop']: 262 | 263 | if slicePart in sliceComponent and is_slice_element_opendap_extended(sliceComponent[slicePart]): 264 | # In griddap querys, the slice start and stop, can be between ( ), this notation is a extended 265 | # opendap format that uses the dimensions values or special keywords to slice the dimensions. 266 | # More on this: https://coastwatch.pfeg.noaa.gov/erddap/griddap/documentation.html#query 267 | 268 | sliceComponentValue = get_value_from_opendap_extended_slice_element(sliceComponent[slicePart]) 269 | if validate_iso8601(sliceComponentValue): 270 | sliceComponentValueNum = iso8601STRtoNum(sliceComponentValue) 271 | sliceComponentIdx = self.dimensions[dimensionName].closestIdx(sliceComponentValueNum) 272 | 273 | elif validate_float(sliceComponentValue): 274 | sliceComponentValue = float(sliceComponentValue) 275 | sliceComponentIdx = self.dimensions[dimensionName].closestIdx(sliceComponentValue) 276 | 277 | elif validate_int(sliceComponentValue): 278 | sliceComponentValue = int(sliceComponentValue) 279 | sliceComponentIdx = self.dimensions[dimensionName].closestIdx(sliceComponentValue) 280 | 281 | elif validate_last_keyword(sliceComponentValue): 282 | sliceComponentValue2Eval = sliceComponentValue.replace('last',str(self.dimensions[dimensionName].values.index[-1])) 283 | sliceComponentIdx = self.dimensions[dimensionName].closestIdx(eval(sliceComponentValue2Eval)) 284 | else: 285 | raise Exception('Malformed subset : ({}) , couldn\'t parse: ({})'.format(resultVariables[qIdx], sliceComponentValue)) 286 | 287 | else: 288 | # In the slice is not between ( ) , then this means the slice component its using the numeric indexes 289 | # to create the slice. The only special keyword allowed here, its 'last', which means the last numeric 290 | # index in the current dimension. 291 | # More on this: https://coastwatch.pfeg.noaa.gov/erddap/griddap/documentation.html#last 292 | if slicePart in sliceComponent: 293 | sliceComponentValue = sliceComponent[slicePart] 294 | if validate_last_keyword(sliceComponentValue): 295 | sliceComponentValue2Eval = sliceComponentValue.replace('last',str(self.dimensions[dimensionName].values.iloc[-1])) 296 | sliceComponentIdx = int(eval(sliceComponentValue2Eval)) 297 | else: 298 | sliceComponentIdx = int(sliceComponentValue) 299 | 300 | if sliceComponentIdx is None: 301 | raise Exception('Malformed subset : ({}) , The constraint ({}) is out of dimension range: [{}]'.format(resultVariables[qIdx], sliceComponentValue, self.dimensions[dimensionName].range)) 302 | 303 | indexSlice[slicePart] = sliceComponentIdx 304 | #endfor slicePart 305 | 306 | # Build the valid slice object with equivalent numeric indexes in self.__positional_indexes 307 | _sobj = None # slice object, to include to self.__positional_indexes 308 | if 'stride' in sliceComponent: 309 | _sobj = slice( indexSlice['start'], indexSlice['stop'] + 1, int(sliceComponent['stride']) ) 310 | elif 'stop' in sliceComponent: 311 | _sobj = slice(indexSlice['start'], indexSlice['stop'] + 1) 312 | elif 'start' in sliceComponent: 313 | _sobj = slice(indexSlice['start'], indexSlice['start'] + 1) 314 | 315 | _parsed_positional_indexes[dimensionName] = _sobj 316 | 317 | #endfor dimension 318 | self.__positional_indexes = _parsed_positional_indexes 319 | _validDapIndexing = self._convertPositionalIndexes2DapQuery() 320 | 321 | # Join the variable name with the openDap indexing 322 | parsedResultVariables.append(queryComponent['variableName'] + _validDapIndexing) 323 | 324 | #end for queryComponent 325 | 326 | return parsedResultVariables 327 | 328 | 329 | def _convertPositionalIndexes2DapQuery(self): 330 | """ 331 | This function will convert the positional_indexes dictionary of slices, to a string 332 | query type, compatible with the opendap protocol. 333 | 334 | The property __positional_indexes will contain the slices for each dimension in a dict. 335 | 336 | OrderedDict([('time', slice(200, 201, None)), 337 | ('altitude', slice(0, 1, None)), 338 | ('latitude', slice(337, 465, None)), 339 | ('longitude', slice(1018, 1146, None))]) 340 | 341 | This method converts and returns the above to the opendap compatible query string. 342 | 343 | [200:200][0:0][337:464][1018:1145] 344 | 345 | Returns: 346 | - Compatible opendap query string 347 | 348 | """ 349 | 350 | def parseNegativeIndex(nidx, dref): 351 | return nidx if nidx >= 0 else dref.size + nidx 352 | 353 | if self.__positional_indexes is None or all(dimSlice is None for dimName, dimSlice in self.__positional_indexes.items()): 354 | return "" 355 | 356 | validDapIndexing = "" 357 | for dimName, dimSlice in self.__positional_indexes.items(): 358 | if dimSlice is None: 359 | raise Exception("Not a valid slice available for dimension: {} ".format(dimName)) 360 | if not dimSlice.step is None: 361 | _start = parseNegativeIndex(dimSlice.start, self.dimensions[dimName]) 362 | _stop = parseNegativeIndex(dimSlice.stop, self.dimensions[dimName]) 363 | _sc = "[{}:{}:{}]".format(_start, dimSlice.step, _stop-1) 364 | elif not dimSlice.start is None: 365 | _start = parseNegativeIndex(dimSlice.start, self.dimensions[dimName]) 366 | _stop = parseNegativeIndex(dimSlice.stop, self.dimensions[dimName]) 367 | _sc = "[{}:{}]".format(_start, _stop-1) 368 | elif not dimSlice.stop is None: 369 | _stop = parseNegativeIndex(dimSlice.stop, self.dimensions[dimName]) 370 | _sc = "[{}]".format(_stop) 371 | else: 372 | raise Exception("No valid slice available for dimension: {} ".format(dimName)) 373 | validDapIndexing+=_sc 374 | 375 | return validDapIndexing 376 | 377 | 378 | def _resultVariablesWithValidDapIndexing(self): 379 | """ 380 | Returns the opendap query of the variables listed in the resultVariables 381 | property, with valid opendap indexing. 382 | The indexing its built from the current positional_indexes. 383 | """ 384 | 385 | _validDapIndexing = self._convertPositionalIndexes2DapQuery() 386 | validDapQuery = [] 387 | if self.resultVariables is None: 388 | for varName in self.variables.keys(): 389 | validDapQuery.append(varName + _validDapIndexing) 390 | else: 391 | for varName in self.resultVariables: 392 | _justvarname = extractVariableName(varName) 393 | validDapQuery.append(_justvarname + _validDapIndexing) 394 | 395 | return validDapQuery 396 | 397 | 398 | @property 399 | def positional_indexes(self): 400 | return self.__positional_indexes 401 | 402 | @property 403 | def xarray(self): 404 | """ 405 | Returns the xarray object representation of the whoe dataset. Ths method creates the 406 | xarray object by calling the open_dataset method and connecting to the 407 | opendap endpoint that ERDDAP provides. 408 | """ 409 | if not hasattr(self,'__xarray'): 410 | if self.erddapauth: 411 | session = requests.Session() 412 | session.auth = self.erddapauth 413 | store = xr.backends.PydapDataStore.open(self.getBaseURL('opendap'), 414 | session=session) 415 | self.__xarray = xr.open_dataset(store) 416 | else: 417 | self.__xarray = xr.open_dataset(self.getBaseURL('opendap')) 418 | return self.__xarray 419 | 420 | 421 | @property 422 | def ncDataset(self): 423 | """ 424 | Returns the netCDF4.Dataset object representation of the whole dataset. Ths method 425 | creates the Dataset object by calling the Dataset constructor connecting 426 | to the opendap endpoint that ERDDAP provides. 427 | """ 428 | if not hasattr(self,'__netcdf4Dataset'): 429 | if self.erddapauth: 430 | # TODO Add user, password in URL 431 | self.__netcdf4Dataset = Dataset(self.getBaseURL('opendap')) 432 | else: 433 | self.__netcdf4Dataset = Dataset(self.getBaseURL('opendap')) 434 | return self.__netcdf4Dataset 435 | 436 | 437 | -------------------------------------------------------------------------------- /erddapClient/erddap_griddap_dimensions.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from erddapClient.formatting import erddap_dimensions_str, erddap_dimension_str 3 | from erddapClient.parse_utils import iso8601STRtoNum, numtodate, dttonum 4 | import datetime as dt 5 | 6 | 7 | class ERDDAP_Griddap_dimensions(OrderedDict): 8 | """ 9 | Class with the representation and methods for a ERDDAP Griddap 10 | dimensions variables 11 | """ 12 | def __str__(self): 13 | return erddap_dimensions_str(self) 14 | 15 | def __getitem__(self, val): 16 | if isinstance(val, int): 17 | return self[list(self.keys())[val]] 18 | else: 19 | return super().__getitem__(val) 20 | 21 | 22 | def subsetI(self, *pdims, **kwdims): 23 | """ 24 | This method receives slices with the numeric indexes for each dimension. 25 | It will validate if the provided slices are valid and inside the dimension size, just to 26 | warn and avoid further problems when requesting data. 27 | 28 | """ 29 | 30 | def parseSlice(sobj, dref): 31 | estart, estop, estep = None, None, None 32 | if isinstance(sobj, slice): 33 | if sobj.start is None: 34 | estart = None 35 | else: 36 | estart = sobj.start 37 | if estart >= dref.size: 38 | raise Exception("index {} its out of bounds for the dimensions {} with size {}".format(sobj.start, dref.name, dref.size)) 39 | 40 | if sobj.stop is None: 41 | estop = None 42 | else: 43 | estop = sobj.stop 44 | if estop > dref.size: 45 | raise Exception("index stop {} its out of bounds for the dimensions {} with size {}".format(sobj.stop, dref.name, dref.size)) 46 | 47 | estep = sobj.step if not sobj.step is None else None 48 | elif isinstance(sobj, int): 49 | estop = sobj 50 | if estop > dref.size: 51 | raise Exception("index stop {} its out of bounds for the dimensions {} with size {}".format(sobj.stop, dref.name, dref.size)) 52 | else: 53 | raise Exception("Invalid slice format for dimension {}".format(dref.name)) 54 | 55 | if estart is None: 56 | # Deal the ugly case of -1 integer index. An aplied slice(-1) will return a empty subset. 57 | # So set the slice.stop component to the size of the dimension. 58 | if estop == -1: 59 | return slice(estop, dref.size) 60 | else: 61 | return slice(estop, estop + 1) 62 | else: 63 | return slice(estart, estop , estep) 64 | 65 | validDimSlices = OrderedDict( { k : None for k in self.keys() } ) 66 | 67 | # Parse positional arguments, dimensions slices in order 68 | for idx, pdim in enumerate(pdims): 69 | validDimSlices[self[idx].name] = parseSlice(pdim,self[idx]) 70 | 71 | # Parse keyword arguments, dimension names, order not important 72 | for kdim, vdim in kwdims.items(): 73 | validDimSlices[kdim] = parseSlice(vdim, self[kdim]) 74 | 75 | return validDimSlices 76 | 77 | 78 | def subset(self, *pdims, **kwdims): 79 | """ 80 | This method receives slices for the dimensions, parses and returns the numeric 81 | index values, in slice objects. 82 | 83 | Usage example: 84 | ``` 85 | iidx = dimensions.subset(slice("2014-06-15","2014-07-15"), 0.0, slice(18.1,31.96), slice(-98, -76.41)) 86 | # or 87 | iidx = dimensions.subset(time=slice("2014-06-15","2014-07-15"), depth=0.0, latitude=slice(18.1,31.96), longitude=slice(-98, -76.41)) 88 | 89 | # Returns, the integer indexes for the closest inside values of the dimensions 90 | 91 | { time : slice(0:10), depth : slice(0:1), latitude: slice(0:100), longitude : slice(0:200) } 92 | ``` 93 | 94 | """ 95 | 96 | def parseSlice(sobj, dref): 97 | estart, estop, estep = None, None, None 98 | if isinstance(sobj, slice): 99 | if sobj.start is None: 100 | estart = None 101 | else: 102 | estart = dref.closestIdx(sobj.start) 103 | if estart is None: 104 | raise Exception("{} its outside the dimensions values of {}".format(sobj.start, dref.name)) 105 | 106 | if sobj.stop is None: 107 | estop = None 108 | else: 109 | estop = dref.closestIdx(sobj.stop) 110 | if estop is None: 111 | raise Exception("{} its outside the dimensions values of {}".format(sobj.stop, dref.name)) 112 | 113 | estep = sobj.step if not sobj.step is None else None 114 | else: 115 | estop = dref.closestIdx(sobj) 116 | 117 | if estart is None: 118 | return slice(estop, estop + 1) # +1 to make it a valid integer index for python 119 | else: 120 | return slice(estart, estop + 1, estep) 121 | 122 | # 123 | validDimSlices = OrderedDict( { k : None for k in self.keys() } ) 124 | 125 | for idx, pdim in enumerate(pdims): 126 | validDimSlices[self[idx].name] = parseSlice(pdim,self[idx]) 127 | 128 | for kdim, vdim in kwdims.items(): 129 | validDimSlices[kdim] = parseSlice(vdim, self[kdim]) 130 | 131 | return validDimSlices 132 | 133 | 134 | @property 135 | def timeDimension(self): 136 | if 'time' in self.keys(): 137 | return self['time'] 138 | else: 139 | None 140 | 141 | 142 | @property 143 | def ndims(self): 144 | return len(self) 145 | 146 | 147 | 148 | 149 | 150 | 151 | class ERDDAP_Griddap_dimension: 152 | """ 153 | Class with the representation and methods for each ERDDAP Griddap 154 | dimension, for its metadata and values 155 | 156 | """ 157 | def __init__(self, name, values, metadata): 158 | self.name = name 159 | self.values = values 160 | self.metadata = metadata 161 | 162 | def __getitem__(self, val): 163 | return self.values.index[val] 164 | 165 | def __str__(self): 166 | return erddap_dimension_str(self) 167 | 168 | def closestIdx(self, value, method='nearest'): 169 | """ 170 | Returns the integer index that matches the closest 'value' in 171 | dimensions values. 172 | 173 | Arguments: 174 | 175 | `value` : The value to search in the dimension values. If the object 176 | contains a time dimension, this parameter can be a valid ISO 86091 string 177 | or datetime. 178 | 179 | `method` : The argument passed to pandas index.get_loc method 180 | that returns the closest value index. 181 | """ 182 | 183 | if self.isTime and isinstance(value,str): 184 | value = iso8601STRtoNum(value) 185 | elif isinstance(value, dt.datetime): 186 | value = dttonum(value) 187 | 188 | if self.isTime: 189 | rangemin = dttonum(self.metadata['actual_range'][0]) 190 | rangemax = dttonum(self.metadata['actual_range'][1]) 191 | else: 192 | rangemin = self.metadata['actual_range'][0] 193 | rangemax = self.metadata['actual_range'][1] 194 | if value > rangemax or value < rangemin: 195 | return None 196 | idx = self.values.index.get_loc(value, method=method) 197 | return idx 198 | 199 | @property 200 | def info(self): 201 | return self.metadata 202 | 203 | @property 204 | def data(self): 205 | """ 206 | Returns the dimension values 207 | """ 208 | return self.values.index 209 | 210 | @property 211 | def size(self): 212 | """ 213 | Returns dimension lenght 214 | """ 215 | return self.data.size 216 | 217 | @property 218 | def timeData(self): 219 | if self.isTime: 220 | return numtodate(self.data) 221 | 222 | @property 223 | def isTime(self): 224 | return self.name == 'time' 225 | 226 | @property 227 | def range(self): 228 | if 'actual_range' in self.metadata: 229 | return self.metadata['actual_range'] 230 | elif self.name == 'time': 231 | return (numtodate(self.values.index.min()), numtodate(self.values.index.max())) 232 | else: 233 | return (self.values.index.min(), self.values.index.max()) 234 | 235 | 236 | 237 | -------------------------------------------------------------------------------- /erddapClient/erddap_server.py: -------------------------------------------------------------------------------- 1 | import os 2 | from urllib.parse import quote_plus 3 | from erddapClient import url_operations 4 | from erddapClient.formatting import erddap_search_results_repr, erddap_server_repr 5 | from erddapClient.parse_utils import parseConstraintDateTime, parseERDDAPStatusPage, parseNumericVersion 6 | from erddapClient.remote_requests import urlread 7 | from erddapClient.erddap_dataset import ERDDAP_Dataset 8 | from erddapClient.erddap_tabledap import ERDDAP_Tabledap 9 | from erddapClient.erddap_griddap import ERDDAP_Griddap 10 | from erddapClient.erddap_constants import ERDDAP_Metadata_Rows, ERDDAP_Search_Results_Rows 11 | 12 | 13 | 14 | class ERDDAP_Server: 15 | """ 16 | Class with the representation and methods to access a ERDDAP server. 17 | """ 18 | 19 | ALLDATASETS_VARIABLES = [ 'datasetID','accessible','institution','dataStructure', 20 | 'cdm_data_type','class','title','minLongitude','maxLongitude', 21 | 'longitudeSpacing','minLatitude','maxLatitude','latitudeSpacing', 22 | 'minAltitude','maxAltitude','minTime','maxTime','timeSpacing', 23 | 'griddap','subset','tabledap','MakeAGraph','sos','wcs','wms', 24 | 'files','fgdc','iso19115','metadata','sourceUrl','infoUrl', 25 | 'rss','email','testOutOfDate','outOfDate','summary' ] 26 | 27 | def __init__(self, url, auth=None, lazyload=True): 28 | """ 29 | Constructs a ERDDAP Server object 30 | ... 31 | Arguments: 32 | 33 | `url` : The ERDDAP Server URL 34 | 35 | `auth` : Tupple with username and password, to access a protected ERDDAP Server 36 | 37 | """ 38 | self.serverURL = url 39 | self.auth = auth 40 | self.tabledapAllDatasets = ERDDAP_Dataset(self.serverURL, 'allDatasets', auth=auth) 41 | """ An `erddapClient.ERDDAP_Tabledap` object with the reference to the "allDatasets" 42 | Dataset, [About allDatasets](https://coastwatch.pfeg.noaa.gov/erddap/download/setupDatasetsXml.html#EDDTableFromAllDatasets) """ 43 | self.__status_values = None 44 | 45 | def __repr__(self): 46 | return erddap_server_repr(self) 47 | 48 | @property 49 | def version_numeric(self): 50 | if not hasattr(self,'__version_numeric'): 51 | self.__version_numeric = parseNumericVersion(self.version) 52 | return self.__version_numeric 53 | 54 | @property 55 | def version(self): 56 | if not hasattr(self,'__version'): 57 | try: 58 | req = urlread( url_operations.url_join(self.serverURL, 'version'), self.auth) 59 | __version = req.text 60 | self.__version = __version.replace("\n", "") 61 | except: 62 | self.__version = 'ERDDAP_version=<1.22' 63 | return self.__version 64 | 65 | @property 66 | def version_string(self): 67 | if not hasattr(self,'__version_string'): 68 | try: 69 | req = urlread( url_operations.url_join(self.serverURL, 'version_string'), self.auth) 70 | __version_string = req.text 71 | self.__version_string = __version_string.replace("\n", "") 72 | except: 73 | self.__version_string = 'ERDDAP_version_string=<1.80' 74 | return self.__version_string 75 | 76 | 77 | def search(self, **filters): 78 | """ 79 | Makes a search request to the ERDDAP Server 80 | 81 | Search filters kwargs: 82 | 83 | `searchFor` : 84 | 85 | * This is a Google-like search of the datasets metadata: 86 | Type the words you want to search for, with spaces between 87 | the words. ERDDAP will search for the words separately, 88 | not as a phrase. 89 | * To search for a phrase, put double quotes around the 90 | phrase (for example, "wind speed"). 91 | To exclude datasets with a specific word, use 92 | -excludedWord To exclude datasets with a specific 93 | phrase, use -"excluded phrase". 94 | * Don't use AND between search terms. It is implied. The 95 | results will include only the datasets that have all of 96 | the specified words and phrases (and none of the excluded 97 | words and phrases) in the dataset's metadata (data about 98 | the dataset). 99 | * Searches are not case-sensitive. 100 | * To search for specific attribute values, use 101 | attName=attValue . 102 | * To find just grid or just table datasets, include 103 | protocol=griddap or protocol=tabledap in your search. 104 | * This ERDDAP is using searchEngine=original. 105 | * In this ERDDAP, you can search for any part of a word. 106 | * For example, searching for spee will find datasets with 107 | speed and datasets with WindSpeed. 108 | * In this ERDDAP, the last word in a phrase may be a partial 109 | word. For example, to find datasets from a specific website 110 | (usually the start of the datasetID), include (for example) 111 | "datasetID=erd" in your search. 112 | 113 | Optional filters: 114 | 115 | `itemsPerPage` : Set the maximum number of results. (Default: 1000) 116 | 117 | `page` : If the number of results is bigger than the "`itemsPerPage`" you can 118 | specify the page of results. (Default: 1) 119 | 120 | Returns a `erddapClient.ERDDAP_SearchResults` object 121 | """ 122 | 123 | searchURL = self.getSearchURL( **filters) 124 | rawSearchResults = urlread(searchURL, self.auth) 125 | dictSearchResult = rawSearchResults.json() 126 | formatedResults = ERDDAP_SearchResults(self.serverURL, dictSearchResult['table']['rows']) 127 | 128 | return formatedResults 129 | 130 | 131 | def getSearchURL(self, filetype='json', **searchFilters): 132 | """ 133 | Builds the url call for the basic Search ERDDAP API Rest service. 134 | 135 | Arguments 136 | 137 | `filetype` : The result format (htmlTable, csv, json, tsv, etc) 138 | [https://coastwatch.pfeg.noaa.gov/erddap/rest.html#responses](https://coastwatch.pfeg.noaa.gov/erddap/rest.html#responses) 139 | 140 | 141 | Search filters kwargs: 142 | 143 | `searchFor` 144 | 145 | * This is a Google-like search of the datasets metadata: 146 | Type the words you want to search for, with spaces between 147 | the words. ERDDAP will search for the words separately, 148 | not as a phrase. 149 | * To search for a phrase, put double quotes around the 150 | phrase (for example, "wind speed"). 151 | To exclude datasets with a specific word, use 152 | -excludedWord To exclude datasets with a specific 153 | phrase, use -"excluded phrase". 154 | * Don't use AND between search terms. It is implied. The 155 | results will include only the datasets that have all of 156 | the specified words and phrases (and none of the excluded 157 | words and phrases) in the dataset's metadata (data about 158 | the dataset). 159 | * Searches are not case-sensitive. 160 | * To search for specific attribute values, use 161 | attName=attValue . 162 | * To find just grid or just table datasets, include 163 | protocol=griddap or protocol=tabledap in your search. 164 | * This ERDDAP is using searchEngine=original. 165 | * In this ERDDAP, you can search for any part of a word. 166 | * For example, searching for spee will find datasets with 167 | speed and datasets with WindSpeed. 168 | * In this ERDDAP, the last word in a phrase may be a partial 169 | word. For example, to find datasets from a specific website 170 | (usually the start of the datasetID), include (for example) 171 | "datasetID=erd" in your search. 172 | 173 | Optional filters: 174 | 175 | `itemsPerPage` : Set the maximum number of results. (Default: 1000) 176 | 177 | `page` : If the number of results is bigger than the "itemsPerPage" you can 178 | specify the page of results. (Default: 1) 179 | 180 | Returns a string with the url search request. 181 | """ 182 | searchAPIEndpoint = "search/index.{}".format(filetype) 183 | searchAPIURL = url_operations.url_join( self.serverURL, searchAPIEndpoint ) 184 | 185 | queryElementsDefaults = { 'page' : 1 , 186 | 'itemsPerPage' : 1000, 187 | 'searchFor' : None} 188 | queryURL=[] 189 | 190 | for queryElement, queryElementDefault in queryElementsDefaults.items(): 191 | 192 | queryValue = searchFilters.get(queryElement, queryElementDefault) 193 | 194 | if queryElement == 'searchFor': 195 | if queryValue: 196 | queryValue = quote_plus(queryValue, safe='"\'') 197 | queryURL.append( queryElement + "=" + ("" if queryValue is None else queryValue) ) 198 | continue 199 | 200 | if queryValue is None: 201 | queryURL.append( queryElement + "=" ) 202 | else: 203 | queryURL.append( queryElement + "=" + str(queryValue) ) 204 | 205 | return url_operations.joinURLElements(searchAPIURL, url_operations.parseQueryItems(queryURL, safe='=+-&')) 206 | 207 | 208 | def advancedSearch(self, **filters): 209 | """ 210 | Makes a advancedSearch request to the ERDDAP Server 211 | 212 | Search filters kwargs: 213 | 214 | `searchFor` : This is a Google-like search of the datasets metadata, set the words you 215 | want to search for with spaces between the words. ERDDAP will search 216 | for the words separately, not as a phrase. 217 | To search for a phrase, put double quotes around the phrase (for 218 | example, "wind speed"). 219 | To exclude datasets with a specific word, use -excludedWord. 220 | To exclude datasets with a specific phrase, use -"excluded phrase" 221 | To search for specific attribute values, use attName=attValue 222 | To find just grid or table datasets, include protocol=griddap 223 | or protocol=tabledap 224 | 225 | Optional filters: 226 | 227 | `protocolol` : Set either: griddap, tabledap or wms (Default: (ANY)) 228 | 229 | `cdm_data_type` : Set either: grid, timeseries, point, timeseriesProfile, trajectory 230 | trajectoryProfile, etc.. (Default: (ANY)) 231 | 232 | `institution` : Set either to one of the available instituion values in the ERDDAP 233 | (Default: (ANY)) 234 | 235 | `ioos_category` : Set either to one of the available ioos_category values in the ERDDAP 236 | (Default: (ANY)) 237 | 238 | `keywords` : Set either to one of the available keywords values in the ERDDAP 239 | (Default: (ANY)) 240 | 241 | `long_name` : Set either to one of the available long_name values in the ERDDAP 242 | (Default: (ANY)) 243 | 244 | `standard_name` : Set either to one of the available standard_name values in the ERDDAP 245 | (Default: (ANY)) 246 | 247 | `variableName` : Set either to one of the available variable names values in the 248 | ERDDAP (Default: (ANY)) 249 | 250 | `minLon`, `maxLon` : Some datasets have longitude values within -180 to 180, others 251 | use 0 to 360. If you specify Min and Max Longitude within -180 to 180 252 | (or 0 to 360), ERDDAP will only find datasets that match the values 253 | you specify. Consider doing one search: longitude -180 to 360, or 254 | two searches: longitude -180 to 180, and 0 to 360. 255 | 256 | `minLat`, `maxLat` : Set latitude bounds, range -90 to 90 257 | 258 | `minTime`, `maxTime` : Your can pass a object or a string with the following 259 | specifications 260 | 261 | > 262 | - A time string with the format yyyy-MM-ddTHH:mm:ssZ, 263 | for example, 2009-01-21T23:00:00Z. If you specify something, you must include yyyy-MM-dd. 264 | You can omit (backwards from the end) Z, :ss, :mm, :HH, and T. 265 | Always use UTC (GMT/Zulu) time. 266 | - Or specify the number of seconds since 1970-01-01T00:00:00Z. 267 | - Or specify "now-nUnits", for example, "now-7days" 268 | 269 | `itemsPerPage` : Set the maximum number of results. (Default: 1000) 270 | 271 | `page` : If the number of results is bigger than the "`itemsPerPage`" you can 272 | specify the page of results. (Default: 1) 273 | 274 | The search will find datasets that have some data within the specified 275 | time bounds. 276 | 277 | Returns a `erddapClient.ERDDAP_SearchResults` object 278 | """ 279 | 280 | searchURL = self.getAdvancedSearchURL( **filters) 281 | rawSearchResults = urlread(searchURL, self.auth) 282 | dictSearchResult = rawSearchResults.json() 283 | formatedResults = ERDDAP_SearchResults(self.serverURL, dictSearchResult['table']['rows']) 284 | 285 | return formatedResults 286 | 287 | 288 | def getAdvancedSearchURL(self, filetype='json', **searchFilters): 289 | """ 290 | Builds the url call for the advanced Search ERDDAP API Rest service. 291 | 292 | Search filters kwargs: 293 | 294 | `searchFor` : This is a Google-like search of the datasets metadata, set the words you 295 | want to search for with spaces between the words. ERDDAP will search 296 | for the words separately, not as a phrase. 297 | To search for a phrase, put double quotes around the phrase (for 298 | example, "wind speed"). 299 | To exclude datasets with a specific word, use -excludedWord. 300 | To exclude datasets with a specific phrase, use -"excluded phrase" 301 | To search for specific attribute values, use attName=attValue 302 | To find just grid or table datasets, include protocol=griddap 303 | or protocol=tabledap 304 | 305 | Optional filters: 306 | 307 | `protocolol` : Set either: griddap, tabledap or wms (Default: (ANY)) 308 | 309 | `cdm_data_type` : Set either: grid, timeseries, point, timeseriesProfile, trajectory 310 | trajectoryProfile, etc.. (Default: (ANY)) 311 | 312 | `institution` : Set either to one of the available instituion values in the ERDDAP 313 | (Default: (ANY)) 314 | 315 | `ioos_category` : Set either to one of the available ioos_category values in the ERDDAP 316 | (Default: (ANY)) 317 | 318 | `keywords` : Set either to one of the available keywords values in the ERDDAP 319 | (Default: (ANY)) 320 | 321 | `long_name` : Set either to one of the available long_name values in the ERDDAP 322 | (Default: (ANY)) 323 | 324 | `standard_name` : Set either to one of the available standard_name values in the ERDDAP 325 | (Default: (ANY)) 326 | 327 | `variableName` : Set either to one of the available variable names values in the 328 | ERDDAP (Default: (ANY)) 329 | 330 | `minLon`, `maxLon` : Some datasets have longitude values within -180 to 180, others 331 | use 0 to 360. If you specify Min and Max Longitude within -180 to 180 332 | (or 0 to 360), ERDDAP will only find datasets that match the values 333 | you specify. Consider doing one search: longitude -180 to 360, or 334 | two searches: longitude -180 to 180, and 0 to 360. 335 | 336 | `minLat`, `maxLat` : Set latitude bounds, range -90 to 90 337 | 338 | `minTime`, `maxTime` : Your can pass a object or a string with the following 339 | specifications 340 | 341 | > 342 | - A time string with the format yyyy-MM-ddTHH:mm:ssZ, 343 | for example, 2009-01-21T23:00:00Z. If you specify something, you must include yyyy-MM-dd. 344 | You can omit (backwards from the end) Z, :ss, :mm, :HH, and T. 345 | Always use UTC (GMT/Zulu) time. 346 | - Or specify the number of seconds since 1970-01-01T00:00:00Z. 347 | - Or specify "now-nUnits", for example, "now-7days" 348 | 349 | `itemsPerPage` : Set the maximum number of results. (Default: 1000) 350 | 351 | `page` : If the number of results is bigger than the "`itemsPerPage`" you can 352 | specify the page of results. (Default: 1) 353 | 354 | The search will find datasets that have some data within the specified 355 | time bounds. 356 | 357 | Returns the string url for the search service. 358 | 359 | """ 360 | 361 | searchAPIEndpoint = "search/advanced.{}".format(filetype) 362 | searchAPIURL = url_operations.url_join( self.serverURL, searchAPIEndpoint ) 363 | 364 | queryElementsDefaults = { 'page' : 1 , 365 | 'itemsPerPage' : 1000, 366 | 'searchFor' : None, 367 | 'protocol' : "(ANY)", 368 | 'cdm_data_type' : "(ANY)", 369 | 'institution' : "(ANY)", 370 | 'ioos_category' : "(ANY)", 371 | 'keywords' : "(ANY)", 372 | 'long_name' : "(ANY)", 373 | 'standard_name' : "(ANY)", 374 | 'variableName' : "(ANY)", 375 | 'maxLat' : None, 376 | 'minLon' : None, 377 | 'maxLon' : None, 378 | 'minLat' : None, 379 | 'minTime' : None, 380 | 'maxTime' : None} 381 | queryURL=[] 382 | 383 | for queryElement, queryElementDefault in queryElementsDefaults.items(): 384 | 385 | queryValue = searchFilters.get(queryElement, queryElementDefault) 386 | 387 | if queryElement == 'searchFor': 388 | if queryValue: 389 | queryValue = quote_plus(queryValue) 390 | queryURL.append( queryElement + "=" + ("" if queryValue is None else queryValue) ) 391 | continue 392 | 393 | if queryValue is None: 394 | queryURL.append( queryElement + "=" ) 395 | elif queryElement in ['minTime', 'maxTime']: 396 | queryURL.append( queryElement + "=" + parseConstraintDateTime(queryValue) ) 397 | else: 398 | queryURL.append( queryElement + "=" + str(queryValue) ) 399 | 400 | return url_operations.joinURLElements(searchAPIURL, url_operations.parseQueryItems(queryURL, safe='=+-&')) 401 | 402 | 403 | def getQueryAllDatasetsURL(self, filetype='json', constraints=[]): 404 | """ 405 | This method returns a string URL with the allDatasets default 406 | Tabledap Dataset from ERDDAP. 407 | 408 | Arguments: 409 | 410 | `filetype` : The result format for the request 411 | 412 | `constraints` : The request constraints list 413 | 414 | Returns a url string 415 | """ 416 | 417 | resultVariables = self.ALLDATASETS_VARIABLES 418 | response = ( 419 | self.tabledapAllDatasets.setResultVariables(resultVariables) 420 | .setConstraints(constraints) 421 | .getDataRequestURL(filetype=filetype) 422 | ) 423 | return response 424 | 425 | 426 | @property 427 | def statusPageURL(self): 428 | """ 429 | Returns the status.html url for the current ERDDAP Server reference. 430 | """ 431 | if not hasattr(self,'__statusPageURL'): 432 | self.__statusPageURL = url_operations.url_join(self.serverURL, 'status.html') 433 | return self.__statusPageURL 434 | 435 | def parseStatusPage(self, force=False): 436 | """ 437 | This method will load the status.html page of the current ERRDAP server reference 438 | this data is parsed into a OrderedDict, with the scalars, and DataFrames with the 439 | tables provided in status.html page. 440 | The data will be available in the `erddapClient.ERDDAP_Server.statusValues` 441 | property 442 | 443 | Parameters: 444 | 445 | `force` : Data is stored in a class property, if force is True, the data will be 446 | reloaded, if False, the last loaded data is returned. 447 | 448 | """ 449 | if self.__status_values is None or force: 450 | statusPageCode = urlread.__wrapped__( self.statusPageURL, self.auth).text 451 | self.__status_values = parseERDDAPStatusPage(statusPageCode, numversion=self.version_numeric) 452 | 453 | @property 454 | def statusValues(self): 455 | """ 456 | Returns a OrderedDict with the parsed data of the status.html page. 457 | More information on the data provided in status.html: 458 | [ERDDAP documentaiton](https://coastwatch.pfeg.noaa.gov/erddap/download/setup.html#monitoring) 459 | """ 460 | self.parseStatusPage(force=False) 461 | return self.__status_values 462 | 463 | 464 | 465 | 466 | 467 | class ERDDAP_SearchResult(object): 468 | datasetid = None 469 | title = None 470 | summary = None 471 | 472 | def __init__(self, url, erddapSearchResultRow, auth=None, lazyload=True): 473 | self.datasetid, self.title, self.summary = \ 474 | erddapSearchResultRow[ERDDAP_Search_Results_Rows.DATASETID], \ 475 | erddapSearchResultRow[ERDDAP_Search_Results_Rows.TITLE], \ 476 | erddapSearchResultRow[ERDDAP_Search_Results_Rows.SUMMARY] 477 | if erddapSearchResultRow[ERDDAP_Search_Results_Rows.GRIDDAP]: 478 | self.dataset = ERDDAP_Griddap(url, self.datasetid, auth=auth, lazyload=lazyload) 479 | elif erddapSearchResultRow[ERDDAP_Search_Results_Rows.TABLEDAP]: 480 | self.dataset = ERDDAP_Tabledap(url, self.datasetid, auth=auth, lazyload=lazyload) 481 | 482 | def __get__(self, instance, owner): 483 | return self.dataset 484 | 485 | 486 | class ERDDAP_SearchResults(list): 487 | 488 | def __init__(self, url, erddapSearchRows, auth=None, lazyload=True): 489 | for erddapSearchRow in erddapSearchRows: 490 | self.append(ERDDAP_SearchResult(url, erddapSearchRow, auth=auth, lazyload=lazyload)) 491 | 492 | def __repr__(self): 493 | return erddap_search_results_repr(self) 494 | 495 | def __getitem__(self, key): 496 | return super(ERDDAP_SearchResults, self).__getitem__(key).dataset 497 | 498 | @property 499 | def results(self): 500 | return list(self) 501 | 502 | -------------------------------------------------------------------------------- /erddapClient/erddap_tabledap.py: -------------------------------------------------------------------------------- 1 | from erddapClient.erddap_dataset import ERDDAP_Dataset 2 | from erddapClient.formatting import tabledap_str 3 | from erddapClient.parse_utils import castTimeRangeAttribute, ifListToCommaSeparatedString, parseTimeRangeAttributes 4 | 5 | 6 | class ERDDAP_Tabledap(ERDDAP_Dataset): 7 | """ 8 | Class with the representation and methods for a ERDDAP Tabledap Dataset 9 | 10 | """ 11 | 12 | DEFAULT_FILETYPE = 'csvp' 13 | 14 | def __init__(self, url, datasetid, auth=None, lazyload=True): 15 | """ 16 | Constructs the ERDDAP_Tabledap, and if specified, automaticaly loads 17 | the metadata information of the dataset. 18 | 19 | Arguments: 20 | 21 | `url` : The url of the ERDDAP Server that contains the tabledap dataset. 22 | 23 | `datasetid` : The identifier of the dataset. 24 | 25 | `auth` : Tupple with username and password for a protected ERDDAP Server. 26 | 27 | `lazyload` : If false calls the loadMetadata method. 28 | 29 | """ 30 | super().__init__(url, datasetid, 'tabledap', auth, lazyload=lazyload) 31 | 32 | def __str__(self): 33 | dst_repr_ = super().__str__() 34 | return dst_repr_ + tabledap_str(self) 35 | 36 | def loadMetadata(self, force=False): 37 | if super().loadMetadata(force): 38 | parseTimeRangeAttributes(self.variables.items()) 39 | 40 | 41 | # 42 | # Tabledap server side functions wrappers 43 | # 44 | def addVariablesWhere(self, attributeName, attributeValue): 45 | ''' 46 | Adds "addVariablesWhere" server side function to the data request query 47 | [https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#addVariablesWhere](https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#addVariablesWhere) 48 | ''' 49 | self.serverSideFunctions.append( 50 | 'addVariablesWhere("{}","{}")'.format(attributeName, attributeValue) 51 | ) 52 | return self 53 | 54 | def distinct(self): 55 | ''' 56 | Adds "distinct" server side function to the data request query 57 | [https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#distinct](https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#distinct) 58 | ''' 59 | self.serverSideFunctions.append( 'distinct()' ) 60 | return self 61 | 62 | def units(self, value): 63 | ''' 64 | https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#units 65 | ''' 66 | self.serverSideFunctions.append( 'units({})'.format(value) ) 67 | 68 | def orderBy(self, variables): 69 | ''' 70 | https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#orderBy 71 | ''' 72 | self.addServerSideFunction('orderBy', variables) 73 | return self 74 | 75 | def orderByClosest(self, variables): 76 | ''' 77 | https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#orderByClosest 78 | ''' 79 | self.addServerSideFunction('orderByClosest', variables) 80 | return self 81 | 82 | def orderByCount(self, variables): 83 | ''' 84 | https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#orderByCount 85 | ''' 86 | self.addServerSideFunction('orderByCount', variables) 87 | return self 88 | 89 | def orderByLimit(self, variables): 90 | ''' 91 | https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#orderByLimit 92 | ''' 93 | self.addServerSideFunction('orderByLimit', variables) 94 | return self 95 | 96 | def orderByMax(self, variables): 97 | ''' 98 | https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#orderByMax 99 | ''' 100 | self.addServerSideFunction('orderByMax', variables) 101 | return self 102 | 103 | def orderByMin(self, variables): 104 | ''' 105 | https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#orderByMin 106 | ''' 107 | self.addServerSideFunction('orderByMin', variables) 108 | return self 109 | 110 | def orderByMinMax(self, variables): 111 | ''' 112 | https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#orderByMinMax 113 | ''' 114 | self.addServerSideFunction('orderByMinMax', variables) 115 | return self 116 | 117 | def orderByMean(self, variables): 118 | ''' 119 | https://coastwatch.pfeg.noaa.gov/erddap/tabledap/documentation.html#orderByMean 120 | ''' 121 | self.addServerSideFunction('orderByMean', variables) 122 | return self 123 | 124 | def addServerSideFunction(self, functionName, arguments): 125 | self.serverSideFunctions.append( 126 | "{}(\"{}\")".format( 127 | functionName, ifListToCommaSeparatedString(arguments) 128 | ) 129 | ) 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /erddapClient/formatting.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | MAX_SUMMARY_LEN = 75 4 | 5 | def erddap_server_repr(sobj): 6 | summary = ["".format(type(sobj).__name__)] 7 | summary.append("Server version: {}".format(sobj.version)) 8 | # summary.append("Server version_string: {}".format(sobj.version_string)) 9 | return "\n".join(summary) 10 | 11 | 12 | def simple_dataset_repr(ds): 13 | summary = ["".format(type(ds).__name__)] 14 | dstTitle = ds.getAttribute('title') 15 | truncatedTitle = (dstTitle[:MAX_SUMMARY_LEN] + '..') if len(dstTitle) > MAX_SUMMARY_LEN else dstTitle 16 | summary.append("\"{}\"".format(truncatedTitle)) 17 | return ' '.join(summary) 18 | 19 | 20 | def dataset_str(ds): 21 | summary = ["".format(type(ds).__name__)] 22 | summary.append("Title: {}".format(ds.getAttribute('title'))) 23 | summary.append("Server URL: {}".format(ds.erddapurl)) 24 | summary.append("Dataset ID: {}".format(ds.datasetid)) 25 | return "\n".join(summary) 26 | 27 | 28 | def tabledap_str(ds): 29 | summary = [ "" ] 30 | if hasattr(ds,'lastRequestURL'): 31 | summary.append("Generated URL: " + ds.lastRequestURL) 32 | summary.append("Variables: ") 33 | for variableName, variableAttributes in ds.variables.items(): 34 | summary.append(" {} ({}) ".format(variableName, variableAttributes['_dataType']) ) 35 | if 'standard_name' in variableAttributes: 36 | summary.append(" Standard name: {} ".format(variableAttributes['standard_name']) ) 37 | if 'units' in variableAttributes: 38 | summary.append(" Units: {} ".format(variableAttributes['units']) ) 39 | 40 | return "\n".join(summary) 41 | 42 | 43 | def griddap_str(ds): 44 | summary = [ "", "Dimensions: " ] 45 | for dimensionName, dimensionInfo in ds.dimensions.items(): 46 | dimensionMeta = dimensionInfo.metadata 47 | summary.append(" {} ({}) range={} ".format(dimensionName, dimensionMeta['_dataType'], dimensionMeta['actual_range']) ) 48 | if 'standard_name' in dimensionMeta: 49 | summary.append(" Standard name: {} ".format(dimensionMeta['standard_name']) ) 50 | if 'units' in dimensionMeta: 51 | summary.append(" Units: {} ".format(dimensionMeta['units']) ) 52 | 53 | summary.append("Variables: ") 54 | for variableName, variableAttributes in ds.variables.items(): 55 | summary.append(" {} ({}) ".format(variableName, variableAttributes['_dataType']) ) 56 | if 'standard_name' in variableAttributes: 57 | summary.append(" Standard name: {} ".format(variableAttributes['standard_name']) ) 58 | if 'units' in variableAttributes: 59 | summary.append(" Units: {} ".format(variableAttributes['units']) ) 60 | 61 | return "\n".join(summary) 62 | 63 | 64 | def erddap_search_results_repr(srobj): 65 | summary = ["".format(type(srobj).__name__)] 66 | summary.append ("Results: {}".format(len(list(srobj)))) 67 | summary.append('[') 68 | for idx, item in enumerate(list(srobj)): 69 | summary.append( " {}".format(idx) + " - ".format(type(item.dataset).__name__) + \ 70 | " " + item.datasetid + " , \"" + item.title + "\"") 71 | summary.append(']') 72 | return '\n'.join(summary) 73 | 74 | 75 | def erddap_dimensions_str(dimsObj): 76 | summary = ["".format(type(dimsObj).__name__)] 77 | summary.append("Dimensions:") 78 | for dimName in dimsObj.keys(): 79 | summary.append (" - {} (nValues={}) {} .. {}".format(dimName, dimsObj[dimName].metadata['_nValues'], dimsObj[dimName].values.index[0], dimsObj[dimName].values.index[-1] )) 80 | return '\n'.join(summary) 81 | 82 | def erddap_dimension_str(dimObj): 83 | summary = ["".format(type(dimObj).__name__)] 84 | summary.append ("Dimension: {}".format(dimObj.name)) 85 | for attName, attValue in dimObj.metadata.items(): 86 | summary.append (" {} : {}".format(attName, attValue)) 87 | return '\n'.join(summary) -------------------------------------------------------------------------------- /erddapClient/parse_utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import datetime as dt 3 | from dateutil.parser import parse 4 | from netCDF4 import date2num, num2date 5 | from erddapClient.erddap_constants import ERDDAP_Metadata_Rows, ERDDAP_Search_Results_Rows, ERDDAP_TIME_UNITS, ERDDAP_DATETIME_FORMAT 6 | from collections import OrderedDict 7 | import pandas as pd 8 | 9 | def parseDictMetadata(dmetadata): 10 | """ 11 | This function parses the metadata json response from a erddap dataset 12 | This function receives a python dictionary created with the metadata response from a erddap dataset, 13 | It parses this dictionary and creates a new dictionary with the dimension variables, the data variables 14 | and the global metadata. 15 | """ 16 | _dimensions=OrderedDict() 17 | _variables=OrderedDict() 18 | _global=OrderedDict() 19 | 20 | inforows = dmetadata['table']['rows'] 21 | for row in inforows: 22 | drow = dict(zip(dmetadata['table']['columnNames'],row)) 23 | if row[ERDDAP_Metadata_Rows.VARIABLE_NAME] == 'NC_GLOBAL': 24 | _global[row[ERDDAP_Metadata_Rows.ATTRIBUTE_NAME]] = \ 25 | castMetadataAttribute(row[ERDDAP_Metadata_Rows.DATA_TYPE], row[ERDDAP_Metadata_Rows.VALUE]) 26 | else: 27 | if row[ERDDAP_Metadata_Rows.ROW_TYPE] == 'dimension': 28 | _dimensions[row[ERDDAP_Metadata_Rows.VARIABLE_NAME]] = parseDimensionValue(row[ERDDAP_Metadata_Rows.VALUE]) 29 | _dimensions[row[ERDDAP_Metadata_Rows.VARIABLE_NAME]]['_dataType'] = row[ERDDAP_Metadata_Rows.DATA_TYPE] 30 | elif row[ERDDAP_Metadata_Rows.ROW_TYPE] == 'variable': 31 | _variables[row[ERDDAP_Metadata_Rows.VARIABLE_NAME]] = {} 32 | _variables[row[ERDDAP_Metadata_Rows.VARIABLE_NAME]]['_dataType'] = row[ERDDAP_Metadata_Rows.DATA_TYPE] 33 | else: # Attributes 34 | if row[ERDDAP_Metadata_Rows.VARIABLE_NAME] in _dimensions: 35 | # Dimension atts 36 | _dimensions[row[ERDDAP_Metadata_Rows.VARIABLE_NAME]][row[ERDDAP_Metadata_Rows.ATTRIBUTE_NAME]] = \ 37 | castMetadataAttribute(row[ERDDAP_Metadata_Rows.DATA_TYPE], row[ERDDAP_Metadata_Rows.VALUE]) 38 | else: 39 | # Variable atts 40 | _variables[row[ERDDAP_Metadata_Rows.VARIABLE_NAME]][row[ERDDAP_Metadata_Rows.ATTRIBUTE_NAME]] = \ 41 | castMetadataAttribute(row[ERDDAP_Metadata_Rows.DATA_TYPE], row[ERDDAP_Metadata_Rows.VALUE]) 42 | # . 43 | 44 | return { 'global' : _global, 'dimensions' : _dimensions, 'variables' : _variables } 45 | 46 | 47 | def parseDimensionValue(value): 48 | """ 49 | This method will parse the ERDDAP dimension name attribute value, that comes 50 | from the info page. 51 | Sample: nValues=16, evenlySpaced=false, averageSpacing=91 days 6h 24m 0s 52 | Returns a dictionary with each of this elements. 53 | """ 54 | dimensionAttributes = {} 55 | elements = value.split(',') 56 | for e in elements: 57 | k,v = e.split('=') 58 | dimensionAttributes['_' + k.strip()] = guessCastMetadataAttribute(v.strip()) 59 | return dimensionAttributes 60 | 61 | 62 | def castMetadataAttribute(data_type, valuestr): 63 | """ 64 | This method will try to cast valuestr to the data_type specified. 65 | valuestr can be a tuple of values separated with a comma. 66 | """ 67 | if data_type != 'String' and ',' in valuestr: 68 | _lvaluestr = valuestr.split(',') 69 | elif data_type == 'String': 70 | return valuestr 71 | else: 72 | _lvaluestr = [valuestr] 73 | 74 | if data_type in ['float', 'double']: 75 | _castedvalue = [ float(v) for v in _lvaluestr ] 76 | elif data_type in ['short', 'int', 'byte', 'char', 'short', 'long']: 77 | _castedvalue = [ int(v) for v in _lvaluestr ] 78 | else: 79 | _castedvalue = [ v for v in _lvaluestr ] 80 | 81 | if len(_castedvalue) == 1: 82 | return _castedvalue[0] 83 | else: 84 | return tuple(_castedvalue) 85 | 86 | def parseTimeRangeAttributes(attItems): 87 | for dimName, dimAtts in attItems: 88 | if '_CoordinateAxisType' in dimAtts.keys() and dimAtts['_CoordinateAxisType'] == 'Time': 89 | dimAtts['actual_range'] = castTimeRangeAttribute(dimAtts['actual_range'], dimAtts['units']) 90 | 91 | def castTimeRangeAttribute(rangenumeric, units): 92 | return ( num2date(rangenumeric[0], units), num2date(rangenumeric[1], units) ) 93 | 94 | def boolify(s): 95 | if s == 'true': 96 | return True 97 | if s == 'false': 98 | return False 99 | raise ValueError() 100 | 101 | def guessCastMetadataAttribute(valuestr): 102 | for fn in (boolify, int, float): 103 | try: 104 | return fn(valuestr) 105 | except ValueError: 106 | pass 107 | return valuestr 108 | 109 | def parseSearchResults(dresults): 110 | _griddap_dsets = [] 111 | _tabledap_dsets = [] 112 | 113 | for row in dresults['table']['rows']: 114 | if row[ERDDAP_Search_Results_Rows.GRIDDAP]: 115 | _griddap_dsets.append(row[ERDDAP_Search_Results_Rows.DATASETID]) 116 | elif row[ERDDAP_Search_Results_Rows.TABLEDAP]: 117 | _tabledap_dsets.append(row[ERDDAP_Search_Results_Rows.DATASETID]) 118 | 119 | return _griddap_dsets, _tabledap_dsets 120 | 121 | 122 | def parseConstraintValue(value): 123 | """ 124 | This functions detect the constraint value type and decide if is a 125 | regular string and if so, put quotes "" around it. 126 | Detect if the constraint value is either: 127 | String value : Return the string inside quotes "" 128 | : Convert to string date with format ISO 8601 129 | Valid ISO 8601 Date : Return the string date 130 | Valid time operation : Return the string operation 131 | Valid variable 132 | operation : Return the string operation 133 | """ 134 | if isinstance(value, str): 135 | if validate_iso8601(value): 136 | return value 137 | if validate_constraint_time_operations(value): 138 | return value 139 | if validate_constraint_var_operations(value): 140 | return value 141 | else: 142 | return '"{}"'.format(value) 143 | elif isinstance(value,dt.datetime): 144 | return parseConstraintPyDatetime(value) 145 | else: 146 | return str(value) 147 | 148 | def parseConstraintDateTime(dtvalue): 149 | if isinstance(dtvalue,dt.datetime): 150 | return parseConstraintPyDatetime(dtvalue) 151 | elif isinstance(dtvalue, str): 152 | return dtvalue 153 | 154 | def parseConstraintPyDatetime(dtvalue): 155 | return dtvalue.strftime(ERDDAP_DATETIME_FORMAT) 156 | 157 | def parse_griddap_resultvariables_slices(resultVariables): 158 | """ 159 | This method will parse each variable subset in the 160 | resultVariables variable. The parsing is done by the 161 | parse_griddap_resultvariable_slice method. 162 | It will return a list with the subset components separated 163 | by dimension subset, with start,stride,stop elements. 164 | """ 165 | resultVariableStructure=[] 166 | for resultVariable in resultVariables: 167 | resultVariableStructure.append( parse_griddap_resultvariable_slice(resultVariable) ) 168 | 169 | return resultVariableStructure 170 | 171 | def parse_griddap_resultvariable_slice(resultVariable): 172 | """ 173 | This method will parse the ERDDAP griddap variable subset, 174 | It'll try to detect if the query is using the opendap extended 175 | format of subsetting. 176 | When using iso8601 dates, latitude, longitude values or keyword 177 | last, last-n , all of them between ( ) 178 | This method returns a list with the subset components for each 179 | dimensions. Each of this elements will have a dict with: 180 | 'start', 'stride', 'stop' slice components. 181 | """ 182 | variableName, variableSlices = "", "" 183 | variableNameSearch = re.search(GROUP_GRIDDAP_VARIABLE, resultVariable) 184 | if variableNameSearch: 185 | variableName = variableNameSearch.group(0) 186 | variableSlicesSearch = re.findall(GROUP_GRIDDAP_SLICE, resultVariable) 187 | if variableSlicesSearch: 188 | variableSlices = variableSlicesSearch 189 | 190 | # print ("variableName: ", variableName) 191 | # print ("variableSlices: ", variableSlices) 192 | sliceStructure = { 'variableName' : variableName, 'sliceComponents' : [] } 193 | for slice in variableSlices: 194 | sliceStructure['sliceComponents'].append( parse_griddap_slice_element(slice) ) 195 | 196 | return sliceStructure 197 | 198 | def parse_griddap_slice_element(slice): 199 | """ 200 | This will parse slice string, that might come as the form 201 | [(2002-06-01T09:00:00Z)] , [(-89.99):1000:(89.99)] , [0:10:100] , [0] 202 | it will return the start, stride and stop elements of the slice 203 | """ 204 | slicetrim = slice.replace('[','').replace(']','') 205 | sliceElementsSearch = re.findall(GROUP_GRIDDAP_SLICE_ELEMENT, slicetrim) 206 | 207 | if sliceElementsSearch: 208 | #sliceelements = sliceElementsSearch 209 | if len(sliceElementsSearch) == 1: 210 | sliceelements = { 'start' : sliceElementsSearch[0] } 211 | elif len(sliceElementsSearch) == 2: 212 | sliceelements = { 'start' : sliceElementsSearch[0], 'stop' : sliceElementsSearch[1] } 213 | elif len(sliceElementsSearch) == 3: 214 | sliceelements = { 'start' : sliceElementsSearch[0], 'stride' : sliceElementsSearch[1], 'stop' : sliceElementsSearch[2] } 215 | else: 216 | raise Exception('Slice is malformed, could not parse : {}'.format(slice)) 217 | 218 | # print ("slice Elements: ", sliceelements) 219 | return sliceelements 220 | 221 | def extractVariableName(qstring): 222 | """ 223 | Returns just the variable name, from a query string. 224 | 225 | extractVariableName('v_current[200][0][337:464][1018:1145]') returns: 'v_current' 226 | 227 | """ 228 | variableNameSearch = re.search(GROUP_GRIDDAP_VARIABLE, qstring) 229 | if variableNameSearch: 230 | variableName = variableNameSearch.group(0) 231 | else: 232 | variableName = "" 233 | return variableName 234 | 235 | def is_slice_element_opendap_extended(sliceElement): 236 | match_opendapextended = re.compile(SLICE_EXTENDED_DAP_FORMAT).match 237 | return validateRegex(sliceElement, match_opendapextended) 238 | 239 | def get_value_from_opendap_extended_slice_element(sliceElement): 240 | return sliceElement.replace('(','').replace(')','') 241 | 242 | def iso8601STRtoDT(iso8601string): 243 | # return dt.datetime.strptime(iso8601string, ERDDAP_DATETIME_FORMAT) 244 | # Using dateutil parse method 245 | return parse(iso8601string) 246 | 247 | def iso8601STRtoNum(iso8601string): 248 | return date2num(iso8601STRtoDT(iso8601string),ERDDAP_TIME_UNITS) 249 | 250 | def numtodate(numdate): 251 | return num2date(numdate, ERDDAP_TIME_UNITS) 252 | 253 | def dttonum(pdt): 254 | return date2num(pdt, ERDDAP_TIME_UNITS) 255 | 256 | # ERDDAP Server URL 257 | ERDDAP_SERVERURL=r'^http.*erddap\/(\w*\.html)$' 258 | # Regular expression validators 259 | ERDDAP_NUMVERSION=r'ERDDAP_version=)(.*)(?=<\/pre>)', htmlcode, re.DOTALL) 379 | if pre is None: 380 | return parsedStatus 381 | statusText = pre.group(1) 382 | parsedStatus['current-time'] = iso8601STRtoDT(tryresearch(r'^Current time is\s*(.*)', statusText, 1)) 383 | parsedStatus['startup-time'] = iso8601STRtoDT(tryresearch(r'^Startup was at\s*(.*)$', statusText, 1)) 384 | parsedStatus['ngriddatasets'] = tryresearchi(r'^nGridDatasets\s*=\s*(.*)$', statusText, 1) 385 | parsedStatus['ntabledatasets'] = tryresearchi(r'^nTableDatasets\s*=\s*(.*)$', statusText, 1) 386 | parsedStatus['ntotaldatasets'] = tryresearchi(r'^nTotalDatasets\s*=\s*(.*)$', statusText, 1) 387 | parsedStatus['ndatasetsfailed2load_sincelast_mld'] = tryresearchi(r'^n Datasets Failed To Load \(in the last major LoadDatasets\)\s*=\s*(\d*)$', statusText,1) 388 | 389 | # Dataset names that failes to load 390 | _datasets_failes2load = tryresearch(r'^n Datasets Failed To Load \(in the last major LoadDatasets\) = \d*\n(.*)\(end\)',statusText,1, re.DOTALL | re.MULTILINE) 391 | _datasets_failes2load = _datasets_failes2load.replace('\n','').strip().split(',') 392 | _datasets_failes2load = [ dst.strip() for dst in _datasets_failes2load if dst != '' ] 393 | parsedStatus['datasetsfailed2load_sincelast_mld'] = _datasets_failes2load 394 | 395 | if numversion >= 2.12: 396 | parsedStatus['nunique_users_since_startup'] = tryresearchi(r'^Unique users \(since startup\)\s*n =\s*(.*)$', statusText, 1) 397 | 398 | parsedStatus['nresponsefailed_since_lastmld'] = tryresearchi(r'Response Failed\s*Time \(since last major LoadDatasets\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 1) 399 | parsedStatus['nresponsefailed_time_since_lastmld'] = tryresearchi(r'Response Failed\s*Time \(since last major LoadDatasets\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 2) 400 | parsedStatus['nresponsefailed_since_lastdr'] = tryresearchi(r'Response Failed\s*Time \(since last Daily Report\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 1) 401 | parsedStatus['nresponsefailed_time_since_lastdr'] = tryresearchi(r'Response Failed\s*Time \(since last Daily Report\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 2) 402 | parsedStatus['nresponsefailed_since_startup'] = tryresearchi(r'Response Failed\s*Time \(since startup\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 1) 403 | parsedStatus['nresponsefailed_time_since_startup'] = tryresearchi(r'Response Failed\s*Time \(since startup\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 2) 404 | 405 | parsedStatus['nresponsesucceeded_since_lastmld'] = tryresearchi(r'Response Succeeded\s*Time \(since last major LoadDatasets\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 1) 406 | parsedStatus['responsesucceeded_time_since_lastmld'] = tryresearchi(r'Response Succeeded\s*Time \(since last major LoadDatasets\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 2) 407 | parsedStatus['nresponsesucceeded_since_lastdr'] = tryresearchi(r'Response Succeeded\s*Time \(since last Daily Report\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 1) 408 | parsedStatus['responsesucceeded_time_since_lastdr'] = tryresearchi(r'Response Succeeded\s*Time \(since last Daily Report\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 2) 409 | parsedStatus['nresponsesucceeded_since_startup'] = tryresearchi(r'Response Succeeded\s*Time \(since startup\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 1) 410 | parsedStatus['responsesucceeded_time_since_startup'] = tryresearchi(r'Response Succeeded\s*Time \(since startup\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 2) 411 | 412 | parsedStatus['ntaskthreadfailed_since_lastdr'] = tryresearchi(r'TaskThread Failed\s* Time \(since last Daily Report\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 1) 413 | parsedStatus['taskthreadfailed_time_since_lastdr'] = tryresearchi(r'TaskThread Failed\s* Time \(since last Daily Report\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 2) 414 | parsedStatus['ntaskthreadfailed_since_startup'] = tryresearchi(r'TaskThread Failed\s* Time \(since startup\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 1) 415 | parsedStatus['taskthreadfailed_time_since_startup'] = tryresearchi(r'TaskThread Failed\s* Time \(since startup\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 2) 416 | 417 | parsedStatus['ntaskthreadsucceeded_since_lastdr'] = tryresearchi(r'TaskThread Succeeded Time \(since last Daily Report\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 1) 418 | parsedStatus['taskthreadsucceeded_time_since_lastdr'] = tryresearchi(r'TaskThread Succeeded Time \(since last Daily Report\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 2) 419 | parsedStatus['ntaskthreadsucceeded_since_startup'] = tryresearchi(r'TaskThread Succeeded Time \(since startup\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 1) 420 | parsedStatus['taskthreadsucceeded_time_since_startup'] = tryresearchi(r'TaskThread Succeeded Time \(since startup\)\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', statusText, 2) 421 | 422 | parsedStatus['nthreads_tomwait'] = tryresearchi(r'^Number of threads: Tomcat-waiting=(\d*), inotify=(\d*), other=(\d*)',statusText,1) 423 | parsedStatus['nthreads_inotify'] = tryresearchi(r'^Number of threads: Tomcat-waiting=(\d*), inotify=(\d*), other=(\d*)',statusText,2) 424 | parsedStatus['nthreads_other'] = tryresearchi(r'^Number of threads: Tomcat-waiting=(\d*), inotify=(\d*), other=(\d*)',statusText,3) 425 | 426 | parsedStatus['memoryinuse'] = tryresearchi(r'^MemoryInUse=\s*(\d*) MB \(highWaterMark=\s*(\d*) MB\) \(Xmx ~=\s*(\d*) MB\)', statusText,1) 427 | parsedStatus['highwatermark'] = tryresearchi(r'^MemoryInUse=\s*(\d*) MB \(highWaterMark=\s*(\d*) MB\) \(Xmx ~=\s*(\d*) MB\)', statusText,2) 428 | parsedStatus['xmx'] = tryresearchi(r'^MemoryInUse=\s*(\d*) MB \(highWaterMark=\s*(\d*) MB\) \(Xmx ~=\s*(\d*) MB\)', statusText,3) 429 | 430 | # Major LoadDatasets Time Series 431 | if numversion >= 2.12: 432 | _major_loaddatasets_timeseries = tryresearch(r'Major LoadDatasets Time Series.*Open\n\s*timestamp.*Files\n(?:-|\s)*\n((.|\n)*)(?:Major LoadDatasets Times Distribution \(since last Daily Report\))', statusText,1) 433 | else: 434 | _major_loaddatasets_timeseries = tryresearch(r'Major LoadDatasets Time Series.*Memory \(MB\)\n\s*timestamp.*highWater\n((.|\n)*)(?:Major LoadDatasets Times Distribution \(since last Daily Report\))', statusText,1) 435 | # Remove characters from timeseries text table : (,),% 436 | _major_loaddatasets_timeseries = _major_loaddatasets_timeseries.replace('(','').replace(')','').replace('%','').split('\n') 437 | 438 | _major_loaddatasets_timeseries = [ row.split() for row in _major_loaddatasets_timeseries if row != '' ] 439 | _major_loaddatasets_timeseries = [ [ iso8601STRtoDT(col) if idx==0 else forceint(col) for idx, col in enumerate(row) ] for row in _major_loaddatasets_timeseries ] 440 | if numversion >= 2.12: 441 | mldts_columns = ['timestamp', 'mld_time', 'DL_ntry', 'DL_nfail', 'DL_ntotal', 'R_nsuccess','R_ns_median', 'R_nfailed','R_nf_median','R_memfail','R_toomany','NT_tomwait','NT_notify','NT_other','M_inuse','open_files_percent'] 442 | elif numversion >= 2.10: 443 | mldts_columns = ['timestamp', 'mld_time', 'DL_ntry', 'DL_nfail', 'DL_ntotal', 'R_nsuccess','R_ns_median', 'R_nfailed','R_nf_median','R_memfail','NT_tomwait','NT_notify','NT_other','M_inuse','M_highwater'] 444 | else: 445 | mldts_columns = ['timestamp', 'mld_time', 'DL_ntry', 'DL_nfail', 'DL_ntotal', 'R_nsuccess','R_ns_median', 'R_nfailed','R_nf_median','NT_tomwait','NT_notify','NT_other','M_inuse','M_highwater'] 446 | 447 | _major_loaddatasets_timeseries_df = pd.DataFrame(_major_loaddatasets_timeseries, 448 | columns=mldts_columns) 449 | _major_loaddatasets_timeseries_df.set_index('timestamp',inplace=True) 450 | parsedStatus['major_loaddatasets_timeseries'] = _major_loaddatasets_timeseries_df 451 | 452 | # Timedistributions Loop 453 | td_items = [ 454 | { 'key' : 'major_loaddatasets_timedistribution_since_lastdr' , 'regex' : r'Major LoadDatasets Times Distribution \(since last Daily Report\):\n((.|\n)*)Major LoadDatasets Times Distribution \(since startup\):', 'group' : 1 }, 455 | { 'key' : 'major_loaddatasets_timedistribution_since_startup' , 'regex' : r'Major LoadDatasets Times Distribution \(since startup\):\n((.|\n)*)Minor LoadDatasets Times Distribution \(since last Daily Report\):', 'group' : 1 }, 456 | { 'key' : 'minor_loaddatasets_timedistribution_since_lastdr' , 'regex' : r'Minor LoadDatasets Times Distribution \(since last Daily Report\):\n((.|\n)*)Minor LoadDatasets Times Distribution \(since startup\):', 'group' : 1 }, 457 | { 'key' : 'minor_loaddatasets_timedistribution_since_startup' , 'regex' : r'Minor LoadDatasets Times Distribution \(since startup\):\n((.|\n)*)Response Failed Time Distribution \(since last major LoadDatasets\):', 'group' : 1 }, 458 | { 'key' : 'response_failed_timedistribution_since_lastmld' , 'regex' : r'Response Failed Time Distribution \(since last major LoadDatasets\):\n((.|\n)*)Response Failed Time Distribution \(since last Daily Report\):', 'group' : 1 }, 459 | { 'key' : 'response_failed_timedistribution_since_lastdr' , 'regex' : r'Response Failed Time Distribution \(since last Daily Report\):\n((.|\n)*)Response Failed Time Distribution \(since startup\):', 'group' : 1 }, 460 | { 'key' : 'response_failed_timedistribution_since_startup' , 'regex' : r'Response Failed Time Distribution \(since startup\):\n((.|\n)*)Response Succeeded Time Distribution \(since last major LoadDatasets\):', 'group' : 1 }, 461 | { 'key' : 'response_succeeded_timedistribution_since_lastmld' , 'regex' : r'Response Succeeded Time Distribution \(since last major LoadDatasets\):\n((.|\n)*)Response Succeeded Time Distribution \(since last Daily Report\):', 'group' : 1 }, 462 | { 'key' : 'response_succeeded_timedistribution_since_lastdr' , 'regex' : r'Response Succeeded Time Distribution \(since last Daily Report\):\n((.|\n)*)Response Succeeded Time Distribution \(since startup\):', 'group' : 1 }, 463 | { 'key' : 'response_succeeded_timedistribution_since_startup' , 'regex' : r'Response Succeeded Time Distribution \(since startup\):\n((.|\n)*)TaskThread Failed Time Distribution \(since last Daily Report\):', 'group' : 1 }, 464 | { 'key' : 'taskthread_failed_timedistribution_since_lastdr' , 'regex' : r'TaskThread Failed Time Distribution \(since last Daily Report\):\n((.|\n)*)TaskThread Failed Time Distribution \(since startup\):', 'group' : 1 }, 465 | { 'key' : 'taskthread_failed_timedistribution_since_startup' , 'regex' : r'TaskThread Failed Time Distribution \(since startup\):\n((.|\n)*)TaskThread Succeeded Time Distribution \(since last Daily Report\):', 'group' : 1 }, 466 | { 'key' : 'taskthread_succeeded_timedistribution_since_lastdr' , 'regex' : r'TaskThread Succeeded Time Distribution \(since last Daily Report\):\n((.|\n)*)TaskThread Succeeded Time Distribution \(since startup\):', 'group' : 1 }, 467 | { 'key' : 'taskthread_succeeded_timedistribution_since_startup', 'regex' : r'TaskThread Succeeded Time Distribution \(since startup\):\n((.|\n)*)SgtMap topography ', 'group' : 1 }, 468 | ] 469 | 470 | for idx, td_item in enumerate(td_items): 471 | _td_item = tryresearch(td_item['regex'], statusText, 1) 472 | _td_item = _td_item.replace('<','<').replace('>','>').split('\n') 473 | _ntime__td_item = _td_item[0] 474 | parsedStatus[ 'n_' + td_item['key']] = tryresearchi(r'\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', _ntime__td_item, 1) 475 | parsedStatus[ 'nmedian_' + td_item['key']] = tryresearchi(r'\s*n =\s*(\d*)(?:,\s*median ~=\s*(\d*) ms)?', _ntime__td_item, 2) 476 | 477 | _td_item = _td_item[1:] 478 | _td_item = [ row.split(':') for row in _td_item if row != ''] 479 | _td_item = [ [ col if idx==0 else forceint(col) for idx, col in enumerate(row) ] for row in _td_item ] 480 | _td_item_df = pd.DataFrame(_td_item, columns=['time_distribution', 'n']) 481 | parsedStatus[td_item['key']] = _td_item_df 482 | 483 | return parsedStatus 484 | 485 | 486 | 487 | -------------------------------------------------------------------------------- /erddapClient/remote_requests.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | import requests 3 | import os 4 | import re 5 | 6 | 7 | def getMessageError(response): 8 | """ 9 | Extracts the error message from an ERDDAP error output. 10 | """ 11 | emessageSearch = re.search(r'message="(.*)"', response) 12 | if emessageSearch: 13 | return emessageSearch.group(1) 14 | else: 15 | return "" 16 | 17 | @lru_cache(maxsize=32) 18 | def urlread(url, auth=None, **kwargs): 19 | """ 20 | 21 | """ 22 | response = requests.get(url, auth=auth, **kwargs) 23 | if response.status_code == 200: 24 | return response 25 | else: 26 | print ("ERDDAP Error: \"{}\"".format(getMessageError(response.text))) 27 | response.raise_for_status() 28 | 29 | -------------------------------------------------------------------------------- /erddapClient/url_operations.py: -------------------------------------------------------------------------------- 1 | import os 2 | from urllib.parse import quote, quote_plus, urlparse, ParseResult 3 | 4 | 5 | def parseQueryItems(items, useSafeURL=True, safe='', item_separator='&'): 6 | if useSafeURL: 7 | return quote(item_separator.join(items), safe=safe) 8 | else: 9 | return item_separator.join(items) 10 | 11 | def url_join(*args): 12 | return "/".join(map(lambda x: str(x).rstrip('/'), args)) 13 | 14 | def joinURLElements(base, query): 15 | return base + '?' + query 16 | 17 | def joinURLElementsWithAuth(base, query, auth): 18 | abase = base.replace("https://", "https://{}:{}@".format(auth[0],auth[1])) 19 | abase = base.replace("http://", "http://{}:{}@".format(auth[0],auth[1])) 20 | return joinURLElements(abase, query) 21 | 22 | 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pandas 3 | xarray 4 | netCDF4 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="erddap-python", 8 | version="1.0.0", 9 | author="Favio Medrano", 10 | author_email="hmedrano@cicese.mx", 11 | description="Python erddap API client", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | install_requires=['pandas', 'requests', 'xarray', 'netCDF4'], 15 | url="https://github.com/hmedrano/erddap-python", 16 | packages=setuptools.find_packages(), 17 | classifiers=[ 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: MIT License", 20 | "Operating System :: OS Independent", 21 | ], 22 | license='MIT', 23 | python_requires='>=3.6', 24 | ) 25 | -------------------------------------------------------------------------------- /tests/cassettes/test_griddap_dimensions.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - python-requests/2.25.1 13 | method: GET 14 | uri: https://coastwatch.pfeg.noaa.gov/erddap/info/hycom_gom310D/index.json 15 | response: 16 | body: 17 | string: !!binary | 18 | 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 48 | GxV3DJYxEMYLy1u2Kd+q2ikVPWVnDkOFnH7fMCpmFHuVA+coD5S7ShV4X8P0oXKrbSdSwngUHetr 49 | BydYrsJDnGZvP0K1Vwptug9Xyq016jcCV22pDExzaI6G5VO77Ntqqb3G5chmhUpZS2eaGqqdg+f0 50 | Dqj71qjeKeBldUrYu06LNNGooMhto9NmvzDPVbrTWwpWv2c2ye+ttupyb60HrGpAskqlhEUTHA3V 51 | 73fm5Q4A+0+ziicRlhUqQ2xXppFKFWTtlFpso66TD2lRHs98OyhipnK82jeEoKi20WKk4ay2YQXR 52 | +WfquvHXuFxs0buD4aBvXloV04y8ks1cxn/C/AbG3wu9OCXsdgm6FQmxTIj6h0JVibWgUzURbPBV 53 | pqBWwtQDwehXtWOHXqFZQVjNVmXTNxwnb9q0Oix/qXMkMHT1Qb8HMQsWVd2RPjK6w+64jOJ0//Tt 54 | EM4qnsBvVqYa3mEFdEdVjpFb6QJZlQpu0x33Rjq1xN6le9yolW4zag/2zxtpH+E1EOXft7NVy1g1 55 | dMsyB2PDHMBEaaQPR2PLHJXBGr4dpeGJeIa1XBp6+YwvX/sYj53a6hUgph+dqgVKCLxMvhGhPYq/ 56 | JN9BqvUqyKv47lSteWw5cV7TrzLU1P/2sKzheNSFuKgmdla3V0ba+u1IW59I2vok0vK1W5O2PpW0 57 | dQVps/QLYhvU1jWoVX2WrBZtydq6hjXTHPctlYL1njkw+1YZapt9896MubzmCfDlhWooNAyzrVAJ 58 | kM2VTmEzr1QC6degLaF52QpUS79EN5RuCWy+cim5YP2ROTBGfcjIcN4fWV1jBIEyElV//37397v/ 59 | AdK/3rOCKAAA 60 | headers: 61 | Connection: 62 | - close 63 | Content-Disposition: 64 | - attachment;filename=hycom_gom310D_info.json 65 | Content-Encoding: 66 | - gzip 67 | Content-Type: 68 | - application/json;charset=UTF-8 69 | Date: 70 | - Sat, 22 May 2021 02:23:14 GMT 71 | Strict-Transport-Security: 72 | - max-age=31536000; includeSubDomains 73 | Transfer-Encoding: 74 | - chunked 75 | X-Frame-Options: 76 | - SAMEORIGIN 77 | status: 78 | code: 200 79 | message: '' 80 | - request: 81 | body: null 82 | headers: 83 | Accept: 84 | - '*/*' 85 | Accept-Encoding: 86 | - gzip, deflate 87 | Connection: 88 | - keep-alive 89 | User-Agent: 90 | - python-requests/2.25.1 91 | method: GET 92 | uri: https://coastwatch.pfeg.noaa.gov/erddap/griddap/hycom_gom310D.csvp?time%2Cdepth%2Clatitude%2Clongitude 93 | response: 94 | body: 95 | string: !!binary | 96 | H4sIAAAAAAAAAIWbwc7ESm5e93kKL21g5kJFUlIp2+y9cjbZBAY8sA3EdmBP3j8slaQudlF9BjOA 97 | 4ftd/WpW8ajUffjXf/23v/zN3/7Pf/gff/enf/rL//3rv/zN3/7b3/3p//zjX//1r//vn/wf/NNf 98 | /vk///KX//rf//4f//nXf/F/8B///s9f/+Qv//hff/27/ybLcvx5sT8v8g/L8t/P//6vPy1/LH8q 99 | 9Y/lKJvVP/358P/zE9QhuPZgkWPbdg/ufxzbssgnbEO4XJct277tV7ocR/2k1zF9XVuWdduPM13r 100 | sizrJ74NcbkuLqbbXnpcD7+tT3wf4/fVa9nWesb3w//zSdchrdfFtRybak/75yyf9DGk7U6vHrrS 101 | /jn3J12WsYR3+ljXrae3fbyTUob0dqVNdS1bT5vfyacqZVzI/Y7vxa6Pufq1909Vyric9Yqvy6GH 102 | 9fgQHRfzuKO2q/SkjCtfwloud7quov0+bB+XvoxrWe7V2USLXXH7ZPewTa5Lb1tZrO8TL8giQ76G 103 | fXLnj3pYv3P1mtShgkfYKFd+161Kr7jKuPayhJ1yx3fbl3558W2rn3gJW+WKV//MvSVEx8UXCVvl 104 | Tq+L9a3iufFWNGyVO12rXB1U/P+pn8qIhb1y5Q/ZSr/z4tv2+BRG1rBX7vjWtu6ZX+q4BWQL++WM 105 | H3/4/e59SRcdd4DsccPccb/4cV39kx1XtJQn60U/a759YUiOsLvufCm+Z698AJGOS1r0ya9yHGc8 106 | kGj98zKuabEnf/gdaf8XRhatkbZlvf8Fkbradv4LI43WCN2yr3d+27T0+ICjNWJXnlrqYst2XX4A 107 | 0hrBK8/tqJVaen1GJK2RvPq5/n6s1gsUoLRG9urzB6zscm6GLWJpjfi15y/YudG2D5XWL/R+Ln3I 108 | ft37CKb1G753fvXilH7xkUxrxO/6XH/daumlf8i0Rvb+/T/+fQtuy7of14VHLq0RvXfa1PYzHKC0 109 | RvjeYb8F7fUekbRG+F7hvdT96LUegbRG9N7hdbPrPkYerZG9d/iwpV5XXsJt1Dlc1df8zAYWrZG6 110 | d3g71HoDBRStEbpX+li2Y+33MYJojci9w2b9cbsFDq2RuHe4Sul9s3xy36vXTiDl2M+yrQFAawTt 111 | HV79ybn2cAl38L16LXxYPZ/I6zd75Hv5PF1U/NP1dASPfK9fSzv5l3qmI3Xke/08LMu+9puOyJHv 112 | 5WtZb8eqPRx4o9+r18L+dL3uIsJGv1fPw77di0gPj6TZIorvtB/EpF86YGaLHL7Tx1qkF28dksly 113 | m/qTsn++kTFb5O8d9iofPTwCZovwvcKrV3m7wvZJJku9eo3lSo542SJz73QV2/s2CnzZInCv9OaL 114 | Vvpij3zZInPvsG/nE/5r4MsWgXuHD+tPxjXwZYu0vcLeJs6uHl7G25hw28LOgKPvjACYbcatp6sj 115 | o/adEQCzzbxtaTO9qjcCZpt528LVn9P9RkbAbDNvPewAKFf2k0vW73Aunw9MC4DZZtS28KFVrnAJ 116 | dzAtX/F7LGXr4QCYbUZtS2/VzqOQRcBsM2s9XRbfd2d45Ms2o7ZlTcv5imUBMFvCWw9XPwevPTwA 117 | Zkt4W/6Qsu9XOUbAbAlvPexP+tJv4wswM289ffjJbTvTX4CZeVu8l/y83OsxAGaGrSe3zXdoTwbA 118 | zLQt3v62rP2WI2Bm2nrYitRejAcwewJPT+61v2laBMyewLP8sZaGgTMdALMnAPW0H6iv8MCXPeGn 119 | Z4+l9OyIlz3BZ/H3RmdG354jXvaEoB7eHKF7Dy/hLpK125fSv7uwiJc9AaintVbttQt42ROCenpf 120 | j2tNRrzsCUGLvzDqtT9HuuwJQEt7XZR+F8snlyxerfv5cq4BLntCzuIviWbn2mmAy56A08NbWY/t 121 | DAe47Ak5PX3Urfb7CHDZE3SKf/z1XBANcNkTfHrWAWD9pke47Ak+xal1HMt15QEue4JPD9vmH/IM 122 | j3DZE3p6uHon9XIEuOwJPcVfCstynvI1wmVP+Onp1YnRP+I6JKcV9OTh276XeYTLnsBT/lBv0PMo 123 | pwEuewJPD2/HfeUBLjM4xYmyLdt12QiXGZyeNr0r8QWXGZ6ebjutXzvSZYanOLe2a4NGuszs9Oyq 124 | x7L2cKDLfFKVhq1qfWOMdKkJa6Vxa9964QJdasJaaeDazueqRrrUhLXSyLX22o1wqQlrpXHLav+E 125 | I11qAltp2NK938byCSaLXf3Ifn6bJ4EvNQGth+0oJ+Ak8KUmnPVwXY/9zAa81ASzfjl/Vp+nWol4 126 | qQlnPe3gOh/WEvhSE9B6+PDj2dHD27jYM21bZdtBoIcHvtQEtx725TujI11qAlvfC344u6oR6FIT 127 | 2Go7b/VNJJEuNYGtngeutS/hOiSn9dN22vLV7kkZF3tGrYe9o/pVR7jUhLSedbacTzP5wKUmmG1d 128 | v9vWqxbgUhPMenqzUvtyBLjUBLMnrfo6j2ipCWc9qr4X+oVHttSEsw2u1jf9SJaaUNah7Q+o2isc 129 | yTKDVtvX9Nva7/iLLDNoPV391bZf+ossM2j9GSbLsfcNF9Eyg1bba+LWt1skywxabW+Jsvbb+JBl 130 | hqw/n3Wp5x2UQJYjgWx78u92Hi9KIMuRMNYPIH6oPl8nS0TLkTDW0/48O5+SJaLlSCjraX8fOb+O 131 | LQEtR0LZdsjSqv0jjmg5EtJ6ePXiXuEBLUdCWg9Xr3OvxwiXIyGtHyBFrR49PMLlSFDr6XanPR3g 132 | ciSo9bSTVq/0kJyW0A/Tqov1zzfC5Ug4287pR/9SvQS6HAln2xvAtpzvL+VDlyPBrCd9b5a+MwJd 133 | jgSznt7r0Wsc4HIklPWXobLatdYjXo4EtO01q9Rrz414ORLQetij14KMgDkS0vrrXvuuqBduBMyR 134 | wNbDW+nnixIBcySw9fTRvmHu6REwRwJbf/dV7V/slgCYI8Gth3f/46WHdVzuGbf+br9se+n3sXyC 135 | yfptfvTcWy2WL8LMsD2/XrDzm4blizAza9sXF07E4wx/EWZmradXKT0b+TKj1rO1nj9wLl94mVFr 136 | f7TvBa9bHvBSloSgHt7KInsPf/DSwsni+VlEzxIvI15aOFm8Q7V/e7cEvLR0snrH5p3Ub3rES0tP 137 | S7j+4a9c5Tz3LQ9eWnJaP0+aaF/rgS4tOy2fZ3c/e/aPN9ClhafVa98xWi29cDddWnJaO0/60137 138 | So908fRMTk/XXc4vzZaAl5aelm/1F0o16YUY8NLC0/J52M+/Sw8PeGnhafnW9kq5770YA15aeFq9 139 | 1V8py6FXeAm3kSyeH1KX2jfGiJeWThbQfAWv8ECXFk5W0Mwv13fRQJcWTlbQj1G29UoPdGnhZBH9 140 | +bBu15Wf4AxPD1rd2qmoHiNdWjhZPj+c7bb2cBlvYUbn2s5m547z8EiXlk7Wb1vPL15aeMBLCyfr 141 | tx3Sfs+ox4iXlk2Wz9ly9LuIdJnp2X5X6h8uomVG5+qHs7K0ryY9HNEyk7P9ELafP5S0cETLzE5P 142 | +5GyL8gXWWZ4rn8cy7Es1tNDMlm7w/zJd/TkgJaSYNbDdTkNFQ+PaCkJZrc/vBD1Wo4HLSVhrCdX 143 | qdpLHNBSEsa2X1LrvvXPFtBSEsZufxSx8wDc0gNaSoLZ8wfg9ShneERLSTjrYX+17oUbyVISzLaf 144 | rOX8naaFl3AX00p72F9/ezUCWEoC2q0ZFOe3Ei09kqUkoPX02URneiRLSUDr4X2tS++RkSwlAe32 145 | h9/Fpr1Tl08wWT5rrGjBGshSEsh6uKqczVcDWUrC2M3hdiwns2okS0kg62lHnPX7CGgpCWWbDOKX 146 | tjM9sqUklN0ccV6KvYe3cbVn0np4k6L9Pka+lIS0Hj722rMjXkoC2s1fQbV919CyI11Kwtlm6xzt 147 | ce3hQJeScHZzyvnbXP9865BM1s9fP/f7uoEuM2Y97G1d+mUjXWbSNj1K2vcdLftpkJmyHrR9X/vK 148 | fcFlpqynq9rS019wmTG7N7tisb49R7hIAk8P+zvf3rfnCBdJ4OnhY5H+8Ua4SILP3RG31vZVcQsv 149 | 4S6mtWu+XbGjb4tAF0no6eljX/b+AQNdJMFnEwVls76AI10kwed+Um7pdz3SRRJ+7k45PSWP+ni/ 150 | LTit394Ad/6AV4P36+GZnR7erZ3garB+WzZZPfPTbzuC1yj9tnSyfOaIa18c1Oj8tnSygH4EP5sk 151 | GL8tm6zfWrrmVYPw28LJ8q2rtFeoGnTflk0Wb637+VtbDbZvCyeL1zTY2ssc4CIJPD2y+vtZr0ag 152 | iyT03Nt3bIv1a6+f5IxO/1C+1Gtfk5EukqCzVcvqFR7pIgk7fdEaKNYe/nTIDM62vOsmffECXiQh 153 | p6f35TifDFHxbelk+fyZo9Y3aMTLTE8Pm++MfssRLzM9Pbzv/Zy6f/FlJqh3XPFXuevKgS8zQD3c 154 | 3kf66n3xZQZow4QvYd/NX3yZz6oNQVWk38jNl/P7gAS3Hl7Vjr6fb75c4Wm9PdzOlD37yU2r7XwV 155 | a7961I/Oe2Wnta7tPVXOA+LH5b3C01J72M/h5x0MJu+Vnta6PWyOrV/5ocsVnta6tp8R+jn8o/Be 156 | 4WSt2x4+d9FH4L3CyVqbrufXxfVj7/bwDNv2SF+svVzXj7p7hZPFW5d17R/wwcsVThZvbYeMfs8P 157 | Xq50soSrn2qv4q1DMlnArf3e3oMyLvZM2nbE8nTfGTddrnCyfI3g59a8VN0rmazdXjapvWgPXa50 158 | snj7WnTt133ocqWT1dvrdsLlo+r27ExaPyFLsb2Hb7hc4WTxHLPn99D1o+pe4WTx6lHOr9jrR9W9 159 | wsnaHf4ysPZyPHC50sn6Ncej786HLVc4WUBvo7XvoYiWGbXtbaisvaEiWWbSenbb7FrrD1pmyvrb 160 | 21L6w2z9YstMWQ/rdv4+WdcvtsyQ9fDux4tyhr/YMkO2vZ9ucn66NcIlO9R6ur339WuPcMkOtR5u 161 | Pzv2ux7hkp1q/W3du+98kKwBLtmpts0FrLX04o1wyQ61Hq7LtvfbCHTJDrVtIsBPUfuZDnTJTrVt 162 | HGDRrVdvHZLJevtp5DwTrYEu2am2fzNzfvmzBrpkp9rjfLFdeuEeumRnWk/W4/ymuK6RLtmZ9vAz 163 | 3/kTUF0jXLIjrYfXo2hfkJEu2ZnWw4fWXooRLtmR9mjf2131HdmSHWnbN3vXK/Aa2JIdadtXhnXZ 164 | ++cLbMmOtO3bSNlrv48Al+xI26YG9v50XwNdslNtGxqQsvYbGfGSnWo93FyJK/wJJqt37MXOG7aA 165 | l+RQ20YLl62cLw4W8JKcaVvYzt+hqkW6JEfaFt5XPd+57JsuE2g93R7A58uqfdFlIm0Lt5euvYcD 166 | XSbatvB+lCs7wEVnxnlWip4ORLUAF50Z18Je5bP7LMJFZ8jpOWJQ116OABedKedpLbten3AdkkmZ 167 | de36drVAF50J18LtINA/30gXnQHnYZPlkL4kD110BlxLOoiWvokCXXRGXEvXo177M+BFZ8Zp+xXM 168 | z5JrTw940RlzLezb4ug7dOSLzpRr4UNF+5VHwOhMOQ9v/kS7LryEu0gWb9ukf5VikS86Y66lm7fV 169 | Lx34ojPodDltk61fe+SLzqBr4c2r1/fGyBedOafn77Tb0u9j+QST9auqZ4014EVnwrVs/0ZeA1x0 170 | 5ptHHYab9OsGuugMuZbW7bB+6UAXTSnnSOwv+RroognliiNxXc/Hnwa6aEK582f+fhzRL7zMkPPw 171 | 7m1SznDEywy5NrhQ+7u4fuNlplwbXZB17/fxhZfpUNnS+1aXfu0BL9OJUtvgQum/XOkXXqYDZQvb 172 | 9S2UBrxYwloP74edxyL94MUS0BaHYeNET454sQS0nra9/4qnES+WgNbTtbTf/KsGuliC2uLnwy4M 173 | Vw10sQS1pR0Py3GFB7pYgloPV9O+L0a6WALbNuVQz2HRqpEulsD2nHI4BwyqRrpYAltPO1tqX8CR 174 | LpbAtg06LNvaLz3SxRLYeni1vX/C5ZNLVm+rtX/9K4EuloC2+AnRj2bWwyXcQbJ6++p7Xs5w4Isl 175 | nG1SnBeg9vTIF0tAW5rB0rMjXSzBbJuHqHvttzHSxRLUeviQ/b7yQBdLWNumIrZ96dUY6WIJbU9V 176 | cbuuHOhiM23b/1oucMVmzj65IfO9aE9GxrWdwHrHIkUmpD6xTw9MJH0ykRwTQ+/cFzMmej65gIsJ 177 | nXdsBMU6c/CJDYhYZwg+sYEO60y/Oxa4sM7ce3IjEdaZeXduZME60+6JDRRYZ849sU8kX4QSWn+d 178 | ufbEyvgHJ6DdsdDu68yyJzc2ejJQf+fGJk8m6Z/YNi7WxK8nNjR2Mj1/x8aWTubmn9jYzMnI/J0L 179 | zZxMyz+5IfO2DGMzJ9Pxd2xs5mQu/ol9Nu4Likps5mQc/s6FZk4m4Z+cjEv1gqPy1cwvOCpfzfxC 180 | pPLVzC9AKt/N/AKk8t3ML0QqX808HeOe2NDMyWT7E/tE8sVaQjMns+xPrIQ/mC/VEps5GWB/cmMz 181 | J+Prd25s5mRu/YkNzZwMrD+xoZmTUfU7NjZzMqT+xMZmTubT71xo5mQ0/ckNmbdlGJs5mUS/Y2Mz 182 | JzPoT8w+mbcVCM2cjJ7fudDMydT5k5NxqV6otYRmTgbNn5iOS/VCrSU0czJcfsdCMydz5U9ubOZk 183 | pPzOjc2cDJM/sdDML+BahmbOobUfX82cQ2s/vpo5Z9Z+fDdzzqz9iM2cjIrfubGZkxnxJzY0czIc 184 | /sSGZk7mwu/Y2MzJSPgTG5s5mQa/c6GZk0HwJzdk3pZhbOZk7vuOjc2cTHw/Mftk3lYgNHMy6X3n 185 | QjMnU95PTsalynG0H6GZk8HuJ6bjUuVE2o/QzMkw9x0LzZzMcT+5sZmTEe47NzZzMrz9xHRcrZxI 186 | +/E0czKq3SM1NHMyp/3ESviD+RrU2MzJcPaTi82c02ivX838QqT61cwvRKpfzfxCpPrVzC9Aqt/N 187 | nJ+i9hqbOZm7fnJD5m21xmZOxqzv2NjMyYT1E7NP5m2lQjMnc9V3LjRzMlL95IZmToap79jYzMkY 188 | 9RMbmjmZn35iy/hHX8BVYzMnQ9NPbmzmZF76zo3NnMxKPzEdV+sFXPVp5mQyukf20MzJWPQTK+EP 189 | 5muwx2ZOhqGf3NjMySj0nRubOZmBfmLbuFgv4NpDMyeDz3dsbOZk5PmJjc2czDvfua9mfgHXPjbz 190 | C7X2r2Z+odb+1cwv1NrHZn5B1v7dzC/I2mMzJ1PMT25o5mR++Y6NzZwMLj+xoZmTieUntoQ/+rZS 191 | oZmTMeUnNzZzMqF858ZmTmaTn9jQzMlQ8hN7Ii/Qis5hMob8xMr4B1+Y9eUZJrPHT25s5mTy+M6N 192 | zZyMHD+xbVysF3BFoTAZM75jYzMnA8ZPbGzmZLb4zoVmTsaKn9yQeVuGsZmTKeI7NjZzMj/8xD4b 193 | 9wVZX4pgMjV8/e8TyIofAlnZQyAreAhklR4DKZmeQMtk9Q2BrLIhkNU0BLKChsDPSp6vqRT4WckT 194 | vhT4WcmSjOFOAapkiooQoEqmcAgBqmQKhBCgSqYQCAGqZNr2IUCVTLs9BKiSaZOHAFXyd3eXZIB1 195 | ClAlf3d3SWZTp8DvSiZq/xT4XerE458Cv0udCPtT4HepEy9/CvwudeLffweg/RPPfgpQJaH9E4t+ 196 | ClAlof0TS34KUCWh/RMBfgpQJaH9E7l9ClAlof0TcX0KUCWh/RMz/TsA7Z8o5VPgdyUTdXwK/K5k 197 | oohPgd+VTETwKfC7kon0/R2A7k787ilAlYTuTtTtKUCVhO5OxOwpQJWE7k6U6ylAlYTuTpTqKUCV 198 | hO5ObOkpQJWE7k6m+74D2N2/Hu4lndubAj9KXdIhvSnwo9QlncabAj9KXdKhuynwo9QlHa77Dvxq 199 | /5IO0U0BquSv9i/piNwUoEr+av+SjsBNAarkr/Yv6XTbFKBK/mr/kk6uTQGq5K/2L+lU2hSgSv5q 200 | /5KOnX0HfrV/SafLYuD307+kQ2RT4Hepfz/9SzogNgV+l/r307+k019T4Hepfz/9SzrdNQWoktD+ 201 | v5/+JZ3WmgJUSWj/30//kg5ffQeg/X8//Us6XDUFqJLQ/r+f/iWdlZoCVElo/2QQagr8rkMy7zQF 202 | ftchmWyaAr/rkEwwTQGsw+8dlYwnTQGqJPRmMno0BaiS0JvJVNEUoEpCbyZTQ98B6M1kQGgKUCWh 203 | N5PRnylAlYTeTMZ6pgBVEh7NyczOFPhdyWQ2Zwr8LnUyhTMFfpc6mbaZAr9LnUzVTIHfpU6mZ74D 204 | 0P7JkMwUoEpC+ydTMFOAKgntn8y3TAGqJLR/Mr0yBaiS0P4v0ykhQJWE9n8ZRwkBqiS0/8v0yRiA 205 | 9n+ZOAmB35V8mTIJgd+VfJkrCYHflXyZJQmB35V8mSEZA9DdL2MjIUCVhO5+mREJAaokdPfLQEgI 206 | UCWhu18GQEKAKgnd/TLwEQJUSejul+mOEKBKQne/jHKMAexueLi/zG2EwO9Sv8xrhMDvUr+MaITA 207 | 71K/TGWEwO9Sv0xijAFo/5fhixCgSkL7vwxbhABVEtr/ZbgiBKiS0P4vkxQhQJWE9n8ZmwgBqiS0 208 | /8uMRAhQJaH9X6YixgC0/8scRAj8ruTL7EMI/K7ky7xDCPyu5MuEQwj8ruTLYMMYgO5+mWcIAaok 209 | dPfL7EIIUCWhu18mFUKAKgnd/TKVEAJUSejulzmEEKBKQne/zByEAFUSuvtlwmAMYHfDw/1lmiAE 210 | fpf6ZYggBH6X+mV2IAR+l/plWiAEfpf6ZUJgDED7v0wFhABVEtr/ZQIgBKiS0P4vzn8IUCWh/V/8 211 | /hCgSkL7v9j8IUCVhPZ/MfdDgCoJ7f+i6o8BaP8XQf8TeDHzQ+B3qV9s/BD4XeoXAz8Efpf6xbkP 212 | gd+lfvHsxwC0/4tfHwJUSWj/F5c+BKiS0P4v5nwIUCWh/V8s+RCgSkL7v2jxIUCVhPb/LcMXkuEL 213 | yfCFZPhCMnwhGb6QDF9Ihi8kwxeS4QvJ8IVk+EIyfCEZvpAMX0iGLyTDF5LhC8nwhWT4QjJ8IRm+ 214 | kAxfSIYvJMMXkuELyfCFZPhCMnwhGb6QDF9Ihi8kwxeS4QvJ8IVk+EIyfCEZvpAMX0iGLyTDF5Lh 215 | C8nwhWT4QjJ8IRm+kAxfSIYvJMMXkuELyfCFZPhCMnwhGb6QDF9Ihi8kwxeS4QvJ8IVk+EIyfCEZ 216 | vpAMX0iGLyTDF5LhC8nwhWT4QjJ8IRm+kAxfSIYvJMMXkuELyfCFZPhCMnwhGb6QDF9Ihi8kwxeS 217 | 4QvJ8IVk+EIyfCEZvpAMX0iGLyTDF5LhC8nwhWT4QjJ8IRm+kAxfSIYvJMMLyfBCMryQDC8kwwvJ 218 | 8EIyvJAMLyTDC8nwQjK8kAwvJMMLyfBCMryQDC8kwwvJ8EIyvJAMLyTDC8nwQjK8kAwvJMMLyfBC 219 | MryQDC8kwwvJ8EIyvJAMLyTDC8nwQjK8kAwvJMMLyfBCMryQDC8kwwvJ8EIyvJAMLyTDC8nwQjK8 220 | kAwvJMMLyfBCMryQDC8kwwvJ8EIyvJAMLyTDC8nwQjK8kAzfA78r+duWF7LlhWx5IVteyJYXsuWF 221 | bHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5 222 | IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFb 223 | XsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17I 224 | lhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYX 225 | suWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7Ll 226 | hWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVs 227 | eSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkh 228 | W17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVte 229 | yJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiW 230 | F7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey 231 | 5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWF 232 | bHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5 233 | IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFb 234 | XsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17I 235 | lhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWVbHklW17Jlley5ZVseSVbXsmWV7LllWx5JVteyZZX 236 | suWVbHklW17Jlley5ZVseSVbXsmWV7LllWx5JVteyZZXsuWVbHklW17Jlley5ZVseSVbXsmWV7Ll 237 | lWx5JVteyZZXsuWVbHklW17Jlley5ZVseSVbXsmWV7LllWx5JVteyZZXsuWVbHklW17Jlley5ZVs 238 | eSVbXsmWV7LllWx5JVteyZZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHgl 239 | GV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRle 240 | SYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmG 241 | V5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS 242 | 4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGV 243 | ZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4 244 | JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZ 245 | XkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5J 246 | hleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZX 247 | kuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5Lh 248 | lWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVk 249 | eCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHgl 250 | GV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRle 251 | SYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmG 252 | V5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS 253 | 4ZVkeCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGN 254 | ZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4 255 | IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ 256 | 3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5I 257 | hjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3 258 | kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5Lh 259 | jWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1k 260 | eCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgj 261 | Gd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4Ixne 262 | SIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiG 263 | N5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS 264 | 4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3n7K8P8ffRoHKKQUAQA= 265 | headers: 266 | Connection: 267 | - close 268 | Content-Disposition: 269 | - attachment;filename=hycom_gom310D_12fe_23fe_acb3.csv 270 | Content-Encoding: 271 | - gzip 272 | Content-Type: 273 | - text/csv;charset=ISO-8859-1 274 | Date: 275 | - Sat, 22 May 2021 02:23:15 GMT 276 | Last-Modified: 277 | - Sat, 22 May 2021 02:23:15 GMT 278 | Strict-Transport-Security: 279 | - max-age=31536000; includeSubDomains 280 | Transfer-Encoding: 281 | - chunked 282 | X-Frame-Options: 283 | - SAMEORIGIN 284 | erddap-server: 285 | - '2.12' 286 | xdods-server: 287 | - dods/3.7 288 | status: 289 | code: 200 290 | message: '' 291 | version: 1 292 | -------------------------------------------------------------------------------- /tests/cassettes/test_griddap_dimensions_creation.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - python-requests/2.25.1 13 | method: GET 14 | uri: https://coastwatch.pfeg.noaa.gov/erddap/info/hycom_gom310D/index.json 15 | response: 16 | body: 17 | string: !!binary | 18 | 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 48 | GxV3DJYxEMYLy1u2Kd+q2ikVPWVnDkOFnH7fMCpmFHuVA+coD5S7ShV4X8P0oXKrbSdSwngUHetr 49 | BydYrsJDnGZvP0K1Vwptug9Xyq016jcCV22pDExzaI6G5VO77Ntqqb3G5chmhUpZS2eaGqqdg+f0 50 | Dqj71qjeKeBldUrYu06LNNGooMhto9NmvzDPVbrTWwpWv2c2ye+ttupyb60HrGpAskqlhEUTHA3V 51 | 73fm5Q4A+0+ziicRlhUqQ2xXppFKFWTtlFpso66TD2lRHs98OyhipnK82jeEoKi20WKk4ay2YQXR 52 | +WfquvHXuFxs0buD4aBvXloV04y8ks1cxn/C/AbG3wu9OCXsdgm6FQmxTIj6h0JVibWgUzURbPBV 53 | pqBWwtQDwehXtWOHXqFZQVjNVmXTNxwnb9q0Oix/qXMkMHT1Qb8HMQsWVd2RPjK6w+64jOJ0//Tt 54 | EM4qnsBvVqYa3mEFdEdVjpFb6QJZlQpu0x33Rjq1xN6le9yolW4zag/2zxtpH+E1EOXft7NVy1g1 55 | dMsyB2PDHMBEaaQPR2PLHJXBGr4dpeGJeIa1XBp6+YwvX/sYj53a6hUgph+dqgVKCLxMvhGhPYq/ 56 | JN9BqvUqyKv47lSteWw5cV7TrzLU1P/2sKzheNSFuKgmdla3V0ba+u1IW59I2vok0vK1W5O2PpW0 57 | dQVps/QLYhvU1jWoVX2WrBZtydq6hjXTHPctlYL1njkw+1YZapt9896MubzmCfDlhWooNAyzrVAJ 58 | kM2VTmEzr1QC6degLaF52QpUS79EN5RuCWy+cim5YP2ROTBGfcjIcN4fWV1jBIEyElV//37397v/ 59 | AdK/3rOCKAAA 60 | headers: 61 | Connection: 62 | - close 63 | Content-Disposition: 64 | - attachment;filename=hycom_gom310D_info.json 65 | Content-Encoding: 66 | - gzip 67 | Content-Type: 68 | - application/json;charset=UTF-8 69 | Date: 70 | - Sat, 22 May 2021 02:14:30 GMT 71 | Strict-Transport-Security: 72 | - max-age=31536000; includeSubDomains 73 | Transfer-Encoding: 74 | - chunked 75 | X-Frame-Options: 76 | - SAMEORIGIN 77 | status: 78 | code: 200 79 | message: '' 80 | - request: 81 | body: null 82 | headers: 83 | Accept: 84 | - '*/*' 85 | Accept-Encoding: 86 | - gzip, deflate 87 | Connection: 88 | - keep-alive 89 | User-Agent: 90 | - python-requests/2.25.1 91 | method: GET 92 | uri: https://coastwatch.pfeg.noaa.gov/erddap/griddap/hycom_gom310D.csvp?time%2Cdepth%2Clatitude%2Clongitude 93 | response: 94 | body: 95 | string: !!binary | 96 | H4sIAAAAAAAAAIWbwc7ESm5e93kKL21g5kJFUlIp2+y9cjbZBAY8sA3EdmBP3j8slaQudlF9BjOA 97 | 4ftd/WpW8ajUffjXf/23v/zN3/7Pf/gff/enf/rL//3rv/zN3/7b3/3p//zjX//1r//vn/wf/NNf 98 | /vk///KX//rf//4f//nXf/F/8B///s9f/+Qv//hff/27/ybLcvx5sT8v8g/L8t/P//6vPy1/LH8q 99 | 9Y/lKJvVP/358P/zE9QhuPZgkWPbdg/ufxzbssgnbEO4XJct277tV7ocR/2k1zF9XVuWdduPM13r 100 | sizrJ74NcbkuLqbbXnpcD7+tT3wf4/fVa9nWesb3w//zSdchrdfFtRybak/75yyf9DGk7U6vHrrS 101 | /jn3J12WsYR3+ljXrae3fbyTUob0dqVNdS1bT5vfyacqZVzI/Y7vxa6Pufq1909Vyric9Yqvy6GH 102 | 9fgQHRfzuKO2q/SkjCtfwloud7quov0+bB+XvoxrWe7V2USLXXH7ZPewTa5Lb1tZrO8TL8giQ76G 103 | fXLnj3pYv3P1mtShgkfYKFd+161Kr7jKuPayhJ1yx3fbl3558W2rn3gJW+WKV//MvSVEx8UXCVvl 104 | Tq+L9a3iufFWNGyVO12rXB1U/P+pn8qIhb1y5Q/ZSr/z4tv2+BRG1rBX7vjWtu6ZX+q4BWQL++WM 105 | H3/4/e59SRcdd4DsccPccb/4cV39kx1XtJQn60U/a759YUiOsLvufCm+Z698AJGOS1r0ya9yHGc8 106 | kGj98zKuabEnf/gdaf8XRhatkbZlvf8Fkbradv4LI43WCN2yr3d+27T0+ICjNWJXnlrqYst2XX4A 107 | 0hrBK8/tqJVaen1GJK2RvPq5/n6s1gsUoLRG9urzB6zscm6GLWJpjfi15y/YudG2D5XWL/R+Ln3I 108 | ft37CKb1G753fvXilH7xkUxrxO/6XH/daumlf8i0Rvb+/T/+fQtuy7of14VHLq0RvXfa1PYzHKC0 109 | RvjeYb8F7fUekbRG+F7hvdT96LUegbRG9N7hdbPrPkYerZG9d/iwpV5XXsJt1Dlc1df8zAYWrZG6 110 | d3g71HoDBRStEbpX+li2Y+33MYJojci9w2b9cbsFDq2RuHe4Sul9s3xy36vXTiDl2M+yrQFAawTt 111 | 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 231 | 5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWF 232 | bHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5 233 | IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFb 234 | XsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17I 235 | lhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWVbHklW17Jlley5ZVseSVbXsmWV7LllWx5JVteyZZX 236 | suWVbHklW17Jlley5ZVseSVbXsmWV7LllWx5JVteyZZXsuWVbHklW17Jlley5ZVseSVbXsmWV7Ll 237 | lWx5JVteyZZXsuWVbHklW17Jlley5ZVseSVbXsmWV7LllWx5JVteyZZXsuWVbHklW17Jlley5ZVs 238 | eSVbXsmWV7LllWx5JVteyZZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHgl 239 | GV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRle 240 | SYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmG 241 | V5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS 242 | 4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGV 243 | ZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4 244 | JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZ 245 | XkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5J 246 | hleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZX 247 | kuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5Lh 248 | lWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVk 249 | eCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHgl 250 | GV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRle 251 | SYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmG 252 | V5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS 253 | 4ZVkeCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGN 254 | ZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4 255 | IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ 256 | 3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5I 257 | hjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3 258 | kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5Lh 259 | jWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1k 260 | eCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgj 261 | Gd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4Ixne 262 | SIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiG 263 | N5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS 264 | 4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3n7K8P8ffRoHKKQUAQA= 265 | headers: 266 | Connection: 267 | - close 268 | Content-Disposition: 269 | - attachment;filename=hycom_gom310D_12fe_23fe_acb3.csv 270 | Content-Encoding: 271 | - gzip 272 | Content-Type: 273 | - text/csv;charset=ISO-8859-1 274 | Date: 275 | - Sat, 22 May 2021 02:14:31 GMT 276 | Last-Modified: 277 | - Sat, 22 May 2021 02:14:31 GMT 278 | Strict-Transport-Security: 279 | - max-age=31536000; includeSubDomains 280 | Transfer-Encoding: 281 | - chunked 282 | X-Frame-Options: 283 | - SAMEORIGIN 284 | erddap-server: 285 | - '2.12' 286 | xdods-server: 287 | - dods/3.7 288 | status: 289 | code: 200 290 | message: '' 291 | version: 1 292 | -------------------------------------------------------------------------------- /tests/cassettes/test_griddap_setSubset_query.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - python-requests/2.25.1 13 | method: GET 14 | uri: https://coastwatch.pfeg.noaa.gov/erddap/info/hycom_gom310D/index.json 15 | response: 16 | body: 17 | string: !!binary | 18 | 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 48 | GxV3DJYxEMYLy1u2Kd+q2ikVPWVnDkOFnH7fMCpmFHuVA+coD5S7ShV4X8P0oXKrbSdSwngUHetr 49 | BydYrsJDnGZvP0K1Vwptug9Xyq016jcCV22pDExzaI6G5VO77Ntqqb3G5chmhUpZS2eaGqqdg+f0 50 | Dqj71qjeKeBldUrYu06LNNGooMhto9NmvzDPVbrTWwpWv2c2ye+ttupyb60HrGpAskqlhEUTHA3V 51 | 73fm5Q4A+0+ziicRlhUqQ2xXppFKFWTtlFpso66TD2lRHs98OyhipnK82jeEoKi20WKk4ay2YQXR 52 | +WfquvHXuFxs0buD4aBvXloV04y8ks1cxn/C/AbG3wu9OCXsdgm6FQmxTIj6h0JVibWgUzURbPBV 53 | pqBWwtQDwehXtWOHXqFZQVjNVmXTNxwnb9q0Oix/qXMkMHT1Qb8HMQsWVd2RPjK6w+64jOJ0//Tt 54 | EM4qnsBvVqYa3mEFdEdVjpFb6QJZlQpu0x33Rjq1xN6le9yolW4zag/2zxtpH+E1EOXft7NVy1g1 55 | dMsyB2PDHMBEaaQPR2PLHJXBGr4dpeGJeIa1XBp6+YwvX/sYj53a6hUgph+dqgVKCLxMvhGhPYq/ 56 | JN9BqvUqyKv47lSteWw5cV7TrzLU1P/2sKzheNSFuKgmdla3V0ba+u1IW59I2vok0vK1W5O2PpW0 57 | dQVps/QLYhvU1jWoVX2WrBZtydq6hjXTHPctlYL1njkw+1YZapt9896MubzmCfDlhWooNAyzrVAJ 58 | kM2VTmEzr1QC6degLaF52QpUS79EN5RuCWy+cim5YP2ROTBGfcjIcN4fWV1jBIEyElV//37397v/ 59 | AdK/3rOCKAAA 60 | headers: 61 | Connection: 62 | - close 63 | Content-Disposition: 64 | - attachment;filename=hycom_gom310D_info.json 65 | Content-Encoding: 66 | - gzip 67 | Content-Type: 68 | - application/json;charset=UTF-8 69 | Date: 70 | - Sat, 22 May 2021 02:54:47 GMT 71 | Strict-Transport-Security: 72 | - max-age=31536000; includeSubDomains 73 | Transfer-Encoding: 74 | - chunked 75 | X-Frame-Options: 76 | - SAMEORIGIN 77 | status: 78 | code: 200 79 | message: '' 80 | - request: 81 | body: null 82 | headers: 83 | Accept: 84 | - '*/*' 85 | Accept-Encoding: 86 | - gzip, deflate 87 | Connection: 88 | - keep-alive 89 | User-Agent: 90 | - python-requests/2.25.1 91 | method: GET 92 | uri: https://coastwatch.pfeg.noaa.gov/erddap/griddap/hycom_gom310D.csvp?time%2Cdepth%2Clatitude%2Clongitude 93 | response: 94 | body: 95 | string: !!binary | 96 | H4sIAAAAAAAAAIWbwc7ESm5e93kKL21g5kJFUlIp2+y9cjbZBAY8sA3EdmBP3j8slaQudlF9BjOA 97 | 4ftd/WpW8ajUffjXf/23v/zN3/7Pf/gff/enf/rL//3rv/zN3/7b3/3p//zjX//1r//vn/wf/NNf 98 | /vk///KX//rf//4f//nXf/F/8B///s9f/+Qv//hff/27/ybLcvx5sT8v8g/L8t/P//6vPy1/LH8q 99 | 9Y/lKJvVP/358P/zE9QhuPZgkWPbdg/ufxzbssgnbEO4XJct277tV7ocR/2k1zF9XVuWdduPM13r 100 | sizrJ74NcbkuLqbbXnpcD7+tT3wf4/fVa9nWesb3w//zSdchrdfFtRybak/75yyf9DGk7U6vHrrS 101 | /jn3J12WsYR3+ljXrae3fbyTUob0dqVNdS1bT5vfyacqZVzI/Y7vxa6Pufq1909Vyric9Yqvy6GH 102 | 9fgQHRfzuKO2q/SkjCtfwloud7quov0+bB+XvoxrWe7V2USLXXH7ZPewTa5Lb1tZrO8TL8giQ76G 103 | fXLnj3pYv3P1mtShgkfYKFd+161Kr7jKuPayhJ1yx3fbl3558W2rn3gJW+WKV//MvSVEx8UXCVvl 104 | Tq+L9a3iufFWNGyVO12rXB1U/P+pn8qIhb1y5Q/ZSr/z4tv2+BRG1rBX7vjWtu6ZX+q4BWQL++WM 105 | H3/4/e59SRcdd4DsccPccb/4cV39kx1XtJQn60U/a759YUiOsLvufCm+Z698AJGOS1r0ya9yHGc8 106 | kGj98zKuabEnf/gdaf8XRhatkbZlvf8Fkbradv4LI43WCN2yr3d+27T0+ICjNWJXnlrqYst2XX4A 107 | 0hrBK8/tqJVaen1GJK2RvPq5/n6s1gsUoLRG9urzB6zscm6GLWJpjfi15y/YudG2D5XWL/R+Ln3I 108 | ft37CKb1G753fvXilH7xkUxrxO/6XH/daumlf8i0Rvb+/T/+fQtuy7of14VHLq0RvXfa1PYzHKC0 109 | RvjeYb8F7fUekbRG+F7hvdT96LUegbRG9N7hdbPrPkYerZG9d/iwpV5XXsJt1Dlc1df8zAYWrZG6 110 | d3g71HoDBRStEbpX+li2Y+33MYJojci9w2b9cbsFDq2RuHe4Sul9s3xy36vXTiDl2M+yrQFAawTt 111 | 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 231 | 5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWF 232 | bHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5 233 | IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFb 234 | XsiWF7LlhWx5IVteyJYXsuWFbHkhW17Ilhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWFbHkhW17I 235 | lhey5YVseSFbXsiWF7LlhWx5IVteyJYXsuWVbHklW17Jlley5ZVseSVbXsmWV7LllWx5JVteyZZX 236 | suWVbHklW17Jlley5ZVseSVbXsmWV7LllWx5JVteyZZXsuWVbHklW17Jlley5ZVseSVbXsmWV7Ll 237 | lWx5JVteyZZXsuWVbHklW17Jlley5ZVseSVbXsmWV7LllWx5JVteyZZXsuWVbHklW17Jlley5ZVs 238 | eSVbXsmWV7LllWx5JVteyZZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHgl 239 | GV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRle 240 | SYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmG 241 | V5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS 242 | 4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGV 243 | ZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4 244 | JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZ 245 | XkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5J 246 | hleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZX 247 | kuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5Lh 248 | lWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVk 249 | eCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHgl 250 | GV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRle 251 | SYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmG 252 | V5LhlWR4JRleSYZXkuGVZHglGV5JhleS4ZVkeCUZXkmGV5LhlWR4JRleSYZXkuGVZHglGV5JhleS 253 | 4ZVkeCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGN 254 | ZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4 255 | IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ 256 | 3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5I 257 | hjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3 258 | kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5Lh 259 | jWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1k 260 | eCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgj 261 | Gd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4Ixne 262 | SIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiG 263 | N5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS 264 | 4Y1keCMZ3kiGN5LhjWR4IxneSIY3kuGNZHgjGd5IhjeS4Y1keCMZ3n7K8P8ffRoHKKQUAQA= 265 | headers: 266 | Connection: 267 | - close 268 | Content-Disposition: 269 | - attachment;filename=hycom_gom310D_12fe_23fe_acb3.csv 270 | Content-Encoding: 271 | - gzip 272 | Content-Type: 273 | - text/csv;charset=ISO-8859-1 274 | Date: 275 | - Sat, 22 May 2021 02:54:48 GMT 276 | Last-Modified: 277 | - Sat, 22 May 2021 02:54:48 GMT 278 | Strict-Transport-Security: 279 | - max-age=31536000; includeSubDomains 280 | Transfer-Encoding: 281 | - chunked 282 | X-Frame-Options: 283 | - SAMEORIGIN 284 | erddap-server: 285 | - '2.12' 286 | xdods-server: 287 | - dods/3.7 288 | status: 289 | code: 200 290 | message: '' 291 | version: 1 292 | -------------------------------------------------------------------------------- /tests/cassettes/test_griddap_subset_parsing.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - python-requests/2.25.1 13 | method: GET 14 | uri: https://coastwatch.pfeg.noaa.gov/erddap/info/erdTAgeomday/index.json 15 | response: 16 | body: 17 | string: !!binary | 18 | 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 59 | headers: 60 | Connection: 61 | - close 62 | Content-Disposition: 63 | - attachment;filename=erdTAgeomday_info.json 64 | Content-Encoding: 65 | - gzip 66 | Content-Type: 67 | - application/json;charset=UTF-8 68 | Date: 69 | - Fri, 21 May 2021 08:14:18 GMT 70 | Strict-Transport-Security: 71 | - max-age=31536000; includeSubDomains 72 | Transfer-Encoding: 73 | - chunked 74 | X-Frame-Options: 75 | - SAMEORIGIN 76 | status: 77 | code: 200 78 | message: '' 79 | - request: 80 | body: null 81 | headers: 82 | Accept: 83 | - '*/*' 84 | Accept-Encoding: 85 | - gzip, deflate 86 | Connection: 87 | - keep-alive 88 | User-Agent: 89 | - python-requests/2.25.1 90 | method: GET 91 | uri: https://coastwatch.pfeg.noaa.gov/erddap/griddap/erdTAgeomday.csvp?time%2Caltitude%2Clatitude%2Clongitude 92 | response: 93 | body: 94 | string: !!binary | 95 | H4sIAAAAAAAAAHWby67syHFF5/oKDy2ALVREJPOhqecayRNPjAbUkAXoAUjt//c5jGBx3+UsQALU 96 | 1ans6BtrM0iu5K9/+dsv//bv//nH//jt8fNff/3Lr//7p6+//Ntvj7/+fP/Fn3758z9/+eVf//33 97 | f/zz1//5+hv/+Puf8Xd++flfv/72N7aW/2Svn6z/0fz3r9fXf/7reP3udfw02u/mOL/+t/lZy+x7 98 | 2bXmWvaHn/9wLev+vSzGvcx/3O1eFtduPXeLn162X2bXbnPcy752O//fPzSu2uyu7WtZ7HaLqza7 99 | a/ta1nb/CnHVZlLbud/Nrt2e2vpuN79qc6lt7HbzqzaX2uZ+WVy7PbWt/T/Urt3etbGnuewq/oin 100 | tn1Prz+KI57a9j29/mCPuGtrH3p6temIu7b2oadX049219Y+9PRC6Gh3be1DTy8gjya1bXt64X00 101 | qW3X076u2k6pbdfTr2XftZ1S266nX8vi2u2pbdfTr2V27faubdvTPq/a+lPbtqdfy75r609t255+ 102 | LYtrt6rt3Pf0a5ldu1Vt576nfVy1jbu2c9/Tr2XftY27tnPf034tOIbUtutpv/5xx5Datj29ij+m 103 | 1Lbt6fVHcUypbdvT6w/2mFLbtqdXm4751Lbv6dX0Yz217Xt6IXSsp7Z9Ty8gj3XX1j/09ML7WHdt 104 | vXrKZTkX7D0Y+oem5mCw92ToH7qak8FeUt62rTka7CX1bftas8Gkvm1jaziY1LftbE0Hk/q2ra3x 105 | YE99+97mfDB/6ts3NweE+VPfvrs5Iew9IsaH9uaIsPeMGB8ymzPC3kNifOhvDgl7T4nxob85JSyk 106 | vm1/c0xYSH3b/uacsCb1bfubg8Ka1Lftb04Ka1Lftr85Kqw99W37e+assPOpb9vfM4eFnU992/6e 107 | OS3sPS7mvr9njgt7z4u57++Z88LeA2Pu+3vmwLD3xJj7/p45MaxLfbv+njkyrEt9u/6eOTNsSH27 108 | /p45NGxIfbv+njk1bEh9u/6eOTZsPPXt+5tzw+ZT376/OThsPvXt+5uTw96jY33ob44Oe8+O9aG/ 109 | OTvsPTzWh/7m8LD39Fgf+pvTw5bUt+1vjg9bUt+2vzk//CX1bfub88NfUt+2vzk//CX1bfub88Nf 110 | T337/ub8cHvq2/c354fbU9++vzk/POeHv16vD/3N+eE5P3Ldbv6e9XyR8yPXbftbDxg5P3Ldtr/1 111 | hOFS37a/9YjhUt+2vzk/PKS+bX9zfnhIfdv+5vzwkPq2/c354fHUt+9vzg9vT337/ub88PbUt+9v 112 | zg9vd332ob85P7zd9dk+vy3nh593fbbvb8v54eddn+3723J++Cn17frbcn74KfXt+ttyfniX+nb9 113 | bTk/vEt9u/62nB/epb5df1vOD+9Pfdv+tpwfPp76tv1tOT98PPVt+9tyfvi46/N9f1vODx93ff6h 114 | vzk/fN71+Yf+5vzwedfnH/qb88On1Lftb84Pn1Lftr85P3xJfdv+5vzwJfVt+5vzw5fUt+1vzg9f 115 | T337/ub8iNdT376/OT/i9dS372/Oj3jd9X14NdVyfsTrru/Du6mW8yPsru/Dy6mW8yPsru/D26mW 116 | 8yNM6tv2N+dHmNS37W/Oj3Cpb9vfnB/hUt+2vzk/wqW+bX9zfoQ/9e37W++o4qlv3996SRVPffv+ 117 | 1luq9/z48Jqq1Wuq9/xo+/nbcn7Ee358eFHVcn7Ee358eFPVcn5Ek/q2/c35EU3q2/U3cn7EKfXt 118 | +hs5P+KU+nb9jZwfcUp9u/5Gzo84n/q2/Y2cH9Gf+rb9jZwf0Z/6tv2NnB/xnh8fXllFzo94z48P 119 | 76wi50e858eHl1aR8yPe8+PDW6tccMSQ+nb9zX/gEUPq2/Y350dMqW/b35wfMaW+bX9zfsSU+rb9 120 | zfkR86lv39+cH7Ge+vb9zfkR66lv39+cH/GeHx9eXyXwR7znR//Q35wf7T0/Pry/ykAe7T0/Pry/ 121 | yoAf7SX1bfub86O9pL5tf3N+NJP6tv3N+dFM6tv2N+dHM6lv29+cH82e+vb9zfnR/Klv39+cH82f 122 | +vb9zfnR3vPjw/urHFhHe8+PD++vcgAe7T0/Pry/yoF6tPf8+PD+Kgf00ULq2/Y350cLqW/b3/Ic 123 | Terb9rdER5P6tv0t09Gkvm1/S3W0p75tf/MG7GjnU9+2v3lDd7TzqW/b37xBPNp7fnx4f5U3nEd7 124 | z4+5n795A3u09/z48P4qb4iP9p4fH95f5Q320brUt+tv3rAfrUt9u/7mA8DRhtS3628+UBxtSH27 125 | /uYDytGG1Lfrbz7wHG089e37m/Ojzae+fX9zfrT51Lfvb86P9p4fH95f5QPj0d7z48P7q3wAPdp7 126 | fnx4f5UPtEd7z48P76/yAfloS+rb9jfnR1tS37a/OT/ycf43798u8Zez4v1b5Lqu6yzXzWddXu/z 127 | 9cXz27Wf6X55Hc/XJs9vlutkv7w+5+ua57drP9f98rqbr4me3yzXyX55Pc3XU89v136h++V1Ml+L 128 | Pb9ZrpP98vqXr+Oe3679mu6X17V8Dfj8Zrnu2c/KzZ6yn5WIPWU/K+t6yn5WivXU/fL6kq9bn9+u 129 | /brul9eNfM37/Ga5TvbL60G+Xn5+u/Ybul/mPF9rP79ZrpP9Mr/5Ov357dpv6n6Zy3yN//xmuU72 130 | y7ylPnh+u/Zbul/mKLXF85vlOtkv89E1HylXjq75SDFzdM1HSp2jaz5SCB1d85Ey6eiajxRRR9d8 131 | pMQ6uuYjBdjRNR8pz46u+UjxdnTNR0q7o2s+UvgdXfORsvDomo8UjUfXfKSkPLrmIwXn0TUfKUeP 132 | rvlIsXp0zUdK2aNrPjIeXeOR6eiajgxH13BkNrpmo44aaDTqWIEmo44QaDDquIDmImPRNRaZiq6p 133 | yFB0DUVmomsmMhJdI5GJ6JqIDETXQGQeuuYh49A1DpmGrmnIMHQNQ2ahaxYyCkOjkEkYmoQMwtAg 134 | ZA6G5iBjMDQGmYKhKcgQDA1BZmD8MCNyrx9GRO71w4TIvX4YELnXD/znXj/gn3v9QH/u9QP8uZey 135 | n+gPRT/JH0p+gj8U/OR+CPf1i2Bf/zehvvYW6KsAYb6qFOTrX0WIr39fAb7+UIT3+pMT3OuPV2iv 136 | Hgjs1ShhvbopqFfLhfTiQkAveITzIkwwLwyF8mJVIC+ghfGkfgriGY0phGd+pgCeIZvCdx2MEbzr 137 | RI3QXUdxBO46wyNs59VhCtp5CZlCdl5npoCdF6MpXOcVawrWeVmbQnVe+6ZAnRfIKUznVXQK0nmp 138 | nUJ0Xo+nAJ0X7Sk816V9CtA1AaYQXYNiCtI1T6YynVBPhTqpnkp1Yj0V6+R6Ctc1EqeAXZNzCtk1 139 | YKegXXN4Cts1rqfAXVN9Ct01/KfgXfcIU/iuW4kpgNcdxxTC68ZkCuJ1/zKF8brNWQJ53Q0tobxu 140 | mpZgXvdWSzivW7AloNed2hLS64ZuCep137eE9bo9XAJ73UUuob1uNpfgXvekS3ivW9clwNcd7hLi 141 | 60Z4CfJ1v7yE+bqtXgJ93X0vob5u0pdgX/fyS7ivW/4l3NeTwRLu6wFiCff1nLGE+3ocWcJ9PbUs 142 | 4b4ebpZwX89AS6/nyf3SC3pyv/SKntwvvaQn90u4r8e4JdzX094S7uuhcAn39ey4hPt6xFzCfT2J 143 | LuG+HliXcF/PtUu4r0diewn49Tj9/VvIul7ruqybte7Zr46wvAT+en3w/VvIul7ruqybte7Zr47s 144 | vCQA9brk+7eQdb3WdVk3a92zXx1RekkI6vXQ928h63qt67Ju1rpnvzqS9ZIg1Ouw799C1vVa12Xd 145 | rHXP7K8jaC8JQ73++/4tZF2vdV3WzVr37FdH7l4SiHrd+f1byLpe67qsm7Xu2a+OGL4kFPV69/u3 146 | kHW91nVZN2vds18dqXzp3U4dx3zp/U4d5XzpHU8dA31JNuI+QirhiPv4qaQj7qOrEo+4j71KPko5 147 | mEk+SleYST5KdZhJPkqTmEk+SrGYST5Kz5hJPkrtmEk+SguZST5KKZlJPkpHmUk+SmWZST5Kg5lJ 148 | PkqhmUk+Sr+ZST5K3ZlJPkr7mUk+ShmaST5KN5pJPkpVmkk+SnOaST5KkZpJPkqvmkk+Ss2aST5K 149 | 65pJPkoJm0k+SiebST5KRZtJPkpjm0k+SoGbST5Kn5tJPkq9m0k+StubST7afeRY8tHu48qSj3Yf 150 | dZZ8tPuYtD4TVD5MnwoqH6bPBZUP0yeDyodJPupIh7nko46DmEs+6iiJueSjjqGYSz7qCIu55KOO 151 | v5hLPurojLnko47dmEs+6siOueSjjvuYSz7qqJC55KOOGZlLPuqIkrnko443mUs+6miUueSjjlWZ 152 | Sz7qSJa55KOOc5lLPuoomLnko46RmUs+6giaueSjjq+ZSz7q6Ju55KOOzZlLPurInbnko47rmUs+ 153 | 6qifueSjjgmaSz7O+4i65OO8j7dLPs77aLzk47yP1Us+6kilueSjjmOaSz7qKKe55KOOgZpLPuoI 154 | qbnko46fmks+6uiqueSjjr2a67Nz5SP06bnyEfr8XPkIfYKufITk4z4iHJKP+3hxSD7uo8kh+biP 155 | NYfk4z4SHZKP+zh1SD7uo9gh+biPcYfk4z4CHpKP+/h4SD7uo+ch+biPrYfk4z7yHpKP+7h8SD7u 156 | o/Yh+biP6Yfk4z7iH5KP+/OAkHzcnxaE5OP+LCEkH+9PGiQf788hJB/vTykkH+/PMCQf9yccIfm4 157 | P/8Iycf96UhIPu7PTkLycX+yEpKP+3OXkHzcn8qE5OP+zCYkH/cnOiH5uD/vCcnH/WlQSD7uz4pC 158 | 8lFfJFmTfNTHTNYkH/UdlDXJR31CZU3fMVU+mr5lqnw0fc9U+Wj6pqny0SQf9aWZNclHfaRmTfJR 159 | 37dZk3zUp3HWJB/1VZ01yUd9kGdN8lHf8lmTfNRngNYkH/UFoTXJR318aE3yUd8tWpN81CeP1iQf 160 | 4/4ERvIx7s9nJB/j/vRG8jHuz3YkH/VlqDXJR31Uak3yUd+jWpN81Kes1iQf9RWsNclHfUBrTfJR 161 | 395ak3zUZ7vWJB/3F79N8nF/LNwkH/d3xk3ycX+i3CQf9XWzNclHfRhtTfJR31Rbk3zU59jWJB/f 162 | /zUV0fcPwRWdK7CHcQ/jHsY9jHs493Du4dzDuUdwj+AewT2CezTu0bhH4x6Ne5zc4+QeJ/c4uUfn 163 | Hp17dO7RucfgHoN7DO4xuMfkHpN7TO4xucfiHot7LO5BTjs57eS0k9NOTjs57eS0k9NOTjs57eS0 164 | k9NOTjs57eS0k9NOTjs57eS0k9NOTjs57eS0k9NOTjs57eS0k9NOTjs57eS0k9NOTjs57eS0k9NO 165 | Tjs57eS0k9NOTgc5HeR0kNNBTgc5HeR0kNNBTgc5HeR0kNNBTgc5HeR0kNNBTgc5HeR0kNNBTgc5 166 | HeR0kNNBTgc5HeR0kNNBTgc5HeR0kNNBTgc5HeR0kNNBTgc5HeR0kNNBTic5neR0ktNJTic5neR0 167 | ktNJTic5neR0ktNJTic5neR0ktNJTic5neR0ktNJTic5neR0ktNJTic5neR0ktNJTic5neR0ktNJ 168 | Tic5neR0ktNJTic5neR0ktNJThc5XeR0kdNFThc5XeR0kdNFThc5XeR0kdNFThc5XeR0kdNFThc5 169 | XeR0kdNFThc5XeR0kdNFThc5XeR0kdNFThc5XeR0kdNFThc5XeR0kdNFThc5XeR0kdMFTv0FTv0F 170 | Tv0FTv314h7GPYx7GPcw7uHcw7mHcw/nHsE9gnsE9wju0bhH4x6NezTucXKPk3uc3OPkHp17dO7R 171 | uUfnHoN7DO4xuMfgHpN7TO4xucfkHot7LO6xuAc5NXJq5NTIqZFTI6dGTo2cGjk1cmrk1MipkVMj 172 | p0ZOjZwaOTVyauTUyKmRUyOnRk6NnBo5NXJq5NTIqZFTI6dGTo2cGjk1cmrk1MipkVMjp0ZOjZwa 173 | OXVy6uTUyamTUyenTk6dnDo5dXLq5NTJqZNTJ6dOTp2cOjl1curk1Mmpk1Mnp05OnZw6OXVy6uTU 174 | yamTUyenTk6dnDo5dXLq5NTJqZNTJ6dOTp2cOjkNchrkNMhpkNMgp0FOg5wGOQ1yGuQ0yGmQ0yCn 175 | QU6DnAY5DXIa5DTIaZDTIKdBToOcBjkNchrkNMhpkNMgp0FOg5wGOQ1yGuQ0yGmQ0yCnQU6DnAY5 176 | beS0kdNGThs5beS0kdNGThs5beS0kdNGThs5beS0kdNGThs5beS0kdNGThs5beS0kdNGThs5beS0 177 | kdNGThs5beS0kdNGThs5beS0kdNGThs5beS0kdNGTumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrp 178 | o5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOc 179 | Psrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K 180 | 6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumj 181 | nD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+ 182 | yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrp 183 | o5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOc 184 | Psrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K 185 | 6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumjnD7K6aOcPsrpo5w+yumj 186 | nD7K6aOcPsrpo5w+yumjnD7K6aOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+ 187 | Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiro 188 | o4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOC 189 | Piroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q 190 | 6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuij 191 | gj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+ 192 | Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiro 193 | o4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOC 194 | Piroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q 195 | 6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuij 196 | gj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4q6KOCPiroo4I+Kuijgj4qykf9H5zVZInK 197 | dQAA 198 | headers: 199 | Connection: 200 | - close 201 | Content-Disposition: 202 | - attachment;filename=erdTAgeomday_0b0c_b87b_ce62.csv 203 | Content-Encoding: 204 | - gzip 205 | Content-Type: 206 | - text/csv;charset=ISO-8859-1 207 | Date: 208 | - Fri, 21 May 2021 08:14:19 GMT 209 | Last-Modified: 210 | - Fri, 21 May 2021 08:14:19 GMT 211 | Strict-Transport-Security: 212 | - max-age=31536000; includeSubDomains 213 | Transfer-Encoding: 214 | - chunked 215 | X-Frame-Options: 216 | - SAMEORIGIN 217 | erddap-server: 218 | - '2.12' 219 | xdods-server: 220 | - dods/3.7 221 | status: 222 | code: 200 223 | message: '' 224 | version: 1 225 | -------------------------------------------------------------------------------- /tests/cassettes/test_remote_connection.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - python-requests/2.24.0 13 | method: GET 14 | uri: https://coastwatch.pfeg.noaa.gov/erddap/info/cwwcNDBCMet/index.json 15 | response: 16 | body: 17 | string: !!binary | 18 | H4sIAAAAAAAAAL1ca3PjuJX9Pr8C1V/GXSXLkvxq91RtrdvunmTXdjsjZzrJKqWCSUjiNglqCVIc 19 | TSr/fe/Fgw+JBEjJyVSPTFHA4QVwz30R5D9+IORdSl9D9u4j+Qd8ga9eHGYRf6IRE3Dyf979Eufk 20 | Zbtm7wbk3a80CbA1wZ/xxG2aJsFrlpZn7mlKK+3DjL37+6CKjL8p5Cn05Uts1+nIwCRxLvvLbwRw 21 | qBECGz/dzX9++Prp9gG/eH4090GgeaoFKkFfgohNWRKALBrYDZVCHyH7zDd6JkQdVaQ0DWI+IGHM 22 | l0Ga+QwO4RQedbxOzNVPcTLnek5L/Kevt7fk6f7TXX+wJA53wKZxlniMxAuCczTsBnkX8w3jOMid 23 | od99vf3lfjogd19Ox8OrAbm9u7+Ho/OOkiaMopQsokFYB2aJP0QB/5PHlA6X8aYfYtskPn6Zkum3 24 | L9M78vmX+36Q+9oUcIGLjNPSDypLdka7StO1+Hh2luf5cL1gy2G/UcNEsbkEZ34deDKajE9H56fj 25 | mx5IgRDZsUCfqUhZwqNYpHM81jB+nL0qjRxf3wxHo3E3tAUMLUvYy9GEXrJYrIGaNJwDQ+cR/a0u 26 | 1fV4eH354TCsgNexTi8vh6ODoDIepDtE89kyYWCCeJykq/6gMd8fa68V2AXbG+z4+hqm7iAwy3AZ 27 | qE5vzA1L0sCDg3UsgjTY7OiMH+cd+dqE2SBs1A1tFQgg/7be9zaJM+6TdMXI5DJdoV1m1FuRCIz4 28 | akD2zCBB6cOY+kL2AW1hIiVbRpNwSyggyY5wrK6GIpNh+ls6XP5OFgG4LrJI4ohUDQ73X73C4Jzh 29 | 1c7Kzmci9SOWnknsJeMswSuSmLPaFbgn0ckiTpT82icOG0ZAQxFXhkHDEEeNo7m49OmWcBgMAWMW 30 | EnS9UvoeomNH7DdpEnkXulXsGf8MK74llwQ0HRZTdFgKmA4ciF4SbKoExgFqXCGvpBYO2kzICrxx 31 | t3Ep2Hn8KiqHcm5wmNnal4NE6JZBio6+Ptix/F6eexh8PLKOTAz4Iv6zzcPVR9cVtPS1zcHRgBzs 32 | 4r+zbR4n/g6taZAMCHzM1wkDj5iwOZhmweg8ZBsWqp9SFq1Rv+BXOJGCu1uvWO048OAL6BJdwtlX 33 | JDtNtnCUxfDpxaAGOU09oDquMnyynKzjgKfycC4P61fxg4R5Kt70Y9BOim0/U3ALZOoFjENw9x/k 34 | thAFvwQJ+VNGwyDdwrdfAxG8BvjF1S1UISwcPoeUsxQEJ5/0CMgD3bKE/IEFy5Xz+uVckGc9l22n 35 | H8Hiw98Iok1xGGxx+MK4D/2cw2yGmTJKHnChi5MH4iDzvUNAXspV14v4UtWDA3HuQcGeUasORPsG 36 | diYhv9I1GLI3QAs4mE+YpSxZUGgqv+/3/+oxyoU52BkRrpTpb5ViB+Ub0FJeO1jyYAF+DEaB51qV 37 | uq1/zsLQLbRpLi/xDKOPfXAqkv9J5WiewxQMpEsYkGUmQI6VlifwMQdbBCzB/HIr/yhTFNVYEwG4 38 | DwaCyl9SFkMWGC/RUVe/Ay8wS4JPMMfwCfYYP2EMSqDiUEsU4yj0H7jIWo9gbUwDHBY6/n/K2gwI 39 | mEv5YQRF8ynUWs0Fzts8h+mYG7CWn0W5QnMzGS1N03heMZDVRjUjCj/kqMhwVGLDlzVjKAbOOrhs 40 | tHT+QDlvb0CKPF9DFgcELy2Kr3OqLSecQcn0H2hQkyEt7JM5mseLedXhDKQHH5ANsg3+VCx3eTwP 41 | +Fy6Kj0glGVgJMo9EFetH37OUa2qEyRP6lGXxyiHUr1c8fF3VKZ+vnS+iT36moV0N+L9+e7xvmDJ 42 | fxvP2wk7DDzGxW4KCCGPDLYiiB1fGckE82VMlDAfQjIF5cvAawE5BXjelAQC1Bv+cDnz/ozjryFb 43 | QswE3VEpULgglZhYT6EBh9bU87KEguhiSJ5YAMGWCubw8jN+V9ZdBhh7qIBEEkm2+jNkDiAIOgQw 44 | BD/HG8yPGaodtqB8O+MqDgZLD3oSxluGYWsiBYDLAi6Sm36Hs9AaljhJQGtBGdhvUmOg7YwH0DPA 45 | 5YQhhJkPk2QaBoi2QOvgreArVeojp2oRpBwBcBpgJGuKyQ6uHVlnCWRRMCX4CyhlpC+uJisMDIqJ 46 | a/UUbQcz7sUgCpgbRJb9YWoXWai/4khhGTBYTCIVdHdTAjBaMKg5zdIVZB/pjnZBRKlCS1lGKeOr 47 | bthPaPNM2UJ+2atb9KkQrJP4f4FpLQGrnPmDY9Y1CBQIEPYNq2cl5hvWz0rQN6iglWBvXUPTLmsn 48 | OwcyR1hVI3+6I96Ked8FmETw8sphQQATUQ4d4eduVxGy+lq/iHYrBPI5lmxo98lQYG+eak3jzEWC 49 | HqUt40alSrU6hbsvaBdlQ3lTgbzIOw6b665XyWDy0l//VfV5sHvRnszoeJ4kMiiAvAPyCZI6cgcm 50 | HfzCCVL8PSldkNiJxcoKwQymOgZvJZNCaAbOBh0OntiWliLGRQHH81htSyECRrObM4o/z8A7gqHn 51 | LBSmpgKRzpCAqFvZ1mfSr/hgd+Wv0kCCLPICi4UAk8pUICHAKWJZQqHLYi5E7iH6EY+ksTz/DL4Q 52 | Qicd4FJVyvoDRFZBMFB+RSPM+CeG0yZjdd1ZqpmBGMpx/giDr82ECmslFHhALqIgnfFXuBBMJaYP 53 | JlD6SQYq1dRYxzSyBASRzE+YrctvMB8zXonDfpJnMVwiWCpabrGrB1dTwuerAMtClRRBNtVR6Iyb 54 | FFydNnEsQuqkv/qDXoIk2DB/SD5vGFeTU8iNi6baJ/GaLpVdCIqJgDXiaHG2tWkawqwCSDvtsbcP 55 | MYtYpVGofDxIIbwkWJuL4oJUk4jhjM84KriyMZVqVum+VlTAjC8WkNXB+NV9PplSiEHlvKyXwkRR 56 | uT7mbBQIgfqwwXuFGAVUfpPFqkGp8zgZQOoINPIEojYg+BapnEoVx/uCyiT7GQQ+Hqguhholmr6C 57 | iU8EGheMqiHKwbz1PZIDZhgHCBYEJ5sWoX/wO+pQwlSIkhqFCnHtwy2B+DLF4BEWWhTzSNPqnA2I 58 | vi0jqbzvQsmJ9N4fZ9y4bAIAZuVAuE94cUYmo8nodDQ+ndwMVNxk5I051nqpX4lDpQw5LI4XQuQm 59 | UiQcJcsA1Q3rjTAiJCrIpJmIBUNshz8CEeNcXQP0Ul8EV7pSplVibkBuaaSVWqBGyHXAFhCYLQOu 60 | K5AwQXINhuSPsj8SGv6PMsiSIEA00yVXUimnkhWVu3oCRiazITlkvAr0KC+J3gomjp8+0SclodEt 61 | Qb6zdVq5Og4NMOnaWEG1UlQCYoS8o5yon+CacRVPlD3wKMSyqCkcMpIZR6h8xUow7LNM6HrFfFjD 62 | 21DEYJHAaBGIzmMAx2mrygjzMeNAPcgJ/IHMUIpFNrkKWJAFGAkYuheCtd1XWw7CqzIwqCSrV681 63 | nSsdcElfgV7VKr683IkOhFTWEYehlEhq4Yybu4Cj8cto9FH++9t7yYmdorOCCjGnaMCbcTWNMruC 64 | XASsJE1lWqFWWGpU07Vg7d93TBOwRP41S78uwDPvBFwwU6eTSz01HdFArrkXK5M+h6SxjqhkvTgd 65 | Xb1Mxh/PL1HWQ4DB8CQ76cL45hqYPzmdALSZhq7Q6e7Nf8nSItB6rMcjMnq5l1SQF0XnCha028W+ 66 | Mcf93oYbhGY7RSVGw8O6yK2Xr/TwFg0bHSr7NgK/E0ocoe/bD/JMgF7W39p1sIIXxLGYo0Na7t31 67 | +2OB1AUIw9aGjGyqxWoGq05uEfea6V2EMU2ts1vrMr+DcMMHg56y298CsX8b/sGSstSQqJeCOZgn 68 | yPmaIFo9QPUcd6XrcCBNXZK/dOsI0UqcfKLJIyBEWVRX1fGHkSW7acYJ+D7OaS+gFuUrWhUBfeqo 69 | ltRwLUr4ALmYNdmsAbUo4UPRphNKLRusI4X9kHpsGqhRweR93ZlQ6eEmAm23l1Wgdh5gZj0gjipT 70 | DWqPA3/t1M9KgRur4jahNBKgB0yb+utG3bW/gnqU8ldw2nTfVUOoYtg0vw9On71BVcVHd2iUvlyh 71 | 1kua5k6Nxx1YTpRddS8FuLgZjy5GI9T58fAKnMDFxc3n9j1mBeCe0r84+1i0odMgWrSgU1/L6qdd 72 | +sswUWVV9d6QE/7X7dMpBmzEhMlOtAYdEgzic1/oOx8q6BzXYu82xcp9o1ZYPbLbUtV2/iUIQ7VL 73 | uNrrfHJ9de3ouqtIRWfQn/PLdr1Rva0m7/zKZqx2AJqsXZfuDVbuW61yRU5kDll8leU3bAG5m4e1 74 | pqXKjrBiomiPab73PQ+EzsTTJGPk6T3xM1lxK4oeugQlb49hVv1t+nzfbkaVwBbKoNiO3i2EkQO+ 75 | NyN0YOhcfL45SF0stGu4HeoAs1henPNWfoh1wRB3tGFa1zlShgc36j+bqmmE1hBDGtubqw4Q9ijZ 76 | dgtgD6E3YQqABsrcmtIqapKs9JKT6ExYSgMG7mCF1v1tKj1FQZwIewp9yNq69Fp0EqVpKysRp+M2 77 | TV6KtLsi68aH67EGsKvx9eWwfc+xRrBbfZsS7gL0VeKif4MOPzP6nVxirfoDUc5X3qjops4a+FBt 78 | 1t1tyvwzymLXaI1ylEJrjE76bHaluMD6qnW+WfXQa9P6CAOtERwGejI8t8QzGsNuoa0Gdheht4Uu 79 | ABq0e9p8wwz0muG9xfcynKEh7jHBsjpVO5fNLTOd662gC96XiDk7TVdBgrcoq/chKsCiGu5MRqeq 80 | uIuBzzrE0yr4sfgHPRgLo4otirizygnUxq1yf6ET4jhPoUEszKpujmvZZueEdz0NUeWZ3ycO8o8N 81 | g/wuUdDVhRvBSrGJjSG7AH0Z5ltCoPuG+87kRGdxkl7IBH2+vEmmxlC93d3OCd8ZMnWjhG8Pnarb 82 | Yc2oXFhHUcN3xlDWXaou3Kbkuo0StA8l6LGUoJ0SA1tYT4+lxC5AX0rQLllBIyO065D7YpudhctH 83 | 0DfiA+3OBz0kF9RRdKD/IjrQ/nSI+tSSoiOKSdFx1aTo6HLSLkJfHkSWgtIjbsaSJCjLSF6cJEys 84 | gQpS72Oz18ns6DDexNDm/uvj870lAYmc9aFuXIjslSIchLtSFB1XKoqctaK9SKn6lIEL98Cy0StN 85 | ursG3fhw16ABWl3Dh9FoeD0g4/HNh2H7bSmN4khKLm2KvQvRRIybbghNLiJIij2D5GT1TN8PycmP 86 | z798nv6IW+vkFh25R0K+HkMM35MvkKDfnT7ePhERpHLvO+TGuKWLPMiN+HIH3kBtRwJXk+C2quIK 87 | EIUlzM88tdWqeAyGZMJ4H0iHVsA3tRvvVe3LfPo2JS/MW3G5K+M5iaE/oAnyKQtDlkKLyc2YnIzH 88 | Z+OLsw8jC0v1VFhYah5Lc0G0EBTnsyvEUU5KY1g42vqgpguzgZ+gGK0RWxqtu/PStD4iZtMIlo0T 89 | l+fDiwGxcsKgWJl50QuhecNDN4gWZlZ25ZKTOxaKIBNAUKSgYFzAH5Psx9wQTzDIp4EX5L7c0Coa 90 | +wBlNZH1s9iqszo3VW3NXWHLs9JmFLa7ieU4nDAWYvWBOS4C1CAOdqU95Gl1evO7NmblvZiVH82s 91 | 3Mks8HXXDmLlXYh1PumD0LsOZ6MV7rc3Dym66eWzNb50oolTltLZ2xAitxNiOm2/wZ+/BQlyNwla 92 | niZ1Qh5ABp/lPchgWh9RLdMIFjLcDG8G5AIo4QQ53MvsIfQumRUATTUz+FE+q15lQgphHEfnUNwo 93 | 16VqU5PecUyVpyQspTMtxpGcMDAtnCjG0wfruNKZBrGQpPGtFU7AAyiyUTuAujFENz6cIBrAUU6+ 94 | srBDIziSI5ty7yL0JUfRv8lR6M3E5XPl5OR7NCie5Ai3hNNMvgKJRPJlPPrZCZkule/oeY8PkKRM 95 | PQgjn+AoXn2DD32EQSQfgoZ0KGFrSM/1y29GeGY8vCI8CtpppQdgYVW5pX3rQnHsqi5fk+ICOopR 96 | GsNCqL0n/V1YDVz63npTZp362+40Mq0P55FBsOQz58PJgIxtd2YMiD3q6gPQmM10Q2i6t7/7Chpz 97 | P6ayzWsdZvLhfiw+C/UMjXQ3UZxx+R6pooqgn5CR5Qr9PFS6wjcZ4PNapmLHVGVP+zH5DA5gVB4p 98 | bmeVGcoxZQKD0UKqvQlxAh1FKgNi23XZ8soNJ2a/ckEa9NlqblofTi+DYInjhueQ1FwNx+3v6DQg 99 | VnrZbhPtATTSqxtCy75w+YyyLqeBJ1LbCshJxV0tGD4X1+ykCH2NN/JB0VcWxjmRJfOHOGdYAsj1 100 | O45OHh8evlmqa0ZGWxHcvETKCdJaAUdJukEcRRkDYkt/dt5u48TqszkgF+usO1FM66N2SWYOosjs 101 | /+bassHMgBy1T7KO0FxW6wbRQhX58p69vZJADV8+Lq08U4a7i9cxZ8r55PJB2erm4yXE9MuBeXpd 102 | RW31nbzSh5U7Ia2bMTMHcVybMTMbZwoRBuRv1hcXGaBj92RmDubU3u3lBOq9f02sN72oszmaOpu3 103 | oM7maOrUEQ6izsZKnfJNai7+bP69/NkcyZ9NR/48FhPgRDuWRBsHiervo3MitbNI9sPPf/7wzx/+ 104 | Hzyrd4oNXwAA 105 | headers: 106 | Connection: 107 | - close 108 | Content-Disposition: 109 | - attachment;filename=cwwcNDBCMet_info.json 110 | Content-Encoding: 111 | - gzip 112 | Content-Type: 113 | - application/json;charset=UTF-8 114 | Date: 115 | - Tue, 06 Apr 2021 22:00:19 GMT 116 | Strict-Transport-Security: 117 | - max-age=31536000; includeSubDomains 118 | Transfer-Encoding: 119 | - chunked 120 | X-Frame-Options: 121 | - SAMEORIGIN 122 | status: 123 | code: 200 124 | message: '' 125 | version: 1 126 | -------------------------------------------------------------------------------- /tests/test_erddap_dataset.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from erddapClient import ERDDAP_Tabledap 3 | import datetime as dt 4 | 5 | 6 | @pytest.mark.vcr() 7 | def test_remote_connection(): 8 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 9 | datasetid = 'cwwcNDBCMet' 10 | remote = ERDDAP_Tabledap(url, datasetid) 11 | datasetTitle = remote.getAttribute('title') 12 | assert datasetTitle == 'NDBC Standard Meteorological Buoy Data, 1970-present' 13 | 14 | 15 | def test_request_url_quoted_strings(): 16 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 17 | datasetid = 'cwwcNDBCMet' 18 | remote = ERDDAP_Tabledap(url, datasetid) 19 | ( 20 | remote.setResultVariables(['station','longitude','latitude','time','atmp']) 21 | .addConstraint({'station=' : '0Y2W3'}) 22 | .addConstraint({'atmp>=' : 15}) 23 | .addConstraint({'atmp<=' : 23}) 24 | .orderByLimit(20) 25 | ) 26 | orderbyurl_test = remote.getDataRequestURL() 27 | print(orderbyurl_test) 28 | assert orderbyurl_test == 'https://coastwatch.pfeg.noaa.gov/erddap/tabledap/cwwcNDBCMet.csvp?station%2Clongitude%2Clatitude%2Ctime%2Catmp&station=%220Y2W3%22&atmp%3E=15&atmp%3C=23&orderByLimit(%2220%22)' 29 | 30 | def test_request_url(): 31 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 32 | datasetid = 'cwwcNDBCMet' 33 | remote = ERDDAP_Tabledap(url, datasetid) 34 | ( 35 | remote.setResultVariables(['station','time','atmp']) 36 | .addConstraint('time>=2020-12-24T00:00:00Z') 37 | .addConstraint('time<=2020-12-31T01:15:00Z') 38 | .orderBy(['station']) 39 | ) 40 | orderbyurl_test = remote.getDataRequestURL() 41 | remote.clearQuery() 42 | 43 | ( 44 | remote.setResultVariables(['station','time','atmp']) 45 | .addConstraint('time>=2020-12-24T00:00:00Z') 46 | .addConstraint('time<=2020-12-31T01:15:00Z') 47 | .orderByClosest(['station','time/1day']) 48 | ) 49 | orderbyclosest_test = remote.getDataRequestURL() 50 | remote.clearQuery() 51 | 52 | print (orderbyurl_test) 53 | assert orderbyurl_test == 'https://coastwatch.pfeg.noaa.gov/erddap/tabledap/cwwcNDBCMet.csvp?station%2Ctime%2Catmp&time%3E=2020-12-24T00%3A00%3A00Z&time%3C=2020-12-31T01%3A15%3A00Z&orderBy(%22station%22)' 54 | 55 | print (orderbyclosest_test) 56 | assert orderbyclosest_test == 'https://coastwatch.pfeg.noaa.gov/erddap/tabledap/cwwcNDBCMet.csvp?station%2Ctime%2Catmp&time%3E=2020-12-24T00%3A00%3A00Z&time%3C=2020-12-31T01%3A15%3A00Z&orderByClosest(%22station%2Ctime/1day%22)' 57 | 58 | 59 | def test_request_url_dict_constraints(): 60 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 61 | datasetid = 'cwwcNDBCMet' 62 | dtstart = dt.datetime(2020,12,24,0) 63 | dtend = dt.datetime(2020,12,31,1,15) 64 | 65 | remote = ERDDAP_Tabledap(url, datasetid) 66 | ( 67 | remote.setResultVariables(['station','time','atmp']) 68 | .addConstraint( { 'time>=' : dtstart } ) 69 | .addConstraint( { 'time<=' : dtend } ) 70 | .orderBy(['station']) 71 | ) 72 | orderbyurl_test = remote.getDataRequestURL() 73 | remote.clearQuery() 74 | 75 | ( 76 | remote.setResultVariables(['station','time','atmp']) 77 | .addConstraint( { 'time>=' : dtstart } ) 78 | .addConstraint( { 'time<=' : dtend } ) 79 | .orderByClosest(['station','time/1day']) 80 | ) 81 | orderbyclosest_test = remote.getDataRequestURL() 82 | remote.clearQuery() 83 | 84 | print (orderbyurl_test) 85 | assert orderbyurl_test == 'https://coastwatch.pfeg.noaa.gov/erddap/tabledap/cwwcNDBCMet.csvp?station%2Ctime%2Catmp&time%3E=2020-12-24T00%3A00%3A00Z&time%3C=2020-12-31T01%3A15%3A00Z&orderBy(%22station%22)' 86 | 87 | print (orderbyclosest_test) 88 | assert orderbyclosest_test == 'https://coastwatch.pfeg.noaa.gov/erddap/tabledap/cwwcNDBCMet.csvp?station%2Ctime%2Catmp&time%3E=2020-12-24T00%3A00%3A00Z&time%3C=2020-12-31T01%3A15%3A00Z&orderByClosest(%22station%2Ctime/1day%22)' 89 | 90 | 91 | def test_request_url_time_constraints(): 92 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 93 | datasetid = 'cwwcNDBCMet' 94 | dtstart = '2020-12-24T00:00:00Z' 95 | dtend = dt.datetime(2020,12,31,1,15) 96 | 97 | remote = ERDDAP_Tabledap(url, datasetid) 98 | ( 99 | remote.setResultVariables(['station','time','atmp']) 100 | .addConstraint( { 'time>=' : dtstart } ) 101 | .addConstraint( { 'time<=' : dtend } ) 102 | .orderBy(['station']) 103 | ) 104 | orderbyurl_test = remote.getDataRequestURL() 105 | print (orderbyurl_test) 106 | assert orderbyurl_test == 'https://coastwatch.pfeg.noaa.gov/erddap/tabledap/cwwcNDBCMet.csvp?station%2Ctime%2Catmp&time%3E=2020-12-24T00%3A00%3A00Z&time%3C=2020-12-31T01%3A15%3A00Z&orderBy(%22station%22)' 107 | 108 | @pytest.mark.vcr() 109 | def test_getattribute(): 110 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 111 | datasetid = 'cwwcNDBCMet' 112 | remote = ERDDAP_Tabledap(url, datasetid) 113 | 114 | dsttitle = remote.getAttribute('title') 115 | dstwspd_comment = remote.getAttribute('comment','wspd') 116 | 117 | assert dsttitle == 'NDBC Standard Meteorological Buoy Data, 1970-present' 118 | assert dstwspd_comment == 'Average wind speed (m/s).' 119 | 120 | @pytest.mark.vcr() 121 | def test_tabledap_time_range_attribute(): 122 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 123 | datasetid = 'cwwcNDBCMet' 124 | remote = ERDDAP_Tabledap(url, datasetid) 125 | 126 | time_actual_range = remote.variables['time']['actual_range'] 127 | print(time_actual_range) 128 | assert time_actual_range[0] == dt.datetime(1970,2,26,20) 129 | assert time_actual_range[1] == dt.datetime(2021,4,6,21,35) -------------------------------------------------------------------------------- /tests/test_erddap_griddap.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from erddapClient import ERDDAP_Griddap 3 | import datetime as dt 4 | 5 | @pytest.mark.vcr() 6 | def test_griddap_subset_parsing(): 7 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 8 | datasetid = 'erdTAgeomday' 9 | remote = ERDDAP_Griddap(url, datasetid) 10 | remote.setResultVariables(['u_current[(2009-06-16T00:00:00Z)][(0.0)][(9.375):(41.125)][(254.625):(286.375)]', 11 | 'v_current[(2009-06-16T00:00:00Z)][(0.0)][(9.375):(41.125)][(254.625):(286.375)]']) 12 | url = remote.getDataRequestURL(filetype='opendap', useSafeURL=False) 13 | urlquoted = remote.getDataRequestURL(filetype='opendap', useSafeURL=True) 14 | 15 | assert url == "https://coastwatch.pfeg.noaa.gov/erddap/griddap/erdTAgeomday?u_current[200:200][0:0][337:464][1018:1145],v_current[200:200][0:0][337:464][1018:1145]" 16 | assert urlquoted == "https://coastwatch.pfeg.noaa.gov/erddap/griddap/erdTAgeomday?u_current%5B200%3A200%5D%5B0%3A0%5D%5B337%3A464%5D%5B1018%3A1145%5D%2Cv_current%5B200%3A200%5D%5B0%3A0%5D%5B337%3A464%5D%5B1018%3A1145%5D" 17 | 18 | 19 | @pytest.mark.vcr() 20 | def test_griddap_subset_parsing2(): 21 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 22 | datasetid = 'erdTAgeomday' 23 | remote = ERDDAP_Griddap(url, datasetid) 24 | remote.setResultVariables('u_current[(1999-04-16T00:00:00Z):1(2009-06-16T00:00:00Z)][(0.0)][(9.375):last-10][(254.625):(last-73.5)]') 25 | urlunquoted = remote.getDataRequestURL(filetype='opendap', useSafeURL=False) 26 | 27 | assert urlunquoted == "https://coastwatch.pfeg.noaa.gov/erddap/griddap/erdTAgeomday?u_current[78:1:200][0:0][337:589][1018:1145]" 28 | 29 | 30 | @pytest.mark.vcr() 31 | def test_griddap_subset_parsing3(): 32 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 33 | datasetid = 'erdTAgeomday' 34 | remote = ERDDAP_Griddap(url, datasetid) 35 | remote.setResultVariables('u_current[1:10:200][0][337:589][1018:1145]') 36 | urlunquoted = remote.getDataRequestURL(filetype='opendap', useSafeURL=False) 37 | urlquoted = remote.getDataRequestURL(filetype='opendap', useSafeURL=True) 38 | print(urlquoted) 39 | assert urlunquoted == "https://coastwatch.pfeg.noaa.gov/erddap/griddap/erdTAgeomday?u_current[1:10:200][0:0][337:589][1018:1145]" 40 | assert urlquoted == "https://coastwatch.pfeg.noaa.gov/erddap/griddap/erdTAgeomday?u_current%5B1%3A10%3A200%5D%5B0%3A0%5D%5B337%3A589%5D%5B1018%3A1145%5D" 41 | 42 | 43 | @pytest.mark.vcr() 44 | def test_griddap_subset_outrange(): 45 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 46 | datasetid = 'erdTAgeomday' 47 | remote = ERDDAP_Griddap(url, datasetid) 48 | remote.setResultVariables(['u_current[(1890-01-01T00:00:00Z)][(0.0)][(9.375):(41.125)][(254.625):(286.375)]', 49 | 'v_current[(2009-06-16T00:00:00Z)][(0.0)][(9.375):(41.125)][(254.625):(286.375)]']) 50 | 51 | with pytest.raises(Exception): 52 | url = remote.getDataRequestURL(filetype='opendap') 53 | 54 | 55 | @pytest.mark.vcr() 56 | def test_griddap_subset_doesntmatchnumberdimensions(): 57 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 58 | datasetid = 'erdTAgeomday' 59 | remote = ERDDAP_Griddap(url, datasetid) 60 | remote.setResultVariables('v_current[(2009-06-16T00:00:00Z)][(0.0)][(9.375):(41.125)]') 61 | 62 | with pytest.raises(Exception): 63 | url = remote.getDataRequestURL(filetype='opendap') 64 | 65 | 66 | @pytest.mark.vcr 67 | def test_griddap_setSubset_query(): 68 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 69 | datasetid = 'hycom_gom310D' 70 | remote = ERDDAP_Griddap(url, datasetid) 71 | remote.setResultVariables('temperature, salinity') 72 | remote.setSubset( time=slice("2014-06-15"), 73 | depth=0, 74 | latitude=slice(18.09165, 28.25925, 3), 75 | longitude=slice(-91.67999, -81.47998, 3) ) 76 | url = remote.getDataRequestURL('opendap', useSafeURL=False) 77 | 78 | assert url == 'https://coastwatch.pfeg.noaa.gov/erddap/griddap/hycom_gom310D?temperature[1900:1900][0:0][0:3:277][158:3:413],salinity[1900:1900][0:0][0:3:277][158:3:413]' 79 | 80 | @pytest.mark.vcr 81 | def test_griddap_setSubsetI_query(): 82 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 83 | datasetid = 'hycom_gom310D' 84 | remote = ERDDAP_Griddap(url, datasetid) 85 | remote.setResultVariables('temperature, salinity') 86 | remote.setSubsetI(time=1900, 87 | depth=0, 88 | latitude=slice(0, 278, 3), 89 | longitude=slice(158, 414, 3) ) 90 | url = remote.getDataRequestURL('opendap', useSafeURL=False) 91 | 92 | remote.clearQuery() 93 | urlNi = (remote.setResultVariables('temperature, salinity') 94 | .setSubsetI(time=-77, 95 | depth=0, 96 | latitude=slice(0, 278, 3), 97 | longitude=slice(-383, -127, 3) ) 98 | .getDataRequestURL('opendap', useSafeURL=False) 99 | ) 100 | 101 | assert url == 'https://coastwatch.pfeg.noaa.gov/erddap/griddap/hycom_gom310D?temperature[1900:1900][0:0][0:3:277][158:3:413],salinity[1900:1900][0:0][0:3:277][158:3:413]' 102 | assert urlNi == 'https://coastwatch.pfeg.noaa.gov/erddap/griddap/hycom_gom310D?temperature[1900:1900][0:0][0:3:277][158:3:413],salinity[1900:1900][0:0][0:3:277][158:3:413]' 103 | 104 | -------------------------------------------------------------------------------- /tests/test_erddap_griddap_dimensions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from erddapClient import ERDDAP_Griddap 3 | import datetime as dt 4 | 5 | @pytest.mark.vcr() 6 | def test_griddap_dimensions(): 7 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 8 | datasetid = 'hycom_gom310D' 9 | remote = ERDDAP_Griddap(url, datasetid) 10 | dstDims = remote.dimensions 11 | 12 | assert 'time' in dstDims and 'depth' in dstDims and\ 13 | 'latitude' in dstDims and 'longitude' in dstDims 14 | 15 | assert dstDims['time'].size == 1977 16 | 17 | assert dstDims['time'].isTime 18 | 19 | # Depth values to dataset loaded (in cassetes) 20 | # Float64Index([ 0.0, 5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 40.0, 21 | # 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 125.0, 150.0, 22 | # 200.0, 250.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 23 | # 900.0, 1000.0, 1100.0, 1200.0, 1300.0, 1400.0, 1500.0, 1750.0, 24 | # 2000.0, 2500.0, 3000.0, 3500.0, 4000.0, 4500.0, 5000.0, 5500.0] 25 | # ) 26 | assert dstDims['depth'][dstDims['depth'].closestIdx(0)] == dstDims['depth'][0] 27 | assert dstDims['depth'][dstDims['depth'].closestIdx(5500)] == dstDims['depth'][39] 28 | assert dstDims['depth'].closestIdx(5501) == None # If solicited value its outside of valid range returns None 29 | 30 | # Test dimensions indexing by value 31 | dimIndexing = dstDims.subset( "2014-06-15", slice(0), slice(18.10,31.96), slice(-98, -76.41) ) 32 | assert dstDims['time'].timeData[dimIndexing['time']] == dt.datetime(2014, 6, 15) 33 | 34 | # Test dimensions indexing by positional integer index 35 | dimNumIndexing = dstDims.subsetI(time=slice(1900), depth=0, latitude=slice(0,385), longitude=slice(0,541)) 36 | assert dstDims['time'].timeData[dimNumIndexing['time']] == dt.datetime(2014, 6, 15) 37 | 38 | # Test dimensions indexing by positional negative integer index 39 | dimNumIndexing = dstDims.subsetI(time=1900, depth=slice(-2, 40), latitude=slice(0,385), longitude=slice(0,541)) 40 | assert dstDims['depth'][dimNumIndexing['depth']][0] == 5000.0 and \ 41 | dstDims['depth'][dimNumIndexing['depth']][1] == 5500.0 42 | 43 | # Test raise Exception if index its out of bounds 44 | with pytest.raises(Exception): 45 | dimNumIndexing = dstDims.subsetI(time=slice(1900), depth=45, latitude=slice(0,385), longitude=slice(0,541)) 46 | -------------------------------------------------------------------------------- /tests/test_erddap_server.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from erddapClient import ERDDAP_Server 3 | import datetime as dt 4 | 5 | def test_simple_search(): 6 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 7 | remote = ERDDAP_Server(url) 8 | searchURL = remote.getSearchURL(filetype='csv', searchFor='Gulf of Mexico') 9 | assert searchURL == 'https://coastwatch.pfeg.noaa.gov/erddap/search/index.csv?page=1&itemsPerPage=1000&searchFor=Gulf+of+Mexico' 10 | 11 | def test_advanced_search(): 12 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 13 | searchForString = 'Gulf of Mexico' 14 | protocol = 'griddap' 15 | remote = ERDDAP_Server(url) 16 | searchURL = remote.getAdvancedSearchURL(filetype='csv', searchFor='Gulf of Mexico', protocol='griddap', minTime='now-1month') 17 | assert searchURL == 'https://coastwatch.pfeg.noaa.gov/erddap/search/advanced.csv?page=1&itemsPerPage=1000&searchFor=Gulf+of+Mexico&protocol=griddap&cdm_data_type=%28ANY%29&institution=%28ANY%29&ioos_category=%28ANY%29&keywords=%28ANY%29&long_name=%28ANY%29&standard_name=%28ANY%29&variableName=%28ANY%29&maxLat=&minLon=&maxLon=&minLat=&minTime=now-1month&maxTime=' 18 | 19 | def test_advanced_search_dt(): 20 | url = 'https://coastwatch.pfeg.noaa.gov/erddap' 21 | searchForString = 'Gulf of Mexico' 22 | protocol = 'griddap' 23 | remote = ERDDAP_Server(url) 24 | searchURL = remote.getAdvancedSearchURL(filetype='csv', searchFor='Gulf of Mexico', protocol='griddap', minTime=dt.datetime(2010,12,24)) 25 | print (searchURL) 26 | assert searchURL == 'https://coastwatch.pfeg.noaa.gov/erddap/search/advanced.csv?page=1&itemsPerPage=1000&searchFor=Gulf+of+Mexico&protocol=griddap&cdm_data_type=%28ANY%29&institution=%28ANY%29&ioos_category=%28ANY%29&keywords=%28ANY%29&long_name=%28ANY%29&standard_name=%28ANY%29&variableName=%28ANY%29&maxLat=&minLon=&maxLon=&minLat=&minTime=2010-12-24T00%3A00%3A00Z&maxTime=' 27 | 28 | @pytest.mark.vcr() 29 | def test_parsestatus(): 30 | remotev211 = ERDDAP_Server('https://coastwatch.pfeg.noaa.gov/erddap') 31 | assert remotev211.statusValues != None 32 | remotev202 = ERDDAP_Server('http://erddap-goldcopy.dataexplorer.oceanobservatories.org/erddap') 33 | assert remotev202.statusValues != None 34 | 35 | -------------------------------------------------------------------------------- /tests/test_parsing_functions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from erddapClient.parse_utils import parseDictMetadata, parseConstraintValue, validate_iso8601, validate_constraint_time_operations, validate_constraint_var_operations 3 | 4 | 5 | def test_valid_iso8601dates(): 6 | validDates = ["2020-12-24T00:00:00Z", "2020-01-01", "2020-01", "2019-01-11T12"] 7 | print("Valid dates:") 8 | for testDate in validDates: 9 | print (testDate) 10 | assert validate_iso8601(testDate) 11 | 12 | invalidDates = ["20201224T00:00:00Z", "2020-1-1", "2020/01", "2019-01-11T12;22"] 13 | print("Invalid dates:") 14 | for testDate in invalidDates: 15 | print (testDate) 16 | assert not validate_iso8601(testDate) 17 | 18 | def test_extended_time_constraint_functionality(): 19 | 20 | constraintValues = ["max(time)-1months", "min(time)+1years", "min(time)+100seconds", "min(time)+20minutes", 21 | "max(time)-3hours", "max(time)-5days"] 22 | for testC in constraintValues: 23 | print (testC) 24 | assert validate_constraint_time_operations(testC) 25 | 26 | invalidConstraintValues = ["max(time)*1months", "min(time)+1year", "min(time)+100.50seconds", "min[time]+2minutes", 27 | "max(time)-3hourly", "max(time)-5weeks"] 28 | for testC in invalidConstraintValues: 29 | print (testC) 30 | assert not validate_constraint_time_operations(testC) 31 | 32 | def test_extended_var_constraint_functionality(): 33 | 34 | constraintValues = ["max(pressure)-10.5", "min(temperature)+5", "max(salinity)"] 35 | for testC in constraintValues: 36 | print (testC) 37 | assert validate_constraint_var_operations(testC) 38 | 39 | invalidConstraintValues = ["max(pressure)-10.5.3", "min(temperature)/5.2.3", "max(25+salinity)"] 40 | for testC in invalidConstraintValues: 41 | print (testC) 42 | assert not validate_constraint_var_operations(testC) 43 | 44 | --------------------------------------------------------------------------------