bfocus 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bfocus-0.1.0/.gitignore +8 -0
- bfocus-0.1.0/LICENSE +21 -0
- bfocus-0.1.0/PKG-INFO +384 -0
- bfocus-0.1.0/README.md +357 -0
- bfocus-0.1.0/bfocus/__init__.py +45 -0
- bfocus-0.1.0/bfocus/_client.py +95 -0
- bfocus-0.1.0/bfocus/_resources.py +862 -0
- bfocus-0.1.0/bfocus/_transport.py +335 -0
- bfocus-0.1.0/bfocus/_version.py +3 -0
- bfocus-0.1.0/bfocus/errors.py +131 -0
- bfocus-0.1.0/bfocus/py.typed +0 -0
- bfocus-0.1.0/bfocus/types.py +310 -0
- bfocus-0.1.0/bfocus/widget.py +36 -0
- bfocus-0.1.0/examples/quickstart.py +62 -0
- bfocus-0.1.0/pyproject.toml +40 -0
- bfocus-0.1.0/tests/_support.py +139 -0
- bfocus-0.1.0/tests/fixtures/cases.json +3133 -0
- bfocus-0.1.0/tests/test_conformance.py +296 -0
- bfocus-0.1.0/tests/test_unit.py +478 -0
- bfocus-0.1.0/tests/test_version.py +47 -0
bfocus-0.1.0/.gitignore
ADDED
bfocus-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Berni Software
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
bfocus-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bfocus
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SDK oficial em Python da API pública do bFocus: clientes, produtos, release notes, base de conhecimento e agentes de IA.
|
|
5
|
+
Project-URL: Homepage, https://bfocus.com.br
|
|
6
|
+
Project-URL: Repository, https://github.com/bernisoftware/bfocus-python
|
|
7
|
+
Author-email: Berni Software <tecnologia@bernisoftware.com.br>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: api,atendimento,base de conhecimento,bfocus,release notes,sdk,suporte
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Natural Language :: Portuguese (Brazilian)
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# bfocus
|
|
29
|
+
|
|
30
|
+
SDK oficial em **Python** da API pública do [bFocus](https://bfocus.com.br): clientes, produtos,
|
|
31
|
+
release notes, base de conhecimento e agentes de IA.
|
|
32
|
+
|
|
33
|
+
Zero dependências (só biblioteca padrão) · Python 3.9+ · tipada (`py.typed`) · novas tentativas e
|
|
34
|
+
idempotência automáticas.
|
|
35
|
+
|
|
36
|
+
## Instalação
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install bfocus==0.1.0
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Hello world
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from bfocus import Bfocus
|
|
46
|
+
|
|
47
|
+
bf = Bfocus("bf_live_...")
|
|
48
|
+
|
|
49
|
+
cliente = bf.customers.upsert("ERP 1042", name="Padaria Estrela", email="contato@padaria.example")
|
|
50
|
+
print(cliente["id"], cliente["name"])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`upsert` cria ou atualiza pelo `external_id` do **seu** sistema — rodar de novo não duplica.
|
|
54
|
+
|
|
55
|
+
## Autenticação
|
|
56
|
+
|
|
57
|
+
Crie a chave no bFocus em **Integrações → Chaves de API**, marcando só os escopos de que a
|
|
58
|
+
integração precisa. Ela vai em `Authorization: Bearer <chave>` em toda requisição (a SDK cuida disso).
|
|
59
|
+
|
|
60
|
+
| Escopo | Permite |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `customers:read` | Ler clientes, contatos, produtos vinculados e interações |
|
|
63
|
+
| `customers:write` | Cadastrar, atualizar e excluir clientes, contatos e interações |
|
|
64
|
+
| `products:read` | Ler o catálogo de produtos |
|
|
65
|
+
| `products:write` | Cadastrar, atualizar e arquivar produtos |
|
|
66
|
+
| `kb:read` | Ler e buscar artigos da base de conhecimento |
|
|
67
|
+
| `kb:write` | Criar, atualizar, publicar e excluir artigos da base de conhecimento |
|
|
68
|
+
| `ai_agents:read` | Ler os agentes de IA |
|
|
69
|
+
| `ai_agents:preview` | Testar a resposta de um agente de IA (consome IA da conta) |
|
|
70
|
+
| `release_notes:read` | Ler release notes |
|
|
71
|
+
| `release_notes:write` | Criar, atualizar e publicar release notes |
|
|
72
|
+
|
|
73
|
+
A chave legada (`bf_sk_…`) só alcança clientes (`customers:*`). Guarde a chave fora do código:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import os
|
|
77
|
+
from bfocus import Bfocus
|
|
78
|
+
|
|
79
|
+
bf = Bfocus(
|
|
80
|
+
os.environ["BFOCUS_API_KEY"],
|
|
81
|
+
base_url="https://api.bfocus.com.br", # padrão; em dev: "http://localhost:8000"
|
|
82
|
+
timeout=30, # segundos, por tentativa
|
|
83
|
+
max_retries=2, # novas tentativas além da primeira (0 desliga)
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Construir o cliente não faz nenhuma chamada de rede.
|
|
88
|
+
|
|
89
|
+
## Como os métodos funcionam
|
|
90
|
+
|
|
91
|
+
- **Retorno desembrulhado**: o método devolve o `data` da resposta, como `dict` (anotado com
|
|
92
|
+
`TypedDict` — veja `bfocus.types`). Campos novos que a API passar a devolver aparecem no dict;
|
|
93
|
+
nunca viram erro.
|
|
94
|
+
- **Listas paginadas** devolvem `Page` (`items`, `page`, `page_size`, `total`, `pages`), que é
|
|
95
|
+
iterável. Para percorrer tudo, use `list_all(...)` (clientes, interações, release notes e
|
|
96
|
+
artigos): um gerador preguiçoso que busca página por página (`page_size` padrão 100) e para na
|
|
97
|
+
última página ou numa página vazia.
|
|
98
|
+
- **Só o que você passa muda.** Os upserts são parciais: argumento não informado não é enviado;
|
|
99
|
+
`None` explícito vai como `null` e **limpa** o campo.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
bf.customers.upsert("ERP 1042", phone="11 3333-4444") # só o telefone muda
|
|
103
|
+
bf.customers.upsert("ERP 1042", phone=None) # apaga o telefone
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- Obrigatórios são posicionais; opcionais são nomeados. Toda chamada aceita `timeout=`; as de
|
|
107
|
+
escrita aceitam `idempotency_key=` (veja [Novas tentativas](#novas-tentativas-e-idempotência)).
|
|
108
|
+
- Datas (`updated_since`) aceitam `datetime` — convertido para ISO 8601 em UTC com `Z`; sem fuso é
|
|
109
|
+
tratado como UTC — ou string, que passa como veio.
|
|
110
|
+
|
|
111
|
+
## Clientes
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from datetime import datetime, timedelta, timezone
|
|
115
|
+
|
|
116
|
+
bf.customers.upsert(
|
|
117
|
+
"ERP 1042",
|
|
118
|
+
name="Padaria Estrela",
|
|
119
|
+
document="12.345.678/0001-90",
|
|
120
|
+
custom_fields=[{"key": "plano", "label": "Plano", "value": "ouro"}], # substitui a lista
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
cliente = bf.customers.get("ERP 1042")
|
|
124
|
+
|
|
125
|
+
pagina = bf.customers.list(q="padaria", page=1, page_size=50)
|
|
126
|
+
print(pagina.total, [c["name"] for c in pagina])
|
|
127
|
+
|
|
128
|
+
# Sincronização incremental: tudo o que mudou desde a última rodada, todas as páginas.
|
|
129
|
+
desde = datetime.now(timezone.utc) - timedelta(hours=1)
|
|
130
|
+
for c in bf.customers.list_all(updated_since=desde):
|
|
131
|
+
print(c["external_id"], c["updated_at"])
|
|
132
|
+
|
|
133
|
+
bf.customers.delete("ERP 1042")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Contatos, produtos vinculados e interações
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
bf.customers.contacts.upsert("ERP 1042", "CT-1", name="Ana Souza", role="Financeiro",
|
|
140
|
+
email="ana@padaria.example", is_primary=True)
|
|
141
|
+
bf.customers.contacts.list("ERP 1042")
|
|
142
|
+
bf.customers.contacts.delete("ERP 1042", "CT-1")
|
|
143
|
+
|
|
144
|
+
bf.customers.products.attach("ERP 1042", "erp-cloud")
|
|
145
|
+
bf.customers.products.list("ERP 1042")
|
|
146
|
+
bf.customers.products.detach("ERP 1042", "erp-cloud")
|
|
147
|
+
|
|
148
|
+
bf.customers.interactions.create("ERP 1042", "Pedido 1042 faturado.",
|
|
149
|
+
author_email="carla@suaempresa.com.br")
|
|
150
|
+
for i in bf.customers.interactions.list_all("ERP 1042"):
|
|
151
|
+
print(i["created_at"], i["content"])
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Produtos
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
bf.products.upsert("erp-cloud", name="ERP Cloud", description="Gestão na nuvem", color="#6366F1")
|
|
158
|
+
bf.products.get("erp-cloud")
|
|
159
|
+
bf.products.list(include_inactive=True)
|
|
160
|
+
bf.products.archive("erp-cloud") # arquiva, não apaga
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Release notes — publicar direto do CI
|
|
164
|
+
|
|
165
|
+
Um passo no pipeline de release: cria ou atualiza a nota da versão e já publica.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# scripts/publicar_release_note.py — roda no CI a cada tag
|
|
169
|
+
import os
|
|
170
|
+
import pathlib
|
|
171
|
+
|
|
172
|
+
from bfocus import Bfocus
|
|
173
|
+
|
|
174
|
+
bf = Bfocus(os.environ["BFOCUS_API_KEY"]) # escopo release_notes:write
|
|
175
|
+
versao = os.environ["GITHUB_REF_NAME"] # "v2.3.0" — o "v" na frente é aceito
|
|
176
|
+
|
|
177
|
+
bf.release_notes.upsert(
|
|
178
|
+
"erp-cloud",
|
|
179
|
+
versao,
|
|
180
|
+
title=f"Versão {versao.lstrip('v')}",
|
|
181
|
+
description_markdown=pathlib.Path(f"release-notes/{versao}.md").read_text(encoding="utf-8"),
|
|
182
|
+
audience="external", # "internal" | "external" | "both"
|
|
183
|
+
publish=True, # cria/atualiza e publica numa chamada só
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Rodar de novo para a mesma versão atualiza a nota (é upsert). Também há:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
bf.release_notes.get("erp-cloud", "2.3.0")
|
|
191
|
+
bf.release_notes.list("erp-cloud", published=False) # rascunhos
|
|
192
|
+
bf.release_notes.publish("erp-cloud", "2.3.0")
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Base de conhecimento — sincronizar a partir de arquivos Markdown
|
|
196
|
+
|
|
197
|
+
Mantenha a documentação no repositório e sincronize a cada push. `batch_upsert` aceita **qualquer
|
|
198
|
+
quantidade** de artigos: a SDK divide em lotes de 100 (o limite da API), envia em sequência e
|
|
199
|
+
devolve um único resultado.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
import os
|
|
203
|
+
import pathlib
|
|
204
|
+
|
|
205
|
+
from bfocus import Bfocus
|
|
206
|
+
|
|
207
|
+
bf = Bfocus(os.environ["BFOCUS_API_KEY"]) # escopos kb:read e kb:write
|
|
208
|
+
docs = pathlib.Path("docs")
|
|
209
|
+
|
|
210
|
+
artigos = []
|
|
211
|
+
for arquivo in sorted(docs.rglob("*.md")):
|
|
212
|
+
texto = arquivo.read_text(encoding="utf-8")
|
|
213
|
+
titulo = next((l[2:].strip() for l in texto.splitlines() if l.startswith("# ")), arquivo.stem)
|
|
214
|
+
artigos.append({
|
|
215
|
+
# id estável e SEM "/": o caminho do arquivo com ":" no lugar das barras.
|
|
216
|
+
# Aceita letras, números e . _ : ~ @ + = -
|
|
217
|
+
"external_id": "git:" + ":".join(arquivo.relative_to(docs).with_suffix("").parts),
|
|
218
|
+
"title": titulo,
|
|
219
|
+
"body_markdown": texto,
|
|
220
|
+
"product": "erp-cloud", # ou None (explícito) para um artigo global
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
res = bf.kb.articles.batch_upsert(artigos)
|
|
224
|
+
print(f"{res['created']} criados, {res['updated']} atualizados, "
|
|
225
|
+
f"{res['unchanged']} sem mudança, {res['failed']} com falha")
|
|
226
|
+
|
|
227
|
+
for r in res["results"]: # na mesma ordem enviada
|
|
228
|
+
if not r["ok"]:
|
|
229
|
+
print("falhou:", r["external_id"], r["error"]) # ex.: KB_ARTICLE_TITLE_REQUIRED
|
|
230
|
+
elif r["action"] in ("created", "updated"):
|
|
231
|
+
bf.kb.articles.publish(r["external_id"]) # publica o que entrou ou mudou
|
|
232
|
+
|
|
233
|
+
# Remove do bFocus o que saiu do repositório.
|
|
234
|
+
locais = {a["external_id"] for a in artigos}
|
|
235
|
+
for artigo in bf.kb.articles.list_all(product="erp-cloud"):
|
|
236
|
+
ext = artigo["external_id"] or ""
|
|
237
|
+
if ext.startswith("git:") and ext not in locais:
|
|
238
|
+
bf.kb.articles.delete(ext)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Um item com problema não derruba os outros: ele volta com `ok=False` e o motivo em `error`. Para
|
|
242
|
+
publicar já no lote, mande `"status": "published"` em cada item. Artigo a artigo:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
bf.kb.articles.upsert("notion:emitir-nfse", title="Como emitir NFS-e",
|
|
246
|
+
body_markdown="# Passo a passo\n\n1. Abra o menu **Fiscal**",
|
|
247
|
+
product=None, status="published")
|
|
248
|
+
bf.kb.articles.get("notion:emitir-nfse") # artigo completo, com body_html
|
|
249
|
+
bf.kb.articles.list(status="draft", q="nota") # Page de resumos (sem body_html)
|
|
250
|
+
bf.kb.articles.unpublish("notion:emitir-nfse")
|
|
251
|
+
bf.kb.articles.delete("notion:emitir-nfse")
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Busca
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
for hit in bf.kb.search("como emitir nota fiscal", product="erp-cloud", limit=3):
|
|
258
|
+
print(hit["title"], "—", hit["excerpt"])
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Agentes de IA
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
agentes = bf.ai_agents.list()
|
|
265
|
+
agente = bf.ai_agents.get(agentes[0]["id"])
|
|
266
|
+
|
|
267
|
+
resposta = bf.ai_agents.preview(
|
|
268
|
+
agente["id"],
|
|
269
|
+
"Como emito uma NFS-e?",
|
|
270
|
+
history=[{"role": "customer", "content": "Oi"},
|
|
271
|
+
{"role": "bot", "content": "Olá! Como posso ajudar?"}],
|
|
272
|
+
)
|
|
273
|
+
print(resposta["action"], resposta["answer_html"], resposta["sources"])
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`preview` consome IA da conta (escopo `ai_agents:preview`).
|
|
277
|
+
|
|
278
|
+
## Erros
|
|
279
|
+
|
|
280
|
+
Qualquer resposta fora de 2xx levanta `BfocusError` (ou uma subclasse):
|
|
281
|
+
|
|
282
|
+
| Classe | Quando |
|
|
283
|
+
| --- | --- |
|
|
284
|
+
| `AuthenticationError` | 401 — chave ausente, inválida ou revogada |
|
|
285
|
+
| `PermissionDeniedError` | 403 — chave desligada, IP não liberado ou escopo faltando (`required_scope`) |
|
|
286
|
+
| `NotFoundError` | 404 |
|
|
287
|
+
| `ConflictError` | 409 — ex.: `KB_ARTICLE_EMPTY`, `AI_DISABLED` |
|
|
288
|
+
| `ValidationError` | 422 — motivos por campo em `validation` |
|
|
289
|
+
| `RateLimitError` | 429 — `retry_after` em segundos (depois de esgotar as novas tentativas) |
|
|
290
|
+
| `ServerError` | 5xx |
|
|
291
|
+
| `NetworkError` | conexão/timeout — `status == 0`, `code == "NETWORK_ERROR"` |
|
|
292
|
+
|
|
293
|
+
**Decida pelo `code`** — ele é estável (`CUSTOMER_NOT_FOUND`, `INTEGRATION_SCOPE_MISSING`,
|
|
294
|
+
`VALIDATION_ERROR`…). O `message` é texto para humanos e pode mudar. Ao falar com o suporte,
|
|
295
|
+
informe o `request_id`: ele vem do corpo da resposta, senão do header `X-Request-Id`, senão é o id
|
|
296
|
+
que a própria SDK enviou (a API ecoa o do cliente) — então está sempre preenchido, inclusive em
|
|
297
|
+
`NetworkError`.
|
|
298
|
+
|
|
299
|
+
Se uma resposta 2xx chegar sem o envelope JSON da API (um proxy devolvendo HTML, corpo vazio), a
|
|
300
|
+
SDK não devolve `None` calado: levanta `BfocusError` com `code == "INVALID_RESPONSE"` e o status
|
|
301
|
+
recebido. Corpo de erro que não é JSON vira `code == "HTTP_<status>"`.
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from bfocus import BfocusError, NotFoundError
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
bf.customers.get("ERP 9999")
|
|
308
|
+
except NotFoundError:
|
|
309
|
+
print("não existe")
|
|
310
|
+
except BfocusError as err:
|
|
311
|
+
if err.code == "INTEGRATION_SCOPE_MISSING":
|
|
312
|
+
print("a chave não tem o escopo", err.required_scope)
|
|
313
|
+
elif err.code == "VALIDATION_ERROR":
|
|
314
|
+
print(err.validation) # {"email": "value is not a valid email address"}
|
|
315
|
+
else:
|
|
316
|
+
print(err.code, err.status, err.request_id)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Argumento inválido no seu código (chave vazia; parâmetro de caminho vazio, `"."` ou `".."`; `/`
|
|
320
|
+
no `external_id` de um artigo) levanta `ValueError`/`TypeError` na hora, sem chamar a API.
|
|
321
|
+
|
|
322
|
+
## Novas tentativas e idempotência
|
|
323
|
+
|
|
324
|
+
A SDK tenta de novo sozinha em **erro de rede/timeout, 429, 502, 503 e 504** — até `max_retries`
|
|
325
|
+
vezes (padrão 2). Espera o `Retry-After` quando a API manda (teto de 60 s); senão 0,5 s, 1 s, 2 s…
|
|
326
|
+
(teto de 8 s) + até 25% de variação aleatória. Um 500 ou outro 4xx volta na hora.
|
|
327
|
+
|
|
328
|
+
Toda escrita (POST/PUT/DELETE) leva um `Idempotency-Key`, e **a mesma chave vai em todas as
|
|
329
|
+
tentativas** da chamada: se a primeira chegou a executar e só a resposta se perdeu, a API devolve a
|
|
330
|
+
resposta original (`Idempotent-Replayed: true`) em vez de executar de novo. O `X-Request-Id` também
|
|
331
|
+
se repete, para o suporte ver as tentativas como uma chamada só.
|
|
332
|
+
|
|
333
|
+
Para que a proteção valha também quando o **seu** processo roda de novo (um job reexecutado), passe
|
|
334
|
+
uma chave derivada do evento:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
bf.customers.interactions.create(
|
|
338
|
+
"ERP 1042", "Pedido 1042 faturado.",
|
|
339
|
+
idempotency_key="pedido-1042-faturado",
|
|
340
|
+
)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
A mesma chave com outra requisição volta `IDEMPOTENCY_KEY_REUSED`. No `batch_upsert`, o 1º lote
|
|
344
|
+
usa a sua chave como veio e os seguintes `"<chave>:2"`, `"<chave>:3"`… (sem chave, cada lote gera
|
|
345
|
+
a sua).
|
|
346
|
+
|
|
347
|
+
## Identidade do widget
|
|
348
|
+
|
|
349
|
+
Para o widget de atendimento reconhecer o usuário logado, o **seu backend** assina a identidade
|
|
350
|
+
dele com o segredo do widget (que nunca vai para o navegador). É local — sem rede e sem chave de API:
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
import os
|
|
354
|
+
from bfocus import sign_widget_identity
|
|
355
|
+
|
|
356
|
+
assinatura = sign_widget_identity(
|
|
357
|
+
os.environ["BFOCUS_WIDGET_SECRET"],
|
|
358
|
+
user_external_id="USR-1", # o usuário no seu sistema
|
|
359
|
+
customer_external_id="ERP 1042", # a empresa (cliente) dele
|
|
360
|
+
)
|
|
361
|
+
# HMAC-SHA256 em hex minúsculo de "v1:USR-1:ERP 1042" — entregue junto dos dois ids à página
|
|
362
|
+
# que abre o widget.
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Versões
|
|
366
|
+
|
|
367
|
+
**Fixe a versão exata** (`bfocus==0.1.0` no `requirements.txt` / `pyproject.toml`) e suba de uma
|
|
368
|
+
versão para a outra de propósito. Cada release declara se muda a superfície pública (`additive` ou
|
|
369
|
+
`breaking: …`), então dá para saber o que revisar antes de subir.
|
|
370
|
+
|
|
371
|
+
A SDK se identifica em toda requisição (`X-Bfocus-Client: bfocus-python/<versão>`): quando uma
|
|
372
|
+
correção exigir atualizar, o bFocus avisa as contas que rodam a versão afetada.
|
|
373
|
+
|
|
374
|
+
## Exemplo
|
|
375
|
+
|
|
376
|
+
Um script rodável está em [`examples/quickstart.py`](examples/quickstart.py):
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
BFOCUS_API_KEY=bf_live_... python examples/quickstart.py
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Licença
|
|
383
|
+
|
|
384
|
+
MIT © Berni Software
|