xopr.ops_api

Interface to the Open Polar Server (OPS) database API.

This module provides functions for accessing the OPS database without authentication. The OPS database stores layer picks (surface, bed, internal layers), segment metadata, and citation information for radar data.

Functions use the general _ops_api_request helper to make requests to the OPS API.

While we publicly expose these functions, they are primarily intended for internal use within the xOPR module. Consider using the higher-level xOPR data access functions in xopr.opr_access for loading radar data and metadata instead.

  1"""
  2Interface to the Open Polar Server (OPS) database API.
  3
  4This module provides functions for accessing the OPS database without authentication.
  5The OPS database stores layer picks (surface, bed, internal layers), segment metadata,
  6and citation information for radar data.
  7
  8Functions use the general _ops_api_request helper to make requests to the OPS API.
  9
 10While we publicly expose these functions, they are primarily intended for internal use
 11within the xOPR module. Consider using the higher-level xOPR data access functions in
 12xopr.opr_access for loading radar data and metadata instead.
 13"""
 14
 15import base64
 16import json
 17import time
 18import urllib.parse
 19
 20import requests
 21
 22ops_base_url = "https://ops.cresis.ku.edu/ops"
 23
 24
 25def get_layer_points(segment_name : str, season_name : str, location=None, layer_names=None, include_geometry=True, raise_errors=True):
 26    """
 27    Get layer points for a segment from the OPS API.
 28
 29    Parameters
 30    ----------
 31    segment_name : str
 32        The segment name
 33    season_name : str
 34        The season name
 35    location : str, optional
 36        The location, either 'arctic' or 'antarctic'. If None, inferred from season_name.
 37    layer_names : list of str, optional
 38        List of layer names to retrieve. If None, retrieves all layers.
 39    include_geometry : bool, optional
 40        Whether to include geometry information in the response. Default is True.
 41
 42    Returns
 43    -------
 44    dict or None
 45        API response as JSON containing layer points data, or None if request fails.
 46
 47    Raises
 48    ------
 49    ValueError
 50        If neither segment_id nor both segment_name and season_name are provided.
 51    """
 52
 53    if location is None:
 54        if "antarctica" in season_name.lower():
 55            location = "antarctic"
 56        elif "greenland" in season_name.lower():
 57            location = "arctic"
 58        else:
 59            raise ValueError("Location could not be inferred from the season name. Please specify 'arctic' or 'antarctic' explicitly.")
 60
 61    data_payload = {
 62        "properties": {
 63            "location": location,
 64            "season": season_name,
 65            "segment": segment_name
 66        }
 67    }
 68
 69    if include_geometry:
 70        data_payload["properties"]["return_geom"] = 'geog'
 71
 72    # Add layer names if specified
 73    if layer_names:
 74        data_payload["properties"]["lyr_name"] = layer_names
 75
 76    return _ops_api_request(f"/get/layer/points", data_payload, request_type='POST')
 77
 78def get_layer_metadata():
 79    """
 80    Get the mapping of layer IDs to groups and names from the OPS API.
 81
 82    Returns
 83    -------
 84    dict or None
 85        API response as JSON containing layer metadata, or None if request fails.
 86    """
 87
 88    return _ops_api_request(f"/get/layers", {}, request_type='POST')
 89
 90def get_segment_metadata(segment_name : str, season_name : str):
 91    """
 92    Get segment metadata from the OPS API.
 93
 94    Parameters
 95    ----------
 96    segment_name : str, optional
 97        The segment name (alternative to segment_id).
 98    season_name : str, optional
 99        The season name (required if using segment_name).
100
101    Returns
102    -------
103    dict or None
104        API response as JSON containing segment metadata, or None if request fails.
105
106    Raises
107    ------
108    ValueError
109        If neither segment_id nor both segment_name and season_name are provided.
110    """
111
112    data_payload = {
113        "properties": {
114            "segment": segment_name,
115            "season": season_name
116        }
117    }
118
119    return _ops_api_request(f"/get/segment/metadata", data_payload)
120
121
122def _ops_api_request(path, data, request_type='POST', headers=None, base_url=ops_base_url, retries=3, job_timeout=200, debug=False, initial_retry_time=1):
123    """
124    Helper function to make a POST request to the OPS API.
125
126    Parameters
127    ----------
128    path : str
129        The API endpoint path.
130    data : dict
131        The data payload to send in the request.
132    headers : dict, optional
133        Additional headers to include in the request.
134    base_url : str, optional
135        The base URL for the OPS API. Default is ops_base_url.
136    retries : int, optional
137        Number of retry attempts for failed requests. Default is 3.
138
139    Returns
140    -------
141    dict or None
142        API response as JSON, or None if request fails after retries.
143    """
144
145    url = f"{base_url}/{path.lstrip('/')}"
146
147
148    # Encode the JSON data
149    encoded_data = urllib.parse.quote(json.dumps(data))
150    form_data = f"app=rds&data={encoded_data}"
151
152    # Create basic auth header for anonymous user
153    if headers is None:
154        credentials = base64.b64encode(b"anonymous:anonymous").decode("ascii")
155        headers = {
156            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
157            'Authorization': f'Basic {credentials}',
158            'Cookie': 'userName=anonymous; isAuthenticated=Yes'
159        }
160
161    try:
162        # Make the request
163        if request_type == 'POST':
164            if debug:
165                print(f"Making POST request to {url} with data: {form_data}")
166            response = requests.post(url, data=form_data, headers=headers)
167        elif request_type == 'GET':
168            if debug:
169                print(f"Making GET request to {url}")
170            response = requests.get(url, headers=headers)
171
172        # Check if request was successful
173        response.raise_for_status()
174
175        response_json = response.json()
176
177        if debug:
178            print(f"Received response with status: {response_json['status']}")
179
180        if response_json['status'] == 303: # Indicates that the request was processed as a background task
181            task_id = response_json['data']['task_id']
182            task_start_time = time.time()
183            if debug:
184                print(f"Task {task_id} started.")
185            while time.time() - task_start_time < job_timeout:
186                # Check the status of the background task
187                status_response = _ops_api_request(f"/get/status/{urllib.parse.quote(task_id)}", {}, request_type='GET', headers=headers, base_url=base_url, retries=retries)
188
189                if debug:
190                    print(f"Checking status for task {task_id}: {status_response}")
191
192                if status_response and status_response['status'] != 503:
193                    return status_response
194
195                time.sleep(initial_retry_time)  # Wait before retrying
196                initial_retry_time *= 2  # Exponential backoff
197
198            raise TimeoutError(f"Task {task_id} timed out after {job_timeout} seconds.")
199
200        return response_json
201
202    except requests.exceptions.HTTPError as e:
203        if debug:
204            print(f"HTTP error occurred: {response.status_code}")
205        if response.status_code == 504:
206            print(f"Gateway timeout occurred. Retrying in {initial_retry_time} seconds...")
207            time.sleep(initial_retry_time)
208            return _ops_api_request(path, data, request_type=request_type, headers=headers, base_url=base_url, retries=retries-1, initial_retry_time=initial_retry_time*2)
209        raise e
ops_base_url = 'https://ops.cresis.ku.edu/ops'
def get_layer_points( segment_name: str, season_name: str, location=None, layer_names=None, include_geometry=True, raise_errors=True):
26def get_layer_points(segment_name : str, season_name : str, location=None, layer_names=None, include_geometry=True, raise_errors=True):
27    """
28    Get layer points for a segment from the OPS API.
29
30    Parameters
31    ----------
32    segment_name : str
33        The segment name
34    season_name : str
35        The season name
36    location : str, optional
37        The location, either 'arctic' or 'antarctic'. If None, inferred from season_name.
38    layer_names : list of str, optional
39        List of layer names to retrieve. If None, retrieves all layers.
40    include_geometry : bool, optional
41        Whether to include geometry information in the response. Default is True.
42
43    Returns
44    -------
45    dict or None
46        API response as JSON containing layer points data, or None if request fails.
47
48    Raises
49    ------
50    ValueError
51        If neither segment_id nor both segment_name and season_name are provided.
52    """
53
54    if location is None:
55        if "antarctica" in season_name.lower():
56            location = "antarctic"
57        elif "greenland" in season_name.lower():
58            location = "arctic"
59        else:
60            raise ValueError("Location could not be inferred from the season name. Please specify 'arctic' or 'antarctic' explicitly.")
61
62    data_payload = {
63        "properties": {
64            "location": location,
65            "season": season_name,
66            "segment": segment_name
67        }
68    }
69
70    if include_geometry:
71        data_payload["properties"]["return_geom"] = 'geog'
72
73    # Add layer names if specified
74    if layer_names:
75        data_payload["properties"]["lyr_name"] = layer_names
76
77    return _ops_api_request(f"/get/layer/points", data_payload, request_type='POST')

Get layer points for a segment from the OPS API.

Parameters
  • segment_name (str): The segment name
  • season_name (str): The season name
  • location (str, optional): The location, either 'arctic' or 'antarctic'. If None, inferred from season_name.
  • layer_names (list of str, optional): List of layer names to retrieve. If None, retrieves all layers.
  • include_geometry (bool, optional): Whether to include geometry information in the response. Default is True.
Returns
  • dict or None: API response as JSON containing layer points data, or None if request fails.
Raises
  • ValueError: If neither segment_id nor both segment_name and season_name are provided.
def get_layer_metadata():
79def get_layer_metadata():
80    """
81    Get the mapping of layer IDs to groups and names from the OPS API.
82
83    Returns
84    -------
85    dict or None
86        API response as JSON containing layer metadata, or None if request fails.
87    """
88
89    return _ops_api_request(f"/get/layers", {}, request_type='POST')

Get the mapping of layer IDs to groups and names from the OPS API.

Returns
  • dict or None: API response as JSON containing layer metadata, or None if request fails.
def get_segment_metadata(segment_name: str, season_name: str):
 91def get_segment_metadata(segment_name : str, season_name : str):
 92    """
 93    Get segment metadata from the OPS API.
 94
 95    Parameters
 96    ----------
 97    segment_name : str, optional
 98        The segment name (alternative to segment_id).
 99    season_name : str, optional
100        The season name (required if using segment_name).
101
102    Returns
103    -------
104    dict or None
105        API response as JSON containing segment metadata, or None if request fails.
106
107    Raises
108    ------
109    ValueError
110        If neither segment_id nor both segment_name and season_name are provided.
111    """
112
113    data_payload = {
114        "properties": {
115            "segment": segment_name,
116            "season": season_name
117        }
118    }
119
120    return _ops_api_request(f"/get/segment/metadata", data_payload)

Get segment metadata from the OPS API.

Parameters
  • segment_name (str, optional): The segment name (alternative to segment_id).
  • season_name (str, optional): The season name (required if using segment_name).
Returns
  • dict or None: API response as JSON containing segment metadata, or None if request fails.
Raises
  • ValueError: If neither segment_id nor both segment_name and season_name are provided.