Metadata-Version: 2.5
Name: mitra-feature-flags-sdk-python
Version: 1.1.0
Summary: Feature flag SDK for Mitra Python services
Project-URL: Homepage, https://github.com/mitralab-dev/mitra-feature-flags-sdk-python
Project-URL: Repository, https://github.com/mitralab-dev/mitra-feature-flags-sdk-python
Project-URL: Issues, https://github.com/mitralab-dev/mitra-feature-flags-sdk-python/issues
Author: Mitra Platform
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx<1,>=0.28.1
Provides-Extra: test
Requires-Dist: build<2,>=1.2; extra == 'test'
Requires-Dist: mypy<2,>=1.15; extra == 'test'
Requires-Dist: pytest-asyncio<1,>=0.25; extra == 'test'
Requires-Dist: pytest-cov<7,>=6; extra == 'test'
Requires-Dist: pytest<9,>=8.3; extra == 'test'
Requires-Dist: ruff<1,>=0.11; extra == 'test'
Requires-Dist: twine<8,>=7; extra == 'test'
Description-Content-Type: text/markdown

# Mitra Feature Flags SDK Python

SDK assíncrono para carregar feature flags em serviços Python da Mitra e avaliá-las localmente. O SDK mantém um snapshot imutável em memória, preserva o último valor válido em falhas e usa o fallback declarado no código quando uma flag não existe.

## Instalação

```bash
pip install mitra-feature-flags-sdk-python
```

## Uso

```python
from mitra_feature_flags import (
    BooleanFlag,
    FeatureFlagsClient,
    FeatureFlagsConfig,
    JsonFlag,
    StringFlag,
    StringListFlag,
)

EMBEDDED_IDE = BooleanFlag("EMBEDDED_IDE", False)
TENANTS_IN_ROLLOUT = StringListFlag("TENANTS_IN_ROLLOUT")
CHECKOUT_MODE = StringFlag("CHECKOUT_MODE", "legacy")
PRICING_CONFIG = JsonFlag("PRICING_CONFIG", {"trial_days": 7})

feature_flags = FeatureFlagsClient(
    FeatureFlagsConfig(
        service_name="mitra-sandbox",
        internal_secret=settings.internal_secret,
    )
)

feature_flags.start()

if feature_flags.is_enabled(EMBEDDED_IDE):
    ...

if feature_flags.contains(TENANTS_IN_ROLLOUT, tenant_id):
    ...

if feature_flags.get_string(CHECKOUT_MODE) == "new":
    ...

trial_days = feature_flags.get_json(PRICING_CONFIG).get("trial_days", 7)

await feature_flags.close()
```

`start()` é síncrono: agenda o refresh em segundo plano e retorna sem bloquear o startup. `is_enabled()`, `get_string_list()`, `contains()`, `get_string()` e `get_json()` leem somente memória e nunca fazem I/O.

## Tipos de flag

| Declaração | Leitura | Valor no snapshot |
|---|---|---|
| `BooleanFlag(name, default)` | `is_enabled()` | boolean |
| `StringListFlag(name, default=())` | `get_string_list()` e `contains()` | lista de strings, lida como `tuple` |
| `StringFlag(name, default)` | `get_string()` | string |
| `JsonFlag(name, default)` | `get_json()` | objeto JSON |

O valor de `JsonFlag` é somente leitura em toda a profundidade: objetos viram `MappingProxyType` e arrays viram `tuple`, tanto no default quanto no que vem do snapshot. Quando a flag não existe no snapshot ou chega com outro tipo, a leitura devolve o default declarado no código. O SDK garante que o valor é um objeto JSON, não o formato dele: leia as chaves de que o código precisa com fallback, porque o documento cadastrado no backoffice pode mudar.

## Configuração

| Campo | Obrigatório | Default | Descrição |
|---|---|---|---|
| `service_name` | sim | sem default | Nome do serviço em lower kebab case, com até 100 caracteres. |
| `internal_secret` | sim | sem default | Segredo usado no header interno. Nunca aparece no `repr` da configuração. |
| `base_url` | não | `http://mitra-feature-flags.mitra.local:8080` | URL do control plane. |
| `refresh_interval_seconds` | não | `60.0` | Intervalo base entre atualizações. |
| `refresh_jitter_seconds` | não | `10.0` | Variação aleatória adicional. Aceita zero. |
| `connect_timeout_seconds` | não | `0.2` | Timeout para abrir a conexão HTTP. |
| `read_timeout_seconds` | não | `0.5` | Timeout para receber a resposta HTTP. |

## Lifecycle com FastAPI

O SDK não depende de FastAPI, mas encaixa no lifecycle nativo da aplicação:

```python
from contextlib import asynccontextmanager

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(_app: FastAPI):
    feature_flags.start()
    yield
    await feature_flags.close()


app = FastAPI(lifespan=lifespan)
```

## Contrato

- Nomes de flags usam `UPPER_SNAKE_CASE` e têm até 100 caracteres.
- Valores aceitos são boolean, string, lista de strings ou objeto JSON.
- O snapshot é filtrado pelo `service_name` enviado ao control plane.
- O SDK envia `valueTypes=BOOLEAN,STRING_LIST,STRING,JSON`. O control plane só entrega flags STRING e JSON a quem declara esses tipos, porque versões anteriores do SDK rejeitam o snapshot inteiro diante de um valor desconhecido. Contra um control plane que ainda não suporta os tipos novos, o parâmetro é ignorado e as flags STRING e JSON ficam no default do código.
- `X-Internal-Secret` nunca é logado.
- `ETag` evita baixar snapshots sem alteração.
- Payload inválido ou serviço indisponível preserva o último snapshot válido.

## Desenvolvimento

```bash
ruff check .
mypy
pytest
python -m build
python -m twine check dist/*
```
