Skip to content

Low-Level API Access

The SDK's high-level Client covers the most common workflows — hierarchy traversal, file I/O, metadata updates, and more. When you need more control — to call endpoints the client does not expose, inspect raw HTTP responses, or extend the client with custom logic — you work directly with the underlying ApiClient and model layer. This page documents those lower-level primitives.

Warning

The classes described here are auto-generated from the OpenAPI specification and are not part of the public SDK contract. Method signatures and model attributes may change between SDK versions without a deprecation notice.

Direct API Calls

When to use the raw API

The high-level fw.<method>() calls satisfy the vast majority of use cases. Drop to the raw ApiClient when you need to:

  • Call an endpoint the high-level client does not expose
  • Inspect HTTP status codes or response headers alongside the response body
  • Send a one-off custom header or query parameter without changing global defaults
  • Receive the raw bytes from a response without SDK deserialization

The ApiClient instance for an existing client is accessible via fw.api_client.

Using call_api() directly

ApiClient.call_api() accepts any resource path, HTTP method, and optional request body. Pass a model class name as a string for response_type to trigger automatic deserialization, or pass None to skip deserialization entirely. After every call_api() invocation — including those made internally by high-level fw.* methods — api_client.last_response holds the raw RESTResponse; its .data attribute is the response body as bytes, which is the most direct way to read the body when response_type=None is passed.

Retrieve raw JSON from an endpoint
import json

api_client = fw.api_client

_, status, headers = api_client.call_api(
    "/config",
    "GET",
    response_type=None,
    auth_settings=["ApiKey"],
    _return_http_data_only=False,
)

raw = json.loads(api_client.last_response.data)
print(f"HTTP {status} — site: {raw.get('site', {}).get('name')}")

Setting _return_http_data_only=False makes call_api() return a three-tuple of (data, status_code, headers) instead of just the deserialized data. Use this when you need to branch on the HTTP status code or read a response header.

Call an endpoint and read the response headers
data, status, headers = api_client.call_api(
    "/users/self",
    "GET",
    response_type="User",
    auth_settings=["ApiKey"],
    _return_http_data_only=False,
)

print(f"Headers: {headers}")
print(f"User: {data.id}")

Pass additional query parameters or headers directly to call_api() for a single call. These do not affect the global defaults set on the ApiClient instance.

Send a one-off query parameter and custom header
result, status, _ = api_client.call_api(
    "/sessions",
    "GET",
    query_params=[("limit", "10"), ("filter", "label=baseline")],
    header_params={"X-Debug-Trace": "my-script"},
    response_type="list[SessionOutput]",
    auth_settings=["ApiKey"],
    _return_http_data_only=False,
)

print(f"Returned {len(result)} sessions with status {status}")

Response parsing

By default, call_api() parses the response body and returns a typed SDK model. Set _preload_content=False to skip body loading and receive the raw requests.Response directly. This is useful when you need to stream a large binary response rather than hold it all in memory.

Some endpoints use a ticket-based flow: a POST returns a JSON ticket and a subsequent GET with that ticket delivers the file bytes. api_client.last_response.data holds the raw bytes of the most recent response, which is convenient for reading the ticket without a separate response_type parameter.

Retrieve raw bytes without automatic parsing
import json

# Step 1: POST to /download returns a JSON ticket, not the file.
api_client.call_api(
    "/download",
    "POST",
    body={"nodes": [{"level": "session", "_id": session_id}]},
    auth_settings=["ApiKey"],
    _return_http_data_only=True,
)
ticket = json.loads(api_client.last_response.data)["ticket"]

# Step 2: GET with the ticket streams the actual file bytes.
raw_resp = api_client.call_api(
    "/download",
    "GET",
    query_params=[("ticket", ticket)],
    auth_settings=["ApiKey"],
    _preload_content=False,
    _return_http_data_only=True,
)

with open("download.tar", "wb") as f:
    for chunk in raw_resp.iter_content(chunk_size=65536):
        f.write(chunk)

Custom API Client

Adding custom methods

For a single endpoint that does not warrant a full subclass, assign a method to an existing client instance at runtime. Python's types.MethodType binds a function to an object so that self resolves correctly, giving the function access to the client's _fw.api_client.

Note

The following example API calls use legacy Reader Tasks instead of the more modern Task Manager. To learn more about both Reader Tasks versions, see Clinical: Reader Tasks.

GET /readertasks/project/{project_id} is not exposed by the high-level client. Attaching it as a runtime method lets you call it on any existing flywheel.Client instance without defining a subclass.

Attach a reader task listing method at runtime
import json
import types


def get_reader_tasks(self, project_id, filter=None):
    query_params = []
    if filter is not None:
        query_params.append(("filter", filter))

    self._fw.api_client.call_api(
        f"/readertasks/project/{project_id}",
        "GET",
        query_params=query_params,
        auth_settings=["ApiKey"],
        _return_http_data_only=True,
    )
    return json.loads(self._fw.api_client.last_response.data)


fw.get_reader_tasks = types.MethodType(get_reader_tasks, fw)

todo = fw.get_reader_tasks(project_id, filter="status=Todo")
for task in todo["results"]:
    print(task["_id"])

The filter parameter accepts comma-separated expressions in the same format as other Flywheel API filters — for example, "status=Todo,assignee=user@example.com".

Extending the client

When you need to add several related methods for the same resource, a subclass keeps them organized and makes the intent clear. Client delegates unknown attribute lookups to the internal Flywheel instance via __getattr__, so a subclass retains full access to all existing SDK methods alongside the new ones.

Multiple legacy reader task endpoints are a natural set to bundle into an ExtendedClient:

Subclass Client to bundle multiple unexposed endpoints
import json
import flywheel


class ExtendedClient(flywheel.Client):
    def get_reader_tasks(self, project_id, filter=None):
        query_params = []
        if filter is not None:
            query_params.append(("filter", filter))

        self._fw.api_client.call_api(
            f"/readertasks/project/{project_id}",
            "GET",
            query_params=query_params,
            auth_settings=["ApiKey"],
            _return_http_data_only=True,
        )
        return json.loads(self._fw.api_client.last_response.data)

    def get_reader_task(self, task_id):
        self._fw.api_client.call_api(
            f"/readertasks/{task_id}",
            "GET",
            auth_settings=["ApiKey"],
            _return_http_data_only=True,
        )
        return json.loads(self._fw.api_client.last_response.data)

With all needed methods on the class, the workflow stays on a single client instance and the methods self-document as a unit:

List tasks and fetch full details for one
fw = ExtendedClient(api_key="your-api-key")

page = fw.get_reader_tasks(project_id, filter="status=Todo")
if page["results"]:
    task_ids = [task["_id"] for task in page["results"]]
    first_task = fw.get_reader_task(task_ids[0])
    print(first_task.get("assignee"))

Middleware patterns

ApiClient exposes two methods for attaching persistent request modifications:

Method Effect
set_default_header(name, value) Sends the header on every subsequent request
set_default_query_param(name, value) Appends the query parameter to every request

Use set_default_header() to inject a tracing identifier or a custom User-Agent without repeating the header on every individual call.

Attach a trace header to all requests for a script run
import uuid

api_client = fw.api_client
api_client.set_default_header("X-Script-Run-Id", str(uuid.uuid4()))
api_client.user_agent = "my-pipeline/1.0 flywheel-sdk"

set_default_query_param() is lower-level and appended unconditionally — it does not merge with existing query parameters of the same name, so avoid calling it more than once for the same parameter key.

Request and response interceptors

ApiClient supports a one-shot version check function via set_version_check_fn(). The function is called before the next request is dispatched and is cleared afterwards.

Register a version check function
def assert_minimum_version():
    version = fw.get_version()
    if version.release < "22.0":
        raise RuntimeError(f"Requires Flywheel >= 22.0, got {version.release}")

fw.api_client.set_version_check_fn(assert_minimum_version)

For persistent intercept logic — such as logging all request URLs or collecting timing metrics — subclass ApiClient and override call_api(), delegating to super() after your logic runs.

Subclass ApiClient to log every outbound request
import time
from flywheel.api_client import ApiClient


class LoggingApiClient(ApiClient):
    def call_api(self, resource_path, method, *args, **kwargs):
        start = time.monotonic()
        result = super().call_api(resource_path, method, *args, **kwargs)
        elapsed = time.monotonic() - start
        print(f"{method} {resource_path} completed in {elapsed:.3f}s")
        return result

To use a custom ApiClient subclass, initialize flywheel.Client as normal and then replace its api_client. Copying the configuration from the original carries over authentication and host settings automatically.

Wire a custom ApiClient into an existing client
import flywheel

fw = flywheel.Client()
config = fw.api_client.configuration
fw.api_client = LoggingApiClient(configuration=config)

Working with Raw Models

What SDK models are

SDK models are auto-generated Python classes produced from the OpenAPI specification by the Swagger code generator. They are not Pydantic models. Each model class carries two class-level dictionaries that describe its schema:

Attribute Purpose
swagger_types Maps Python attribute names to their declared types as strings
attribute_map Maps Python attribute names to their JSON key equivalents
rattribute_map The reverse — maps JSON keys back to Python attribute names
Inspect a model's type schema at runtime
session = fw.get_session(session_id)

for attr, type_str in session.swagger_types.items():
    json_key = session.attribute_map[attr]
    print(f"{attr} ({type_str}) -> JSON key: {json_key}")

Constructing models directly

Construct any SDK model by importing it from flywheel and passing keyword arguments. Only fields with non-None values are serialized when the model is sent to the API.

Build a session input model for a POST request
session_input = flywheel.SessionInput(
    label="Baseline",
    subject=flywheel.SessionEmbeddedSubject(label=subject_label, age=30),
    project=project_id,
)

new_session_id = fw.add_session(session_input)

Type safety with model attributes

The swagger_types dict documents the expected Python type for each attribute. Assigning a value of the wrong type does not raise an error at assignment time — the mismatch only surfaces when the model is serialized or when the server rejects the request.

Inspect expected types before assigning values
session_input = flywheel.SessionInput()

for attr, expected_type in session_input.swagger_types.items():
    print(f"  {attr}: {expected_type}")

Use this introspection when building models dynamically from external data sources to validate field types before making API calls.

Validate field values against swagger_types before sending
EXPECTED_TYPES = flywheel.SessionInput.swagger_types

def validate_session_payload(payload):
    for key, value in payload.items():
        expected = EXPECTED_TYPES.get(key)
        if not isinstance(value, expected)
            raise TypeError(
                f"Field '{key}' must be of type '{expected}', got {type(value).__name__}"
            )

Model serialization

ApiClient.sanitize_for_serialization() converts a model instance — or any nested structure of models, lists, and dicts — into a plain Python dict suitable for JSON encoding. It follows nested models recursively and omits attributes whose value is None.

Convert a model instance to a plain dict
import json

api_client = fw.api_client

session = fw.get_session(session_id)

session_dict = api_client.sanitize_for_serialization(session)
print(json.dumps(session_dict, default=str, indent=2))

The output uses the JSON keys from attribute_map — for example, the Python attribute id maps to the JSON key _id.

Model deserialization

Pass a response_type string to call_api() to receive a fully deserialized model object. The response_type must match a class name exported from flywheel.models.

Call an endpoint and receive a typed model object
session = api_client.call_api(
    f"/sessions/{session_id}",
    "GET",
    response_type="SessionOutput",
    auth_settings=["ApiKey"],
    _return_http_data_only=True,
)

print(type(session).__name__)  # SessionOutput
print(session.label)

For list responses, wrap the type string in list[...]:

Deserialize a list endpoint response
sessions = api_client.call_api(
    "/sessions",
    "GET",
    query_params=[("limit", "10")],
    response_type="list[SessionOutput]",
    auth_settings=["ApiKey"],
    _return_http_data_only=True,
)

for session in sessions:
    print(session.label)