.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
.. SPDX-License-Identifier: Apache-2.0
..
.. Licensed under the Apache License, Version 2.0 (the "License");
.. you may not use this file except in compliance with the License.
.. You may obtain a copy of the License at
..
.. http://www.apache.org/licenses/LICENSE-2.0
..
.. Unless required by applicable law or agreed to in writing, software
.. distributed under the License is distributed on an "AS IS" BASIS,
.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
.. See the License for the specific language governing permissions and
.. limitations under the License.

Programmatic API
================

**Python clients: the same search pipeline the UI runs, callable from code.**

Wheel exposes the same search pipeline that powers the web UI through two
Python clients, so scripts and notebooks can build slices, run benchmarks, or
upload artifacts without launching the web server locally.

Which Client
------------

The two differ in one respect: where the search actually runs.

``WheelClient`` runs it **in your own process**, reading the indices,
captions, and metadata straight off disk. Nothing has to be running first,
but the script needs access to the data files.

``WheelHTTPClient`` sends it to a **Wheel server that is already running**.
You need its URL and a login, but none of the data files.

Everything else is the same. The method names, the arguments, and the results
are identical, so a script written against one runs against the other by
changing how the client is constructed.

WheelClient
-----------

Point it at the same YAML the server reads and it runs queries directly.

.. code-block:: python

   from sil_wheel.client import WheelClient

   # Load the stores listed in the YAML; no server is started
   client = WheelClient.from_config("config/wheel_launch_dev_server_config.yaml")

   # Keyword search over captions, returning the matching clips
   result = client.search_caption("intersection")

   # One row per clip, with its per-modality scores
   df = result.as_dataframe()

   # Any helper takes extra filters, so modes compose in a single call
   result = client.search_classifier(
       "<classifier_run_id>",       # the run whose scores to filter on
       expression="p > 0.9",        # keep only high-confidence clips
       search_country="DE",         # and only clips recorded in Germany
   )

WheelHTTPClient
---------------

Point it at a running server. No data files and no YAML, just the URL and a
login. Scores are not sent back over the wire, so use ``result.clip_ids``.

.. code-block:: python

   from sil_wheel.http_client import WheelHTTPClient

   # Log in once; the session is reused for every later call
   client = WheelHTTPClient(
       server_url="http://wheel-host:8012",
       username="alice",
       password="...",
   )

   # Same call as the local client, executed on the server instead
   result = client.search_caption("intersection")

   # Full ranked list; scores are not returned over HTTP
   clip_ids = result.clip_ids

Both clients also accept URLs copied straight from the browser address bar
through ``search_from_url``:

.. code-block:: python

   # Replay a search built in the UI, filters and all
   result = client.search_from_url(
       "http://wheel-host:8012/?search=intersection&search_country=DE"
   )

Both the hash-fragment form (``http://host:port/#&search=...``) and the
plain query-string form (``?search=...``) are accepted, so any URL the
UI produces can be replayed verbatim.

Search Helpers
--------------

``search(**kwargs)`` accepts every filter documented in
:doc:`url_parameters`, and there is a named helper for each search mode:
``search_caption``, ``search_caption_embed``, ``search_semantic_text``,
``search_clip``, ``search_visual_text``, ``search_visual_image``,
``search_trajectory_pattern``, ``search_trajectory_shape``,
``search_classifier``, ``search_cluster``, ``search_country``,
``search_clip_list``, and ``search_world_model``.

Every helper takes extra keyword arguments, so filters compose in a single
call, as in the classifier example above.

Working With Results
--------------------

Every search returns the matching clips ranked by whichever mode is scoring
them, following the priority order in :doc:`search/index` when several are
active:

.. code-block:: python

   result.clip_ids            # full ranked list
   result.head(5)             # first 5
   result.as_dataframe()      # one row per clip, with its per-modality scores

``as_dataframe`` is the quickest way to check why clips ranked as they did, or
to export a slice for downstream use. It carries scores only for
``WheelClient``, since the server returns clip IDs alone.

Uploading Clustering Runs
-------------------------

``WheelHTTPClient`` ships an extra method, ``upload_clustering_run``, for the
offline-clustering workflow described in :doc:`search/cluster_search`. Give it
a directory holding a completed clustering run (``cluster_assignments.parquet``,
``representative_by_cluster.json``, ``umap.json``, ``metadata.json``, and
optionally ``cluster_topics.json``) and it ships them to the server. The new
run then shows up in the Clustering Tools panel exactly like an in-UI run.

.. code-block:: python

   # Ship a locally computed run to the server; overwrite=False keeps any
   # existing run with the same ID
   response = client.upload_clustering_run(run_dir, overwrite=False)

   # The ID to open in the Clustering Tools panel, and what was written
   print(response["run_id"], response["files_written"])

End-to-end runnable workflows live under ``examples/`` in the wheel
repository. ``examples/cluster_from_search.py`` is the canonical
reference: it copies a search URL from the UI, runs k-means and UMAP
locally against the FAISS embeddings, and uploads the resulting run
back to the server.

Common Patterns
---------------

- **Run a search from a script or notebook** without copy-pasting filter
  values from the URL.
- **Bulk-export slices** for downstream training or evaluation pipelines,
  writing the ranked clip IDs straight to disk or a parquet shard.
- **Reproduce a UI search** in code by handing the browser URL to
  ``search_from_url``, so a slice built interactively carries over into a
  script unchanged.
- **Scale clustering offline** and push the result back through
  ``upload_clustering_run``, letting you cluster runs that would not fit in
  the in-UI launcher's capacity.
