From 8639b38e4ea59d1171c51b7fdfd1c18e0acb9dcb Mon Sep 17 00:00:00 2001
From: "duncan.macleod" <duncan.macleod@ligo.org>
Date: Mon, 6 Nov 2023 20:56:13 -0800
Subject: [PATCH] ui: support dynamic API selection

new `api` keyword argument to all UI functions, new `-A/--api` command-line option
---
 gwdatafind/__main__.py        |  35 +++++++---
 gwdatafind/api/__init__.py    |  19 +++++-
 gwdatafind/tests/test_main.py |  14 ++++
 gwdatafind/tests/test_ui.py   |  63 ++++++++++++------
 gwdatafind/ui.py              | 117 ++++++++++++++++++++++++++++------
 5 files changed, 199 insertions(+), 49 deletions(-)

diff --git a/gwdatafind/__main__.py b/gwdatafind/__main__.py
index a214fca..cedde89 100644
--- a/gwdatafind/__main__.py
+++ b/gwdatafind/__main__.py
@@ -27,6 +27,7 @@ from . import (
     __version__,
     ui,
 )
+from .api import DEFAULT_API
 from .io import (
     format_cache,
     lal_cache,
@@ -206,7 +207,7 @@ def command_line():
         "-x",
         "--extension",
         metavar="EXT",
-        default="gwf",
+        default=ui.DEFAULT_EXT,
         help="file extension for which to search",
     )
 
@@ -228,6 +229,12 @@ def command_line():
         action="store_true",
         help="attempt to authenticate without a grid proxy",
     )
+    sargs.add_argument(
+        "-A",
+        "--api",
+        default=DEFAULT_API,
+        help="API version to use",
+    )
 
     oargs = parser.add_argument_group(
         "Output options",
@@ -319,7 +326,11 @@ def ping(args, out):
     exitcode : `int` or `None`
         the return value of the action or `None` to indicate success.
     """
-    ui.ping(host=args.server, ext=args.extension)
+    ui.ping(
+        ext=args.extension,
+        host=args.server,
+        api=args.api,
+    )
     print(f"LDRDataFindServer at {args.server} is alive", file=out)
 
 
@@ -340,9 +351,10 @@ def show_observatories(args, out):
         the return value of the action or `None` to indicate success.
     """
     sitelist = ui.find_observatories(
-        host=args.server,
-        match=args.match,
         ext=args.extension,
+        match=args.match,
+        host=args.server,
+        api=args.api,
     )
     print("\n".join(sitelist), file=out)
 
@@ -365,9 +377,10 @@ def show_types(args, out):
     """
     typelist = ui.find_types(
         site=args.observatory,
+        ext=args.extension,
         match=args.match,
         host=args.server,
-        ext=args.extension,
+        api=args.api,
     )
     print("\n".join(typelist), file=out)
 
@@ -393,8 +406,9 @@ def show_times(args, out):
         frametype=args.type,
         gpsstart=args.gpsstart,
         gpsend=args.gpsend,
-        host=args.server,
         ext=args.extension,
+        host=args.server,
+        api=args.api,
     )
     print("# seg\tstart     \tstop      \tduration", file=out)
     for i, seg in enumerate(seglist):
@@ -423,10 +437,11 @@ def latest(args, out):
     cache = ui.find_latest(
         args.observatory,
         args.type,
+        ext=args.extension,
         urltype=args.url_type,
         on_missing="warn",
         host=args.server,
-        ext=args.extension,
+        api=args.api,
     )
     return postprocess_cache(cache, args, out)
 
@@ -452,6 +467,7 @@ def filename(args, out):
         urltype=args.url_type,
         on_missing="warn",
         host=args.server,
+        api=args.api,
     )
     return postprocess_cache(cache, args, out)
 
@@ -477,11 +493,12 @@ def show_urls(args, out):
         args.type,
         args.gpsstart,
         args.gpsend,
+        ext=args.extension,
         match=args.match,
         urltype=args.url_type,
-        host=args.server,
         on_gaps="ignore",
-        ext=args.extension,
+        host=args.server,
+        api=args.api,
     )
     return postprocess_cache(cache, args, out)
 
diff --git a/gwdatafind/api/__init__.py b/gwdatafind/api/__init__.py
index 0dbab9e..cf624d3 100644
--- a/gwdatafind/api/__init__.py
+++ b/gwdatafind/api/__init__.py
@@ -15,10 +15,27 @@
 # You should have received a copy of the GNU General Public License
 # along with GWDataFind.  If not, see <http://www.gnu.org/licenses/>.
 
-"""API definitions for the GWDataFind Client."""
+"""API definitions for the GWDataFind Client.
+
+Each API module must define the same set of functions that
+return paths to be queried on the host to return the various
+endpoints.
+
+The required functions and signatures are:
+
+- ``ping_path()``
+- ``find_observatories_path()``
+- ``find_types_path(site=None)``
+- ``find_times_path(site, frametype, start, end)``
+- ``find_url_path(framefile)``
+- ``find_latest_path(site, frametype, urltype)``
+- ``find_urls_path(site, frametype, start, end, urltype=None, match=None)``
+"""
 
 # list of APIs
 APIS = (
     "ldr",
     "v1",
 )
+
+DEFAULT_API = "ldr"
diff --git a/gwdatafind/tests/test_main.py b/gwdatafind/tests/test_main.py
index 0b8f61f..5bbaeab 100644
--- a/gwdatafind/tests/test_main.py
+++ b/gwdatafind/tests/test_main.py
@@ -126,12 +126,14 @@ def test_ping(mping):
     """Test `ping()."""
     args = argparse.Namespace(
         server="test.datafind.com:443",
+        api="v1",
         extension="gwf",
     )
     out = StringIO()
     main.ping(args, out)
     mping.assert_called_with(
         host=args.server,
+        api=args.api,
         ext=args.extension,
     )
     out.seek(0)
@@ -145,6 +147,7 @@ def test_show_observatories(mfindobs):
     mfindobs.return_value = ["A", "B", "C"]
     args = argparse.Namespace(
         server="test.datafind.com:443",
+        api="v1",
         extension="gwf",
         match="test",
     )
@@ -153,6 +156,7 @@ def test_show_observatories(mfindobs):
     out.seek(0)
     mfindobs.assert_called_with(
         host=args.server,
+        api=args.api,
         match=args.match,
         ext=args.extension,
     )
@@ -165,6 +169,7 @@ def test_show_types(mfindtypes):
     mfindtypes.return_value = ["A", "B", "C"]
     args = argparse.Namespace(
         server="test.datafind.com:443",
+        api="v1",
         extension="gwf",
         observatory="X",
         match="test",
@@ -174,6 +179,7 @@ def test_show_types(mfindtypes):
     out.seek(0)
     mfindtypes.assert_called_with(
         host=args.server,
+        api=args.api,
         match=args.match,
         site=args.observatory,
         ext=args.extension,
@@ -187,6 +193,7 @@ def test_show_times(mfindtimes):
     mfindtimes.return_value = [segment(0, 1), segment(1, 2), segment(3, 4)]
     args = argparse.Namespace(
         server="test.datafind.com:443",
+        api="v1",
         extension="gwf",
         observatory="X",
         type="test",
@@ -197,6 +204,7 @@ def test_show_times(mfindtimes):
     main.show_times(args, out)
     mfindtimes.assert_called_with(
         host=args.server,
+        api=args.api,
         site=args.observatory,
         frametype=args.type,
         gpsstart=args.gpsstart,
@@ -215,6 +223,7 @@ def test_latest(mlatest):
     mlatest.return_value = ["file:///test/X-test-0-10.gwf"]
     args = argparse.Namespace(
         server="test.datafind.com:443",
+        api="v1",
         extension="gwf",
         observatory="X",
         type="test",
@@ -230,6 +239,7 @@ def test_latest(mlatest):
         urltype=args.url_type,
         on_missing="warn",
         host=args.server,
+        api=args.api,
         ext=args.extension,
     )
     out.seek(0)
@@ -242,6 +252,7 @@ def test_filename(mfindurl):
     mfindurl.return_value = ["file:///test/X-test-0-10.gwf"]
     args = argparse.Namespace(
         server="test.datafind.com:443",
+        api="v1",
         filename="X-test-0-10.gwf",
         url_type="file",
         type=None,
@@ -255,6 +266,7 @@ def test_filename(mfindurl):
         urltype=args.url_type,
         on_missing="warn",
         host=args.server,
+        api=args.api,
     )
     out.seek(0)
     assert out.read().rstrip() == mfindurl.return_value[0]
@@ -271,6 +283,7 @@ def test_show_urls(mfindurls, ext):
     mfindurls.return_value = urls
     args = argparse.Namespace(
         server="test.datafind.com:443",
+        api="v1",
         extension=ext,
         observatory="X",
         type="test",
@@ -293,6 +306,7 @@ def test_show_urls(mfindurls, ext):
         on_gaps="ignore",
         ext=ext,
         host=args.server,
+        api=args.api,
     )
     out.seek(0)
     assert list(map(str.rstrip, out.readlines())) == urls
diff --git a/gwdatafind/tests/test_ui.py b/gwdatafind/tests/test_ui.py
index f2e9439..f407ba2 100644
--- a/gwdatafind/tests/test_ui.py
+++ b/gwdatafind/tests/test_ui.py
@@ -30,11 +30,12 @@ import igwn_segments as segments
 import pytest
 
 from gwdatafind import ui
-from gwdatafind.api import ldr as api_ldr
+from gwdatafind.api import v1 as api
 
 __author__ = "Duncan Macleod <duncan.macleod@ligo.org>"
 
 TEST_SERVER = "test.datafind.org"
+TEST_API = "v1"
 TEST_URL_BASE = f"https://{TEST_SERVER}"
 TEST_DATA = {
     "A": {
@@ -50,6 +51,10 @@ TEST_DATA = {
     },
 }
 
+# partials with the TEST_API set by default:
+find_url = partial(ui.find_url, api=TEST_API)
+find_urls = partial(ui.find_urls, api=TEST_API)
+
 
 @contextmanager
 def no_warning():
@@ -126,17 +131,19 @@ def noauth():
     pytest.param(
         None,
         TEST_URL_BASE,
+        id="default-host",
     ),
 ])
+@mock.patch("gwdatafind.ui._api_func", lambda x, y: lambda: "test")
 def test_url_scheme_handling(in_, url):
     """Test URL scheme handling in `_url()`."""
-    assert ui._url(in_, lambda: "test") == f"{url}/test"  # noqa: SLF001
+    assert ui._url(in_, "api", "funcname") == f"{url}/test"  # noqa: SLF001
 
 
 def test_ping(requests_mock):
     """Test `ping()`."""
-    requests_mock.get(_url(api_ldr.ping_path()), status_code=200)
-    ui.ping()
+    requests_mock.get(_url(api.ping_path()), status_code=200)
+    ui.ping(api=TEST_API)
 
 
 @pytest.mark.parametrize(("match", "result"), [
@@ -146,10 +153,13 @@ def test_ping(requests_mock):
 def test_find_observatories(match, result, requests_mock):
     """Test `find_observatories()`."""
     requests_mock.get(
-        _url(api_ldr.find_observatories_path()),
+        _url(api.find_observatories_path()),
         json=list(TEST_DATA),
     )
-    assert ui.find_observatories(match=match) == list(set(result))
+    assert ui.find_observatories(
+        api=TEST_API,
+        match=match,
+    ) == list(set(result))
 
 
 @pytest.mark.parametrize(("site", "match", "result"), [
@@ -179,10 +189,11 @@ def test_find_types(site, match, result, requests_mock):
     else:
         respdata = [ft for site in TEST_DATA for ft in TEST_DATA[site]]
     requests_mock.get(
-        _url(api_ldr.find_types_path(site=site)),
+        _url(api.find_types_path(site=site)),
         json=respdata,
     )
     assert ui.find_types(
+        api=TEST_API,
         site=site,
         match=match,
     ) == list(set(result))
@@ -193,10 +204,16 @@ def test_find_times(requests_mock):
     site = "A"
     frametype = "A1_TEST"
     requests_mock.get(
-        _url(api_ldr.find_times_path(site, frametype, 1, 100)),
+        _url(api.find_times_path(site, frametype, 1, 100)),
         json=TEST_DATA[site][frametype],
     )
-    assert ui.find_times(site, frametype, 1, 100) == segments.segmentlist([
+    assert ui.find_times(
+        site,
+        frametype,
+        1,
+        100,
+        api=TEST_API,
+    ) == segments.segmentlist([
         segments.segment(0, 10),
         segments.segment(10, 20),
         segments.segment(30, 50),
@@ -210,12 +227,12 @@ def test_find_url(requests_mock):
         "gsiftp://localhost:2811/data/A/A1_TEST/A-A1_TEST-0-1.gwf",
     ]
     requests_mock.get(
-        _url(api_ldr.find_url_path("A-A1_TEST-0-1.gwf")),
+        _url(api.find_url_path("A-A1_TEST-0-1.gwf")),
         json=urls,
     )
-    assert ui.find_url("/my/data/A-A1_TEST-0-1.gwf") == urls[:1]
-    assert ui.find_url("/my/data/A-A1_TEST-0-1.gwf", urltype=None) == urls
-    assert ui.find_url(
+    assert find_url("/my/data/A-A1_TEST-0-1.gwf") == urls[:1]
+    assert find_url("/my/data/A-A1_TEST-0-1.gwf", urltype=None) == urls
+    assert find_url(
         "/my/data/A-A1_TEST-0-1.gwf",
         urltype="gsiftp",
     ) == urls[1:]
@@ -247,12 +264,12 @@ def test_find_url_on_missing(requests_mock, on_missing, ctx):
     """Test `find_url` handling of missing data."""
     # mock the request
     requests_mock.get(
-        _url(api_ldr.find_url_path("A-A1_TEST-0-1.gwf")),
+        _url(api.find_url_path("A-A1_TEST-0-1.gwf")),
         json=[],
     )
 
     with ctx:
-        assert ui.find_url(
+        assert find_url(
             "A-A1_TEST-0-1.gwf",
             on_missing=on_missing,
         ) == []
@@ -267,10 +284,14 @@ def test_find_latest(requests_mock):
         "gsiftp://localhost:2811/data/A/A1_TEST/A-A1_TEST-0-1.gwf",
     ]
     requests_mock.get(
-        _url(api_ldr.find_latest_path("A", "A1_TEST", "file")),
+        _url(api.find_latest_path("A", "A1_TEST", "file")),
         json=urls[:1],
     )
-    assert ui.find_latest("A", "A1_TEST") == urls[:1]
+    assert ui.find_latest(
+        "A",
+        "A1_TEST",
+        api=TEST_API,
+    ) == urls[:1]
 
 
 def _file_url(seg):
@@ -282,10 +303,10 @@ def test_find_urls(requests_mock):
     """Test `find_urls()`."""
     urls = list(map(_file_url, TEST_DATA["A"]["A1_TEST"][:2]))
     requests_mock.get(
-        _url(api_ldr.find_urls_path("A", "A1_TEST", 0, 20, "file")),
+        _url(api.find_urls_path("A", "A1_TEST", 0, 20, "file")),
         json=urls,
     )
-    assert ui.find_urls("A", "A1_TEST", 0, 20, on_gaps="error") == urls
+    assert find_urls("A", "A1_TEST", 0, 20, on_gaps="error") == urls
 
 
 @pytest.mark.parametrize(("on_gaps", "ctx"), [
@@ -315,13 +336,13 @@ def test_find_urls_on_gaps(requests_mock, on_gaps, ctx):
     # configure the mock request
     urls = list(map(_file_url, TEST_DATA["A"]["A1_TEST"]))
     requests_mock.get(
-        _url(api_ldr.find_urls_path("A", "A1_TEST", 0, 100, "file")),
+        _url(api.find_urls_path("A", "A1_TEST", 0, 100, "file")),
         json=urls,
     )
 
     # make the request
     with ctx:
-        assert ui.find_urls(
+        assert find_urls(
             "A",
             "A1_TEST",
             0,
diff --git a/gwdatafind/ui.py b/gwdatafind/ui.py
index e210bcb..18f2e83 100644
--- a/gwdatafind/ui.py
+++ b/gwdatafind/ui.py
@@ -23,6 +23,7 @@ referred to in usage as ``gwdatafind.<function>`` and not
 """
 
 from functools import wraps
+from importlib import import_module
 from re import compile as compile_regex
 from urllib.parse import urlparse
 from warnings import warn
@@ -31,12 +32,25 @@ import igwn_segments as segments
 import requests
 from igwn_auth_utils.requests import get as _get
 
-from .api import ldr as api_ldr
+from .api import DEFAULT_API
 from .utils import (
     file_segment,
     get_default_host,
 )
 
+try:
+    from functools import cache
+except ImportError:  # python < 3.9
+    from functools import (
+        lru_cache,
+        partial,
+    )
+
+    @wraps(lru_cache)
+    def cache(func):
+        """Return `functools.lru_cache` with ``maxsize=None``."""
+        return lru_cache(maxsize=None)(func)
+
 __author__ = "Duncan Macleod <duncan.macleod@ligo.org>"
 
 __all__ = [
@@ -49,6 +63,27 @@ __all__ = [
     "find_urls",
 ]
 
+DEFAULT_EXT = "gwf"
+
+
+# -- api handling --------------------
+
+def _api_mod(api):
+    """Return the API implementation module with the module name ``api``."""
+    api = (api or DEFAULT_API).lower()
+    try:
+        return import_module(f".api.{api}", package=__package__)
+    except ImportError:
+        raise ValueError(f"unsupported api '{api}'")
+
+
+@cache
+def _api_func(api, name):
+    """Return the function with ``name`` for the matching ``api``."""
+    return getattr(_api_mod(api), name)
+
+
+# -- user interface ------------------
 
 @wraps(_get)
 def get(url, *args, **kwargs):
@@ -90,17 +125,26 @@ def get_json(*args, **kwargs):
     return response.json()
 
 
-def _url(host, api_func, *args, **kwargs):
+def _url(
+    host,
+    api,
+    funcname,
+    *args,
+    **kwargs,
+):
     """Construct the full URL for a query to ``host`` using an API function.
 
     Parameters
     ----------
     host : `str`, `None`
-        the host to query, if `None` `~gwdatafind.utils.get_default_host()`
+        The host to query, if `None` `~gwdatafind.utils.get_default_host()`
         will be used to discover the default host.
 
-    api_func : `callable`
-        the function from the :mod:`gwdatafind.api` module to use in
+    api : `str`, optional
+        The API version to use.
+
+    funcname: `str`
+        The name of the function from the API implementation to use in
         constructing the URL path.
 
     *args, **kwargs
@@ -112,6 +156,7 @@ def _url(host, api_func, *args, **kwargs):
     url : `str`
         a full URL including scheme, host, and path
     """
+    api_func = _api_func(api, funcname)
     path = api_func(*args, **kwargs)
     if host is None:
         host = get_default_host()
@@ -127,7 +172,13 @@ def _url(host, api_func, *args, **kwargs):
     return f"{host.rstrip('/')}/{path}"
 
 
-def ping(host=None, ext=api_ldr.DEFAULT_EXT, session=None, **request_kw):
+def ping(
+    host=None,
+    api=DEFAULT_API,
+    ext=DEFAULT_EXT,
+    session=None,
+    **request_kw,
+):
     """Ping the GWDataFind host to test for life.
 
     Parameters
@@ -137,6 +188,9 @@ def ping(host=None, ext=api_ldr.DEFAULT_EXT, session=None, **request_kw):
         :func:`~gwdatafind.utils.get_default_host` will be used to
         discover the default host.
 
+    api : `str`, optional
+        The API version to use.
+
     ext : `str`, optional
         the file extension for which to search.
 
@@ -161,7 +215,7 @@ def ping(host=None, ext=api_ldr.DEFAULT_EXT, session=None, **request_kw):
     requests.RequestException
         if the request fails for any reason
     """
-    qurl = _url(host, api_ldr.ping_path, ext=ext)
+    qurl = _url(host, api, "ping_path")
     response = get(qurl, session=session, **request_kw)
     response.raise_for_status()
 
@@ -169,7 +223,8 @@ def ping(host=None, ext=api_ldr.DEFAULT_EXT, session=None, **request_kw):
 def find_observatories(
     match=None,
     host=None,
-    ext=api_ldr.DEFAULT_EXT,
+    api=DEFAULT_API,
+    ext=DEFAULT_EXT,
     session=None,
     **request_kw,
 ):
@@ -186,6 +241,9 @@ def find_observatories(
         :func:`~gwdatafind.utils.get_default_host` will be used to
         discover the default host.
 
+    api : `str`, optional
+        The API version to use.
+
     ext : `str`, optional
         the file extension for which to search.
 
@@ -222,7 +280,7 @@ def find_observatories(
     >>> find_observatories(match="H", host="datafind.gwosc.org")
     ['H']
     """
-    qurl = _url(host, api_ldr.find_observatories_path, ext=ext)
+    qurl = _url(host, api, "find_observatories_path", ext=ext)
     sites = set(get_json(qurl, session=session, **request_kw))
     if match:
         match = compile_regex(match).search
@@ -234,7 +292,8 @@ def find_types(
     site=None,
     match=None,
     host=None,
-    ext=api_ldr.DEFAULT_EXT,
+    api=DEFAULT_API,
+    ext=DEFAULT_EXT,
     session=None,
     **request_kw,
 ):
@@ -254,6 +313,9 @@ def find_types(
         :func:`~gwdatafind.utils.get_default_host` will be used to
         discover the default host.
 
+    api : `str`, optional
+        The API version to use.
+
     ext : `str`, optional
         the file extension for which to search.
 
@@ -292,7 +354,7 @@ def find_types(
 
     (accurate as of Nov 18 2021)
     """  # noqa: E501
-    qurl = _url(host, api_ldr.find_types_path, site=site, ext=ext)
+    qurl = _url(host, api, "find_types_path", site=site, ext=ext)
     types = set(get_json(qurl, session=session, **request_kw))
     if match:
         match = compile_regex(match).search
@@ -306,7 +368,8 @@ def find_times(
     gpsstart=None,
     gpsend=None,
     host=None,
-    ext=api_ldr.DEFAULT_EXT,
+    api=DEFAULT_API,
+    ext=DEFAULT_EXT,
     session=None,
     **request_kw,
 ):
@@ -334,6 +397,9 @@ def find_times(
         :func:`~gwdatafind.utils.get_default_host` will be used to
         discover the default host.
 
+    api : `str`, optional
+        The API version to use.
+
     ext : `str`, optional
         the file extension for which to search.
 
@@ -377,7 +443,8 @@ def find_times(
     """  # noqa: E501
     qurl = _url(
         host,
-        api_ldr.find_times_path,
+        api,
+        "find_times_path",
         site,
         frametype,
         gpsstart,
@@ -410,6 +477,7 @@ def find_url(
     urltype="file",
     on_missing="error",
     host=None,
+    api=DEFAULT_API,
     session=None,
     **request_kw,
 ):
@@ -436,6 +504,9 @@ def find_url(
         :func:`~gwdatafind.utils.get_default_host` will be used to
         discover the default host.
 
+    api : `str`, optional
+        The API version to use.
+
     session : `requests.Session`, optional
         the connection session to use; if not given, a
         :class:`igwn_auth_utils.requests.Session` will be
@@ -465,7 +536,7 @@ def find_url(
     RuntimeError
         if no matching URLs are found and ``on_missing="error"`` was given
     """
-    qurl = _url(host, api_ldr.find_url_path, framefile)
+    qurl = _url(host, api, "find_url_path", framefile)
     return _get_urls(
         qurl,
         scheme=urltype,
@@ -481,7 +552,8 @@ def find_latest(
     urltype="file",
     on_missing="error",
     host=None,
-    ext=api_ldr.DEFAULT_EXT,
+    api=DEFAULT_API,
+    ext=DEFAULT_EXT,
     session=None,
     **request_kw,
 ):
@@ -510,6 +582,9 @@ def find_latest(
         :func:`~gwdatafind.utils.get_default_host` will be used to
         discover the default host.
 
+    api : `str`, optional
+        The API version to use.
+
     ext : `str`, optional
         the file extension for which to search.
 
@@ -549,7 +624,8 @@ def find_latest(
     """  # noqa: E501
     qurl = _url(
         host,
-        api_ldr.find_latest_path,
+        api,
+        "find_latest_path",
         site,
         frametype,
         ext=ext,
@@ -567,7 +643,8 @@ def find_urls(
     urltype="file",
     on_gaps="warn",
     host=None,
-    ext=api_ldr.DEFAULT_EXT,
+    api=DEFAULT_API,
+    ext=DEFAULT_EXT,
     session=None,
     **request_kw,
 ):
@@ -606,6 +683,9 @@ def find_urls(
         :func:`~gwdatafind.utils.get_default_host` will be used to
         discover the default host.
 
+    api : `str`, optional
+        The API version to use.
+
     ext : `str`, optional
         the file extension for which to search.
 
@@ -635,7 +715,8 @@ def find_urls(
     """
     qurl = _url(
         host,
-        api_ldr.find_urls_path,
+        api,
+        "find_urls_path",
         site,
         frametype,
         gpsstart,
-- 
GitLab