Metadata-Version: 2.5
Name: shade-plugins-notion
Version: 0.1.0
Summary: Consultar uma base, criar página (sem duplicar, pela coluna "Chave Shade") e atualizar página no Notion, com o token OAuth da conexão da plataforma.
Author: ShadeOne
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: shade-sdk<0.23,>=0.22.3
Description-Content-Type: text/markdown

# shade-plugins-notion

Consulta uma base, cria uma página (sem duplicar) e atualiza uma página no
[Notion](https://developers.notion.com/), de dentro de um robô do
[`shade-sdk`](https://pypi.org/project/shade-sdk/), com o token OAuth da conexão do Notion na
plataforma. Só a biblioteca padrão do Python: nenhuma dependência além do `shade-sdk`.

No `shade.toml` do robô, o host da API vai em `[hosts] permitidos` e a conexão em `[conexoes]`:

```toml
[hosts]
permitidos = ["api.notion.com"]

[conexoes]
nomes = ["notion"]

[plugins]
notion = "0.1.0"
```

```python
import shade_plugins_notion as notion

BASE = "0f1e2d3c4b5a69788796a5b4c3d2e1f0"  # o id, da URL da base

# Leitura: roda inclusive no ensaio.
novos = notion.consultar_base(BASE, filtro={"property": "Status", "select": {"equals": "Novo"}})
nomes = [notion.texto_de(p["propriedades"]["Nome"]) for p in novos]

# Escrita: só dentro de item.efetivar() (no ensaio não roda).
with item.efetivar("registrar o pedido"):
    nova = notion.criar_pagina(
        BASE,
        {"Nome": notion.titulo(f"Pedido {item['numero']}"), "Valor": {"number": item["valor"]}},
    )
    notion.atualizar_pagina(nova["id"], {"Observação": notion.texto(item["detalhe"])})
```

## Antes de usar

- **Crie na base a coluna de texto `Chave Shade`.** É o que faz `criar_pagina` não duplicar. Sem ela,
  a operação recusa com `ColunaAusente` (um `ErroDeNegocio`) antes de qualquer escrita. A coluna é do
  plugin: não a edite, e o robô não pode passá-la em `propriedades`.
- **Compartilhe a base com a conexão.** O Notion não tem escopos: a conexão só vê o que a pessoa
  escolheu ao conectar ou adicionou depois no menu da página. O que não foi compartilhado responde
  `404`, igual ao que não existe.

## A conta

- **Conexão:** o robô nunca vê a senha nem o refresh token. O token de acesso chega sob demanda da
  conexão `notion` da plataforma e é renovado por ela. Fora da plataforma (no laptop), ponha um token
  em `SHADE_TOKEN_NOTION`.
- **Fonte de dados:** uma base do Notion pode ter mais de uma. O plugin lê as da base a cada chamada;
  com mais de uma, passe `fonte=` (o nome ou o id); sem ele, o erro é `FonteNaoEscolhida` (um
  `ErroDeNegocio`) e a mensagem lista os nomes.
- **Versão da API:** `Notion-Version: 2026-03-11`, fixa. Versão nova da API é versão nova do plugin.

## O que o plugin garante

- **Página sem duplicar.** `criar_pagina` grava a chave do item na coluna `Chave Shade` e, antes de
  criar, consulta a base por ela; achando, devolve a página (`reaproveitada=True`). Depois de
  despachado, o pedido de criação nunca é repetido: se a resposta se perde, o plugin consulta de novo
  e, sem achar, o erro é `NotionIncerto` (a página pode existir: confira no Notion antes de refazer).
- **Atualização idempotente por natureza.** `atualizar_pagina` grava os valores pedidos, e refazer
  grava os mesmos. Ela também não se repete sozinha depois do envio: o Notion avisa que a escrita que
  respondeu `503` pode ter gravado. Sem confirmação, é `NotionIncerto`, e refazer é seguro.
- **Só dentro de `item.efetivar()`.** Fora dele, a operação é um erro de contrato antes de qualquer
  byte; no ensaio, o bloco é pulado e nada é enviado.
- **Valores literais.** As propriedades são objetos serializados por `json.dumps`, e `titulo()` e
  `texto()` põem o dado em `text.content`: um `**` ou um link do dado vai como texto. Os ids são
  validados antes de qualquer pedido.
- **Host e prazo:** só `api.notion.com`, com o certificado verificado, sem seguir redirecionamento;
  toda chamada aceita `timeout=` (30 s por padrão), que vale para a chamada inteira.
- **Sem conteúdo em evento nem em erro:** cada chamada emite um evento com a operação, o host, a
  duração e o status. Nunca as propriedades, o token nem o corpo da resposta.

## Operações

| Operação | Efeito | O que faz |
|---|---|---|
| `consultar_base(base, filtro=None, *, limite=100, fonte=None)` | não | Páginas da base: `id`, `url` e `propriedades`. `filtro` é o da API do Notion. Segue o `next_cursor`. |
| `criar_pagina(base, propriedades, *, fonte=None)` | sim | Cria a página na base. Devolve `id`, `url` e `reaproveitada`. |
| `atualizar_pagina(pagina, propriedades)` | sim | Grava as propriedades pedidas. Devolve `id` e `url`. |

`propriedades` vão no formato da API do Notion, pelo nome da coluna: `{"Valor": {"number": 1}}`,
`{"Status": {"select": {"name": "Feito"}}}`. Para as de texto, `notion.titulo("x")` e
`notion.texto("x")` montam o valor e dividem o texto longo nas partes que o Notion aceita. Na leitura,
`notion.texto_de(propriedade)` devolve o texto.

## Limites

- Se alguém mover para a lixeira ou arquivar a página antes de um refazer, a consulta não a enxerga, e
  o refazer cria outra.
- A consulta pode demorar a enxergar a página recém-criada. Por isso, a dúvida depois de uma resposta
  perdida pode ficar `NotionIncerto`.
- `consultar_base` devolve até 5.000 páginas por chamada.
- Conteúdo da página (blocos), arquivo e criação de base ficam fora do plugin.

## Testes

A suíte de contrato do `shade_sdk.testing.plugins` e um servidor falso da API (TLS de uma CA de
teste, que recusa propriedade desconhecida, de outro tipo ou acima dos limites de texto, como a de
verdade) acompanham o código-fonte em `tests/`.
