> ## Documentation Index
> Fetch the complete documentation index at: https://docs.foff.twospoon.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Foff Python SDK: sync and async feature flag client

> Install the Foff Python SDK and resolve feature flag values by hierarchy using either the synchronous Client or the asyncio-native AsyncClient.

The Foff Python SDK gives you a synchronous `Client` and an `asyncio`-compatible `AsyncClient` for resolving feature flag values. Both clients poll the Foff API in the background, cache configs in memory, and expose the same `get_feature_config` method. Use the sync client for standard Python applications and the async client when running an async framework such as FastAPI or asyncio.

## Requirements

Python 3.10 or later.

## Installation

```bash theme={null}
pip install foff
```

## Quick start

<Tabs>
  <Tab title="Sync">
    ```python theme={null}
    from foff import Client, Config

    # STEP 1: Create appropriate config
    config = Config(
        api_key="your-api-key",
        base_url="https://foff.twospoon.ai/live",
        scope="name-of-your-scope",
        polling_interval=30,  # refresh configs every 30 seconds (0 to disable)
    )

    # STEP 2: Create the client with the config
    with Client(config) as client:
        # STEP 3: Retrieve configs for your created features for given hierarchies
        value = client.get_feature_config("my-feature", ["org-1", "team-a", "user-123"])
        print(value)
    ```
  </Tab>

  <Tab title="Async">
    ```python theme={null}
    import asyncio
    from foff import AsyncClient, Config

    config = Config(
        api_key="your-api-key",
        base_url="https://foff.twospoon.ai/live",
        scope="name-of-your-scope",
        polling_interval=30,
    )

    async def main():
        async with AsyncClient(config) as client:
            value = client.get_feature_config("my-feature", ["org-1", "team-a", "user-123"])
            print(value)

    asyncio.run(main())
    ```
  </Tab>
</Tabs>

<Note>
  Store your API key in an environment variable and read it with `os.environ["FOFF_API_KEY"]`. Never hard-code it in source files.
</Note>

## Configuration

Configure the SDK using the `foff.Config` dataclass.

| Field              | Type  | Required | Default | Description                                                                    |
| ------------------ | ----- | -------- | ------- | ------------------------------------------------------------------------------ |
| `api_key`          | `str` | Yes      |         | Your Foff API key.                                                             |
| `base_url`         | `str` | Yes      |         | Base URL of the Foff API. Use `https://foff.twospoon.ai/live`.                 |
| `scope`            | `str` | Yes      |         | The scope to fetch configs for, such as `production` or `staging`.             |
| `polling_interval` | `int` | No       | `30`    | How often, in seconds, to poll for config updates. Use `0` to disable polling. |

### Validation

<Note>
  Both `Client` and `AsyncClient` validate the config on construction. If `api_key`, `base_url`, or `scope` is empty, a `ValueError` is raised immediately before any network request is made.
</Note>

## Creating a client

### Sync client

Use `Client` as a context manager. The `with` block handles startup and shutdown automatically.

```python theme={null}
with Client(config) as client:
    value = client.get_feature_config("dark-mode", ["org-1", "team-a", "user-123"])
```

`Client` performs the following steps on entry:

<Steps>
  <Step title="Validate the config">
    Raises `ValueError` if `api_key`, `base_url`, or `scope` is empty.
  </Step>

  <Step title="Fetch all configs (blocking)">
    Makes a `GET` request to `{base_url}/api/v1/scopes/{scope}/configs` and populates the in-memory cache.
  </Step>

  <Step title="Start background polling">
    If `polling_interval > 0`, starts a background thread that re-fetches configs on the configured interval.
  </Step>
</Steps>

### Async client

Use `AsyncClient` as an async context manager in any `asyncio`-based application.

```python theme={null}
async with AsyncClient(config) as client:
    value = client.get_feature_config("dark-mode", ["org-1", "team-a", "user-123"])
```

`AsyncClient` follows the same steps as `Client`, but uses `asyncio` primitives for the background polling loop instead of a thread.

## Resolving feature configs

### `get_feature_config(feature_name, ordered_hierarchy)`

```python theme={null}
get_feature_config(feature_name: str, ordered_hierarchy: list[str]) -> Any
```

Returns the config value for a feature, resolved against a hierarchy. The call is synchronous (even on `AsyncClient`) and reads from the in-memory cache — no network request is made.

```python theme={null}
value = client.get_feature_config("dark-mode", ["org-1", "team-a", "user-123"])
```

### Hierarchy resolution algorithm

The SDK resolves from most-specific to least-specific:

1. It checks the full combination first — `org-1 + team-a + user-123`.
2. It then tries shorter prefixes — `org-1 + team-a`, then `org-1`.
3. If no override matches, it returns the feature's `"default"` value.
4. If the feature does not exist at all, it returns `None`.

This means you can define overrides at any level of your hierarchy — organisation, team, user, environment, region, service — and the SDK finds the most specific match automatically.

## Polling

When `polling_interval > 0`, the client refreshes configs in the background on every interval.

* Polling errors are silently ignored. The SDK continues serving the last successfully fetched config.
* Calling `close()` on `Client`, or `await close()` on `AsyncClient`, stops background polling and releases resources.

### Recommended polling intervals

| Use case            | Interval |
| ------------------- | -------- |
| Near-real-time      | 10s      |
| Standard            | 30–60s   |
| Low-traffic / batch | 300–600s |

## Client lifecycle

Use the context manager form wherever possible — it handles startup and shutdown automatically.

<Tabs>
  <Tab title="Sync">
    ```python theme={null}
    # Context manager (recommended)
    with Client(config) as client:
        ...

    # Manual
    client = Client(config)
    try:
        ...
    finally:
        client.close()
    ```
  </Tab>

  <Tab title="Async">
    ```python theme={null}
    # Context manager (recommended)
    async with AsyncClient(config) as client:
        ...

    # Manual
    client = AsyncClient(config)
    await client.start()
    try:
        ...
    finally:
        await client.close()
    ```
  </Tab>
</Tabs>
