diff --git a/README.rst b/README.rst index 2a530b7..ec87739 100644 --- a/README.rst +++ b/README.rst @@ -36,7 +36,7 @@ The ``read_data`` function expects the following arguments: "2026-01-02" ) -You can optionally specify an API key for instances that require one. +You can specify an API key for instances that require one. .. code:: python @@ -48,6 +48,19 @@ You can optionally specify an API key for instances that require one. api_key="" ) +You can specify a username and password for instances that require basic auth. + +.. code:: python + + df = read_data( + "https://lasp.colorado.edu/lisird/latis/dap2", + "bremen_composite_mgii", + "2026-01-01", + "2026-01-02", + username="", + password="" + ) + For queries more complicated than time selections, you can optionally specify an additional `DAP2 query fragment `__ that will be appended to the query sent to LaTiS. It must be URL-encoded. .. code:: python diff --git a/src/latis/client.py b/src/latis/client.py index cd3013a..6c607f0 100644 --- a/src/latis/client.py +++ b/src/latis/client.py @@ -9,12 +9,15 @@ import numpy as np import pandas as pd import requests +from requests.auth import AuthBase +from requests.auth import HTTPBasicAuth import urllib.parse from typing import List, Dict, Any, Union, Optional -def read_data(base_url, dataset, start_time, end_time, - api_key=None, api_key_header="x-api-key", query=None): +def read_data(base_url, dataset, start_time, end_time, api_key=None, + api_key_header="x-api-key", username=None, password=None, + query=None): """ Make a request to a LaTiS instance for data. @@ -31,6 +34,10 @@ def read_data(base_url, dataset, start_time, end_time, api_key (str): If set, the API key sent with the request. api_key_header (str): The HTTP header used to include the API key given by ``api_key``. + username (str): If set, the username used for basic auth when + making the request. + password (str): If set, the password used for basic auth when + making the request. query (str): Optional additional OPeNDAP DAP2 query fragment. Must be URL-encoded. @@ -49,17 +56,21 @@ def read_data(base_url, dataset, start_time, end_time, rest="" if query is None else "&" + query ) - headers = {} + # No auth by default. + auth = None + if api_key is not None: - headers[api_key_header] = api_key + auth = APIKeyAuth(api_key, api_key_header) + elif username is not None and password is not None: + auth = HTTPBasicAuth(username, password) # If dataset is a string, make a DAP2 query. If dataset is a list # or a tuple, make a join query. if isinstance(dataset, str): - res = _make_dap2_query(base_url, dataset, query, headers) + res = _make_dap2_query(base_url, dataset, query, auth) elif isinstance(dataset, (list, tuple)): if len(dataset) == 1: - res = _make_dap2_query(base_url, dataset[0], query, headers) + res = _make_dap2_query(base_url, dataset[0], query, auth) else: # Joining is only supported for LaTiS 3 instances. The # only way we can check whether an instance is a LaTiS 3 @@ -74,7 +85,7 @@ def read_data(base_url, dataset, start_time, end_time, # slash. join_url = base_url.rstrip("/dap2") + "/join" - res = _make_join_query(join_url, dataset, query, headers) + res = _make_join_query(join_url, dataset, query, auth) else: # Probably not a LaTiS 3 instance. raise ValueError( @@ -95,7 +106,7 @@ def read_data(base_url, dataset, start_time, end_time, return df -def _make_dap2_query(base, dataset, query, headers): +def _make_dap2_query(base, dataset, query, auth): """ Makes a DAP 2 request to a LaTiS instance. """ @@ -106,11 +117,11 @@ def _make_dap2_query(base, dataset, query, headers): query=query ) - res = requests.get(url, headers=headers) + res = requests.get(url, auth=auth) return res -def _make_join_query(base, datasets, query, headers): +def _make_join_query(base, datasets, query, auth): """ Makes a request to the join service of a LaTiS 3 instance. """ @@ -118,10 +129,33 @@ def _make_join_query(base, datasets, query, headers): url = "{base}?{query}".format(base=base, query=query) body = {"datasets": datasets} - res = requests.post(url, json=body, headers=headers) + res = requests.post(url, json=body, auth=auth) return res +class APIKeyAuth(AuthBase): + """Authentication using an API key passed in an HTTP header.""" + + def __init__(self, key, header='x-api-key'): + """ + Construct an APIKeyAuth instance. + + Args: + key (str): The API key + header (str): The HTTP header used to pass the API key. + Defaults to ``x-api-key``. + + Returns: + An APIKeyAuth instance. + """ + self.key = key + self.header = header + + def __call__(self, req): + req.headers[self.header] = self.key + return req + + class LatisInstance: """Represents a LaTiS instance.