From ad4b8285dbfe419af5a157686a790bdcd8569998 Mon Sep 17 00:00:00 2001 From: Chris Lindholm Date: Tue, 30 Jun 2026 13:31:31 -0600 Subject: [PATCH 1/3] Use APIKeyAuth for API key support --- src/latis/client.py | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/latis/client.py b/src/latis/client.py index cd3013a..486cf69 100644 --- a/src/latis/client.py +++ b/src/latis/client.py @@ -9,6 +9,7 @@ import numpy as np import pandas as pd import requests +from requests.auth import AuthBase import urllib.parse from typing import List, Dict, Any, Union, Optional @@ -49,17 +50,19 @@ 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) # 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 +77,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 +98,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 +109,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 +121,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. From 4e2ea02b4e13b970a172d37bbc8c070e2067dcdc Mon Sep 17 00:00:00 2001 From: Chris Lindholm Date: Tue, 30 Jun 2026 13:44:48 -0600 Subject: [PATCH 2/3] Add support for basic auth --- README.rst | 13 +++++++++++++ src/latis/client.py | 12 ++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2a530b7..e27b67e 100644 --- a/README.rst +++ b/README.rst @@ -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 486cf69..6c607f0 100644 --- a/src/latis/client.py +++ b/src/latis/client.py @@ -10,12 +10,14 @@ 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. @@ -32,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. @@ -55,6 +61,8 @@ def read_data(base_url, dataset, start_time, end_time, if api_key is not None: 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. From 3de8d222c7140afb869a924723e87619e7efaa0c Mon Sep 17 00:00:00 2001 From: Chris Lindholm Date: Tue, 30 Jun 2026 13:49:04 -0600 Subject: [PATCH 3/3] Tweak README wording --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e27b67e..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