Usage

For more information on the DAP 2.0 protocol, visit the specification page.

Create a DDS, DAS and DODS responses from arbitrary numpy arrays

Basic imports:

import numpy as np
import opendap_protocol as dap

Define dimensions:

x = dap.Array(name='x', data=np.array([0, 1, 2]), dtype=dap.Int16)
y = dap.Array(name='y', data=np.array([10, 11, 12]), dtype=dap.Int16)
z = dap.Array(name='z', data=np.array([20, 21, 22]), dtype=dap.Int16)

Define an array holding our data:

data_array = dap.Grid(name='data',
                      data=np.random.rand(3, 3, 3),
                      dtype=dap.Float64,
                      dimensions=[x, y, z])

Define attributes:

attrs = [
    dap.Attribute(name='units', value='m s-1', dtype=dap.String),
    dap.Attribute(name='version', value=2, dtype=dap.Int16),
]

and attach them to the data array:

data_array.append(*attrs)

Glue it all together by creating the Dataset and appending to it:

dataset = dap.Dataset(name='Example')
dataset.append(x, y, z, data_array)

Each of the DAP 2.0 responses returns a generator iterator. Such a generator has generally a low memory footprint, since the serialized data set does not need to be held in memory at once. Rather, serialization takes place as a consumer iterates through the data set.

For testing purposes, each response can be printed as string:

print(''.join(dataset.dds()))

# Dataset {
# Int16 x[x = 3];
# Int16 y[y = 3];
# Int16 z[z = 3];
# Grid {
#   Array:
#     Float64 data[x = 3][y = 3][z = 3];
#   Maps:
#     Int16 x[x = 3];
#     Int16 y[y = 3];
#     Int16 z[z = 3];
#   } data;
# } Example;

print(''.join(dataset.das()))

# Attributes {
#     x {
#     }
#     y {
#     }
#     z {
#     }
#     data {
#         String units "m s-1";
#         Int16 version 2;
#     }
# }

print(b''.join(dataset.dods()))

# See for yourself ;-)

Serving data through a web service using Flask

Note

We assume, that the dataset created above is still available as a variable.

Basic setup:

import urllib
from flask import Flask, Response, request

app = Flask(__name__)

Define the web service endpoints needed by the DAP protocol:

@app.route('/dataset.dds', methods=['GET'])
def dds_response():
    # Retrieve constraints from the request to handle slicing, etc.
    constraint = urllib.parse.urlsplit(request.url)[3]
    return Response(
        dataset.dds(constraint=constraint),
        mimetype='text/plain')

@app.route('/dataset.das', methods=['GET'])
def das_response():
    constraint = urllib.parse.urlsplit(request.url)[3]
    return Response(
        dataset.das(constraint=constraint),
        mimetype='text/plain')

@app.route('/dataset.dods', methods=['GET'])
def dods_response():
    constraint = urllib.parse.urlsplit(request.url)[3]
    return Response(
        dataset.dods(constraint=constraint),
        mimetype='application/octet-stream')

app.run(debug=True)

Data can then be loaded from any Python terminal using xarray or netCDF4.

Note

Please be aware, that for opening a dataset the suffix (.dds, .das or .dods) needs to be omitted. The netCDF library figures out on its own which endpoint it has to call in what order.

xarray:

import xarray as xr

data = import xr.open_dataset('http://localhost:5000/dataset')
data.load()

# <xarray.Dataset>
# Dimensions:  (x: 3, y: 3, z: 3)
# Coordinates:
#   * x        (x) int16 0 1 2
#   * y        (y) int16 10 11 12
#   * z        (z) int16 20 21 22
# Data variables:
#     data     (x, y, z) float64 0.7793 0.3464 0.1331 ... 0.2244 0.4277 0.1545

netCDF4:

import netCDF4 as nc

data = nc.Dataset('http://localhost:5000/dataset')
data

# <class 'netCDF4._netCDF4.Dataset'>
# root group (NETCDF3_CLASSIC data model, file format DAP2):
#     dimensions(sizes): x(3), y(3), z(3)
#     variables(dimensions): int16 x(x), int16 y(y), int16 z(z), float64 data(x,y,z)
#     groups: