zapcore-sdk 0.1.1__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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ # A raiz ignora `zapcore` (o binário do motor) em qualquer nível, e o pacote Python tem esse nome.
8
+ !src/zapcore/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Nicolas Gudiño and contributors
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.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.5
2
+ Name: zapcore-sdk
3
+ Version: 0.1.1
4
+ Summary: SDK oficial da API REST v2 do zapCore (WhatsApp): tipado, zero dependência, retry que respeita 429/Retry-After, idempotência automática e verificação de webhook HMAC
5
+ Project-URL: Homepage, https://zapcore.app
6
+ Project-URL: Source, https://github.com/adeiltonpessini/zapcore/tree/main/sdks/python
7
+ Author: Adeilton Pessini
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: hmac,sdk,webhook,whatsapp,whatsapp-api,zapcore
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Communications :: Chat
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+
18
+ # zapcore-sdk (Python)
19
+
20
+ SDK oficial da API REST v2 do [zapCore](https://zapcore.app) (WhatsApp).
21
+ Python ≥ 3.9, **zero dependência** (só biblioteca padrão), tipado (`TypedDict` + `py.typed`).
22
+
23
+ > **Ainda não publicado no PyPI.** Não rode `pip install zapcore-sdk`: o nome ainda não é nosso
24
+ > no registro. Por enquanto instale desta pasta, a partir de um clone do repositório:
25
+ >
26
+ > ```sh
27
+ > pip install ./sdks/python
28
+ > ```
29
+
30
+ ## Início rápido
31
+
32
+ ```python
33
+ from zapcore import Client, ZapcoreError
34
+
35
+ zap = Client("https://api.seudominio.com", token="TOKEN_DA_INSTANCIA") # host sem /api/v2
36
+
37
+ zap.send_text("+55 (27) 99999-9999", "Olá!") # pontuação é limpa
38
+ zap.send_image("5527999999999", open("foto.jpg", "rb").read(), "Seu pedido")
39
+ zap.send_image("5527999999999", "https://site/foto.jpg") # o SDK baixa
40
+ zap.send_buttons("5527999999999", "Confirma o horário?", ["Sim", "Remarcar"])
41
+ zap.send_list("5527999999999", "Escolha o serviço", "Ver opções",
42
+ [{"title": "Cabelo", "rows": [{"RowId": "corte", "title": "Corte", "desc": "R$ 50"}]}])
43
+ zap.send_poll("120363000000000000@g.us", "Melhor dia?", ["Sexta", "Sábado"])
44
+
45
+ zap.status()["connected"]
46
+ zap.get_qr()["QRCode"] # data URL
47
+ zap.pair_phone("5527999999999")["LinkingCode"]
48
+ zap.create_group("Equipe", ["5527999990000"])
49
+ zap.check_numbers(["5527999999999"])[0]["IsInWhatsapp"]
50
+
51
+ for msg in zap.iter_history("5527999999999@s.whatsapp.net"): # pagina sozinho
52
+ print(msg["timestamp"], msg.get("text_content"))
53
+ ```
54
+
55
+ Toda rota existe como método gerado: `zap.post_chat_send_carousel(...)`,
56
+ `zap.get_group_list(query={"limit": 50})`… (235 operações). Para `meta`, status e headers use
57
+ `zap.request("GET", "/api/v2/session/limits")`.
58
+
59
+ ## Votar em enquete, GIF, link, fila assíncrona e busca de conversas
60
+
61
+ Métodos gerados (nome = `operationId` da spec, em snake_case):
62
+
63
+ ```python
64
+ # Votar numa enquete recebida (poll_msg_id do histórico local; options exatas)
65
+ zap.post_chat_send_pollvote({"poll_msg_id": "3EB0...", "options": ["Sexta"]})
66
+ # GIF (mp4 até 16 MB; URL ou data URL) e link com cartão de prévia
67
+ zap.post_chat_send_gif({"Phone": "5527999999999", "Gif": "https://site/anim.mp4", "Caption": "Oi"})
68
+ zap.post_chat_send_link({"Phone": "5527999999999", "Url": "https://site/oferta", "Body": "Veja:"})
69
+
70
+ # Fila assíncrona: enfileirar (async=true na query), listar, consultar, cancelar
71
+ zap.post_chat_send_text({"Phone": "5527999999999", "Body": "oi"},
72
+ query={"async": True, "track_source": "crm", "track_id": "pedido-42"})
73
+ zap.get_chat_send_async(query={"limit": 50})
74
+ zap.get_chat_send_async_id(id)
75
+ zap.delete_chat_send_async_id(id) # 409 se já saiu
76
+ zap.post_chat_send_async_cancel({"track_source": "crm", "track_id": "pedido-42"}) # em massa
77
+
78
+ # Busca de conversas com filtros ("counts" só vem na 1ª página: sem cursor e offset 0)
79
+ r = zap.get_chat_search(query={"type": "group", "unread": True, "limit": 50})
80
+ ```
81
+
82
+ Os filtros da listagem da fila (`status`, `track_source`, `track_id`, `since`) são suportados
83
+ ([API.md](../../API.md)), na query de `get_chat_send_async`.
84
+
85
+ ## Erros, retry e idempotência
86
+
87
+ ```python
88
+ try:
89
+ zap.send_text("5527999999999", "oi", idempotency_key=pedido_id)
90
+ except ZapcoreError as e:
91
+ if e.code == "rate_limited": # case por code, nunca pela mensagem
92
+ print("tente em", e.retry_after, "s")
93
+ print(e.status, e.field, e.request_id)
94
+ ```
95
+
96
+ `429` espera o `Retry-After` (até `max_retry_wait`, padrão 60 s); `502/503/504` e falha de
97
+ rede re-tentam só GET e envios. Envios levam `Idempotency-Key` automática, a mesma em todos
98
+ os retries (`auto_idempotency=False` desliga). Opções: `Client(..., max_retries=3, timeout=15,
99
+ headers={...}, transport=meu_transporte)`.
100
+
101
+ **Redirect nunca é seguido**: o transporte padrão não tem `HTTPRedirectHandler`, e um 3xx vira
102
+ `ZapcoreError(code="redirect_refused")`. Seguir levaria o header `token` para o `Location`.
103
+
104
+ ## Webhook (HMAC)
105
+
106
+ > **Verificação ESTRITA por padrão.** Só passa entrega com `x-hmac-signature-v2` +
107
+ > `x-webhook-timestamp` dentro de 5 min (anti-replay). Entrega só com a v1 (`x-hmac-signature`,
108
+ > sem carimbo) é **recusada**: ela não expira, então quem capturar uma pode reenviá-la para
109
+ > sempre. Motor antigo que ainda não manda a v2? Ligue ``allow_v1=True``, conscientemente.
110
+
111
+ ```python
112
+ from zapcore import parse_webhook, WebhookVerificationError
113
+
114
+ # Flask: request.get_data() é o corpo CRU, não use request.json
115
+ @app.post("/zapcore")
116
+ def webhook():
117
+ try:
118
+ ev = parse_webhook(request.get_data(), request.headers, os.environ["ZAPCORE_HMAC_SECRET"])
119
+ except WebhookVerificationError:
120
+ return "", 401
121
+ print(ev["type"], ev.get("event"))
122
+ return "", 200
123
+ ```
124
+
125
+ `verify_webhook(...)` devolve `bool` e nunca levanta por header malicioso; `tolerance_seconds`
126
+ ajusta a janela. Corpo assinado que não é objeto JSON levanta `WebhookVerificationError`.
127
+
128
+ ## Desenvolvimento
129
+
130
+ ```sh
131
+ PYTHONPATH=src python -m unittest discover -s tests
132
+ ```
133
+
134
+ `src/zapcore/_generated.py` é gerado por `node ../gerar.mjs`, não edite.
135
+
136
+ ---
137
+
138
+ ## English
139
+
140
+ Official Python SDK (≥ 3.9, stdlib only) for the zapCore WhatsApp REST API v2. One generated
141
+ method per operation (snake_case of the `operationId`) plus helpers (`send_text`, `send_image`,
142
+ `send_buttons`, `send_list`, `send_poll`, `get_qr`, `pair_phone`, `status`, `create_group`,
143
+ `check_numbers`, `history`/`iter_history`). Webhook verification is strict by default (v1-only deliveries need `allow_v1=True`) and redirects are never followed. Retries honor `429 Retry-After`, send routes get an
144
+ automatic idempotency key reused across retries, errors raise `ZapcoreError` with a stable
145
+ `code`, and `verify_webhook`/`parse_webhook` check the HMAC-SHA256 signature.
@@ -0,0 +1,128 @@
1
+ # zapcore-sdk (Python)
2
+
3
+ SDK oficial da API REST v2 do [zapCore](https://zapcore.app) (WhatsApp).
4
+ Python ≥ 3.9, **zero dependência** (só biblioteca padrão), tipado (`TypedDict` + `py.typed`).
5
+
6
+ > **Ainda não publicado no PyPI.** Não rode `pip install zapcore-sdk`: o nome ainda não é nosso
7
+ > no registro. Por enquanto instale desta pasta, a partir de um clone do repositório:
8
+ >
9
+ > ```sh
10
+ > pip install ./sdks/python
11
+ > ```
12
+
13
+ ## Início rápido
14
+
15
+ ```python
16
+ from zapcore import Client, ZapcoreError
17
+
18
+ zap = Client("https://api.seudominio.com", token="TOKEN_DA_INSTANCIA") # host sem /api/v2
19
+
20
+ zap.send_text("+55 (27) 99999-9999", "Olá!") # pontuação é limpa
21
+ zap.send_image("5527999999999", open("foto.jpg", "rb").read(), "Seu pedido")
22
+ zap.send_image("5527999999999", "https://site/foto.jpg") # o SDK baixa
23
+ zap.send_buttons("5527999999999", "Confirma o horário?", ["Sim", "Remarcar"])
24
+ zap.send_list("5527999999999", "Escolha o serviço", "Ver opções",
25
+ [{"title": "Cabelo", "rows": [{"RowId": "corte", "title": "Corte", "desc": "R$ 50"}]}])
26
+ zap.send_poll("120363000000000000@g.us", "Melhor dia?", ["Sexta", "Sábado"])
27
+
28
+ zap.status()["connected"]
29
+ zap.get_qr()["QRCode"] # data URL
30
+ zap.pair_phone("5527999999999")["LinkingCode"]
31
+ zap.create_group("Equipe", ["5527999990000"])
32
+ zap.check_numbers(["5527999999999"])[0]["IsInWhatsapp"]
33
+
34
+ for msg in zap.iter_history("5527999999999@s.whatsapp.net"): # pagina sozinho
35
+ print(msg["timestamp"], msg.get("text_content"))
36
+ ```
37
+
38
+ Toda rota existe como método gerado: `zap.post_chat_send_carousel(...)`,
39
+ `zap.get_group_list(query={"limit": 50})`… (235 operações). Para `meta`, status e headers use
40
+ `zap.request("GET", "/api/v2/session/limits")`.
41
+
42
+ ## Votar em enquete, GIF, link, fila assíncrona e busca de conversas
43
+
44
+ Métodos gerados (nome = `operationId` da spec, em snake_case):
45
+
46
+ ```python
47
+ # Votar numa enquete recebida (poll_msg_id do histórico local; options exatas)
48
+ zap.post_chat_send_pollvote({"poll_msg_id": "3EB0...", "options": ["Sexta"]})
49
+ # GIF (mp4 até 16 MB; URL ou data URL) e link com cartão de prévia
50
+ zap.post_chat_send_gif({"Phone": "5527999999999", "Gif": "https://site/anim.mp4", "Caption": "Oi"})
51
+ zap.post_chat_send_link({"Phone": "5527999999999", "Url": "https://site/oferta", "Body": "Veja:"})
52
+
53
+ # Fila assíncrona: enfileirar (async=true na query), listar, consultar, cancelar
54
+ zap.post_chat_send_text({"Phone": "5527999999999", "Body": "oi"},
55
+ query={"async": True, "track_source": "crm", "track_id": "pedido-42"})
56
+ zap.get_chat_send_async(query={"limit": 50})
57
+ zap.get_chat_send_async_id(id)
58
+ zap.delete_chat_send_async_id(id) # 409 se já saiu
59
+ zap.post_chat_send_async_cancel({"track_source": "crm", "track_id": "pedido-42"}) # em massa
60
+
61
+ # Busca de conversas com filtros ("counts" só vem na 1ª página: sem cursor e offset 0)
62
+ r = zap.get_chat_search(query={"type": "group", "unread": True, "limit": 50})
63
+ ```
64
+
65
+ Os filtros da listagem da fila (`status`, `track_source`, `track_id`, `since`) são suportados
66
+ ([API.md](../../API.md)), na query de `get_chat_send_async`.
67
+
68
+ ## Erros, retry e idempotência
69
+
70
+ ```python
71
+ try:
72
+ zap.send_text("5527999999999", "oi", idempotency_key=pedido_id)
73
+ except ZapcoreError as e:
74
+ if e.code == "rate_limited": # case por code, nunca pela mensagem
75
+ print("tente em", e.retry_after, "s")
76
+ print(e.status, e.field, e.request_id)
77
+ ```
78
+
79
+ `429` espera o `Retry-After` (até `max_retry_wait`, padrão 60 s); `502/503/504` e falha de
80
+ rede re-tentam só GET e envios. Envios levam `Idempotency-Key` automática, a mesma em todos
81
+ os retries (`auto_idempotency=False` desliga). Opções: `Client(..., max_retries=3, timeout=15,
82
+ headers={...}, transport=meu_transporte)`.
83
+
84
+ **Redirect nunca é seguido**: o transporte padrão não tem `HTTPRedirectHandler`, e um 3xx vira
85
+ `ZapcoreError(code="redirect_refused")`. Seguir levaria o header `token` para o `Location`.
86
+
87
+ ## Webhook (HMAC)
88
+
89
+ > **Verificação ESTRITA por padrão.** Só passa entrega com `x-hmac-signature-v2` +
90
+ > `x-webhook-timestamp` dentro de 5 min (anti-replay). Entrega só com a v1 (`x-hmac-signature`,
91
+ > sem carimbo) é **recusada**: ela não expira, então quem capturar uma pode reenviá-la para
92
+ > sempre. Motor antigo que ainda não manda a v2? Ligue ``allow_v1=True``, conscientemente.
93
+
94
+ ```python
95
+ from zapcore import parse_webhook, WebhookVerificationError
96
+
97
+ # Flask: request.get_data() é o corpo CRU, não use request.json
98
+ @app.post("/zapcore")
99
+ def webhook():
100
+ try:
101
+ ev = parse_webhook(request.get_data(), request.headers, os.environ["ZAPCORE_HMAC_SECRET"])
102
+ except WebhookVerificationError:
103
+ return "", 401
104
+ print(ev["type"], ev.get("event"))
105
+ return "", 200
106
+ ```
107
+
108
+ `verify_webhook(...)` devolve `bool` e nunca levanta por header malicioso; `tolerance_seconds`
109
+ ajusta a janela. Corpo assinado que não é objeto JSON levanta `WebhookVerificationError`.
110
+
111
+ ## Desenvolvimento
112
+
113
+ ```sh
114
+ PYTHONPATH=src python -m unittest discover -s tests
115
+ ```
116
+
117
+ `src/zapcore/_generated.py` é gerado por `node ../gerar.mjs`, não edite.
118
+
119
+ ---
120
+
121
+ ## English
122
+
123
+ Official Python SDK (≥ 3.9, stdlib only) for the zapCore WhatsApp REST API v2. One generated
124
+ method per operation (snake_case of the `operationId`) plus helpers (`send_text`, `send_image`,
125
+ `send_buttons`, `send_list`, `send_poll`, `get_qr`, `pair_phone`, `status`, `create_group`,
126
+ `check_numbers`, `history`/`iter_history`). Webhook verification is strict by default (v1-only deliveries need `allow_v1=True`) and redirects are never followed. Retries honor `429 Retry-After`, send routes get an
127
+ automatic idempotency key reused across retries, errors raise `ZapcoreError` with a stable
128
+ `code`, and `verify_webhook`/`parse_webhook` check the HMAC-SHA256 signature.
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "zapcore-sdk"
7
+ version = "0.1.1"
8
+ description = "SDK oficial da API REST v2 do zapCore (WhatsApp): tipado, zero dependência, retry que respeita 429/Retry-After, idempotência automática e verificação de webhook HMAC"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Adeilton Pessini" }]
14
+ keywords = ["zapcore", "whatsapp", "whatsapp-api", "sdk", "webhook", "hmac"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ "Typing :: Typed",
19
+ "Topic :: Communications :: Chat",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.urls]
24
+ Homepage = "https://zapcore.app"
25
+ Source = "https://github.com/adeiltonpessini/zapcore/tree/main/sdks/python"
26
+
27
+ [tool.hatch.build.targets.sdist]
28
+ include = ["src/zapcore", "tests", "README.md", "LICENSE"]
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/zapcore"]
@@ -0,0 +1,17 @@
1
+ """SDK oficial da API REST v2 do zapCore (WhatsApp).
2
+
3
+ from zapcore import Client
4
+ zap = Client("https://api.seudominio.com", token="TOKEN_DA_INSTANCIA")
5
+ zap.send_text("5527999999999", "Olá!")
6
+ """
7
+ from ._client import ApiResponse, Client, Page, SDK_VERSION, Transport, destino, parse_retry_after, urllib_transport
8
+ from ._errors import WebhookVerificationError, ZapcoreError
9
+ from ._generated import API_VERSION, SPEC_SHA256
10
+ from .webhook import parse_webhook, verify_webhook
11
+
12
+ __all__ = [
13
+ "Client", "ApiResponse", "Page", "Transport", "ZapcoreError", "WebhookVerificationError",
14
+ "verify_webhook", "parse_webhook", "destino", "parse_retry_after", "urllib_transport",
15
+ "SDK_VERSION", "API_VERSION", "SPEC_SHA256",
16
+ ]
17
+ __version__ = SDK_VERSION