Metadata-Version: 2.4
Name: insta-sophia-sdk
Version: 0.2.0
Summary: SDK oficial da API Nornir para coleta pública de perfis, posts e comentários do Instagram.
License: Proprietary
License-File: LICENSE
Keywords: instagram,scraping,sdk,nornir,sophia
Author: Team Sophia
Author-email: contato@sophialabs.com.br
Requires-Python: >=3.11,<4.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Dist: httpx (>=0.27,<1.0)
Project-URL: Repository, https://github.com/SophiaLab/insta_sophia_sdk
Description-Content-Type: text/markdown

# insta_sophia_sdk

SDK oficial da API **Nornir** para coleta pública de perfis, posts e comentários do Instagram.

O SDK fala apenas com a API. Toda a parte frágil (fingerprint TLS, descoberta de `doc_id`,
proxy, autorização) vive lá e pode mudar sem quebrar o seu código.

## Instalação

```bash
pip install insta-sophia-sdk
```

Ou, enquanto o pacote for privado:

```bash
pip install git+https://github.com/SophiaLab/insta_sophia_sdk.git
```

## Uso em três linhas

```python
from insta_sophia_sdk import SophiaClient

with SophiaClient(main_key="SUA_MAIN_KEY", client_key="SUA_CLIENT_KEY") as client:
    perfil = client.profile("nubank")
    print(perfil.followers)   # 1893011
```

As duas chaves vêm da sua ativação no Moneto: `main_key` é a que você recebe, `client_key` é
emitida quando a chave é ativada.

O SDK já aponta para a API de produção (`https://api.nornir.sophialabs.com.br`). Só passe
`base_url=` para falar com outro ambiente.

## Paginação sem cursor

Posts e comentários são iteradores — o SDK busca as páginas sob demanda e você **nunca vê
cursor**:

```python
for post in client.posts("nubank", limit=100):
    print(post.shortcode, post.url)

for comentario in client.comments("DW6kgl6Dhs1", limit=500):
    print(f"@{comentario.username}: {comentario.text}")
```

A busca é preguiçosa: `limit=100` com 12 itens por página faz 9 requisições, e sair do laço
depois de 3 itens faz **uma**. Nada é baixado à toa.

`limit` é teto, não garantia — se o conteúdo acabar antes, você recebe menos.

### Quando você quer o cursor

Para persistir o progresso e retomar depois:

```python
pagina = client.posts_page("nubank")
while True:
    for post in pagina.posts:
        processar(post)
    if not pagina.has_more:
        break
    pagina = client.posts_page("nubank", cursor=pagina.next_cursor)
```

## Tratando erros

```python
from insta_sophia_sdk import (
    SophiaClient, AuthenticationError, ActivationError,
    NotFoundError, RateLimitError, TransportError, SophiaError,
)

try:
    perfil = client.profile("perfil_que_talvez_nao_exista")
except NotFoundError:
    print("perfil não existe ou está indisponível")
except AuthenticationError:
    print("chaves inválidas")
except ActivationError:
    print("chave expirada ou bloqueada — verifique no Moneto")
except RateLimitError as e:
    print(f"limite atingido; tente em {e.retry_after}s")
except TransportError:
    print("a API não respondeu")
except SophiaError:
    print("qualquer outra falha do SDK")
```

`SophiaError` é a base — capturá-la pega tudo que o SDK levanta.

## Verificando pelo terminal

O pacote instala o comando `sophia`, que serve para conferir credenciais e conectividade sem
escrever código:

```bash
export SOPHIA_MAIN_KEY=...
export SOPHIA_CLIENT_KEY=...

sophia health
sophia profile nubank
sophia posts nubank --limit 50
sophia comments DW6kgl6Dhs1 --limit 100
sophia profile nubank --json
```

Ler as chaves do ambiente evita que elas fiquem no histórico do shell. O comando sai com
código `1` em erro, então dá para encadear em script.

## Proxy — você contrata, você passa

A API sai para o Instagram pelo **seu** proxy. O IP é seu, o custo é seu, e o isolamento
também: se um IP seu for bloqueado, isso não afeta outros clientes.

```python
client = SophiaClient(
    main_key="...", client_key="...",
    proxy="http://usuario:senha@geo.iproyal.com:12321",
)
```

Formato: `esquema://usuario:senha@host:porta` (http ou https).

Para paralelizar, abra clientes com proxies diferentes:

```python
clientes = [
    SophiaClient(main_key=K, client_key=C, proxy=p)
    for p in meus_proxies
]
```

> **Endereços internos são recusados.** A API rejeita proxy apontando para `127.0.0.1`,
> `localhost`, redes privadas (`10.x`, `192.168.x`) ou metadata de cloud
> (`169.254.169.254`), respondendo `422`. É proteção contra SSRF — sem ela, a API poderia
> ser usada para varrer a rede interna do servidor.

Omitir `proxy` faz a API usar o proxy dela, se houver — útil só em desenvolvimento.

## Limite de coletas simultâneas

Cada chave tem um teto de coletas em paralelo (padrão **4**). Acima dele a API responde
`429` e o SDK levanta `RateLimitError`:

```python
from insta_sophia_sdk import RateLimitError

try:
    perfil = client.profile("fulano")
except RateLimitError as e:
    print(f"muitas coletas ao mesmo tempo; aguarde {e.retry_after}s")
```

O teto não é arbitrário: acima de ~4 requisições simultâneas saindo do mesmo IP, o próprio
Instagram começa a recusar — com HTTP 200 e corpo vazio. Recusar na entrada evita gastar seu
proxy numa requisição que falharia adiante.

Para volume maior, use **mais proxies**, não mais threads no mesmo proxy.

## Controlando o ritmo

Duas coisas diferentes, e vale não confundir:

```python
client = SophiaClient(
    main_key="...", client_key="...",
    timeout=120.0,      # quanto ESPERAR uma requisição antes de desistir
    page_delay=1.5,     # quanto PAUSAR entre páginas ao iterar
)
```

`timeout` é o limite de paciência com uma requisição. Aumente se a API demorar — a primeira
coleta de um perfil pode passar pela descoberta de `doc_id`, que é lenta.

`page_delay` é a pausa entre páginas na iteração. Padrão `0` (sem pausa). Vale usar em
coleta longa: 500 comentários são ~35 requisições, e dispará-las em rajada pressiona a API
e o Instagram atrás dela.

```python
# coleta longa e comportada: uma página a cada 2 segundos
for comentario in client.comments("DW6kgl6Dhs1", limit=500):
    processar(comentario)
```

A pausa só acontece quando há próxima página a buscar — nunca depois da última, nem depois
de o `limit` ter sido atingido.

## Resiliência

Timeout em toda requisição (30s por padrão) e retry com backoff exponencial:

| Situação | Retenta? |
|---|---|
| Timeout, falha de rede | sim |
| 5xx | sim |
| 429 | sim, respeitando o `Retry-After` que o servidor mandar |
| 401, 403, 404, 422 | **não** |

Erro de autenticação nunca é retentado: repetir uma chave inválida não a torna válida, só
multiplica latência e ruído. Ajuste com `timeout=` e `max_retries=` no construtor.

## Campos que vêm nulos

`Post.like_count`, `Post.comment_count` e `Profile.posts_count` são `int | None` e chegam
**nulos** — a rota pública do Instagram não os expõe.

`None` significa "não sabemos", nunca "zero". Converter para zero faria você confundir um
post sem curtidas com um post cuja contagem é desconhecida.

Comentários **têm** métrica: `Comment.like_count` vem preenchido.

## Referência

| Método | Devolve |
|---|---|
| `profile(username)` | `Profile` |
| `posts(username, limit=None)` | `Iterator[Post]` |
| `comments(shortcode, limit=None)` | `Iterator[Comment]` |
| `posts_page(username, cursor=None)` | `PostsPage` |
| `comments_page(shortcode, cursor=None)` | `CommentsPage` |
| `health()` | `bool` — não levanta |
| `close()` | fecha a conexão (automático no `with`) |

`Profile.url` e `Post.url` são propriedades calculadas; `Comment.is_reply` diz se o
comentário responde a outro.

## Segurança

As chaves são mascaradas em `repr` e `str`, e não aparecem em mensagem de erro — o `repr` de
um objeto de configuração costuma acabar em issue de GitHub e log de terceiro.

Prefira variáveis de ambiente a chaves literais no código.

## Desenvolvimento

```bash
poetry install
poetry run pytest tests/ -v
poetry run ruff check . && poetry run ruff format --check .
poetry run mypy insta_sophia_sdk/ --ignore-missing-imports
```

A suíte não usa rede: o `MockTransport` nativo do httpx responde no lugar do servidor.

