zsynctech-sdk 1.4.9__py3-none-any.whl

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,208 @@
1
+ Metadata-Version: 2.3
2
+ Name: zsynctech-sdk
3
+ Version: 1.4.9
4
+ Summary: SDK Python para conectar robôs (RPA) à plataforma zsynctech-studio via Socket.IO
5
+ Author: Rodrigo Zavan
6
+ Author-email: Rodrigo Zavan <rodrigo.zavan@zsynctech.com.br>
7
+ Requires-Dist: loguru>=0.7
8
+ Requires-Dist: pydantic>=2.9
9
+ Requires-Dist: python-socketio[client]>=5.11
10
+ Requires-Dist: requests>=2.31
11
+ Requires-Python: >=3.13
12
+ Description-Content-Type: text/markdown
13
+
14
+ # zsynctech-sdk
15
+
16
+ SDK Python oficial para conectar robôs de RPA à plataforma **zsynctech-studio**: handshake,
17
+ heartbeat, ciclo de execução, consumo de fila e acesso ao cofre de credenciais, tudo via
18
+ Socket.IO (namespace `/robot`).
19
+
20
+ ## Uso rápido
21
+
22
+ `client.on_automation_start` registra o que rodar quando a plataforma disparar o botão
23
+ "Iniciar" para esse robô. `client.listen()` mantém o processo vivo aguardando esse evento,
24
+ até a conexão encerrar ou você pressionar Ctrl+C.
25
+
26
+ Há duas formas de reportar trabalho, dependendo de onde a lista de tasks vem:
27
+
28
+ ### Consumindo a fila da plataforma
29
+
30
+ Use quando as tasks vêm de uma planilha importada na plataforma - `client.queue.consume()`
31
+ entrega uma a uma até a fila esvaziar.
32
+
33
+ ```python
34
+ from zsynctech_studio_sdk import RobotClient, TaskStatus
35
+
36
+ client = RobotClient.from_api_key(
37
+ "<sua-api-key>",
38
+ backend_url="http://localhost:5000",
39
+ )
40
+
41
+ @client.on_automation_start
42
+ def handle_automation() -> None:
43
+ with client.execution.run(observation="Minha automação"):
44
+ for task in client.queue.consume():
45
+ with client.queue.processing(task) as outcome:
46
+ resultado = processar(task.payload)
47
+ outcome.success(result=resultado)
48
+
49
+ with client:
50
+ client.listen()
51
+ ```
52
+
53
+ ### Sem fila: robô que processa a própria fonte de dados
54
+
55
+ Use quando o robô lê de uma fonte própria (planilha local, API interna, scraping, banco de
56
+ dados...) em vez de uma fila importada na plataforma - pule `client.queue` inteiramente e
57
+ reporte cada resultado direto com `client.execution.report_tasks(...)`.
58
+
59
+ ```python
60
+ from zsynctech_studio_sdk import RobotClient, TaskItem, TaskStatus
61
+ from zsynctech_studio_sdk.utils import utc_now
62
+
63
+ client = RobotClient.from_api_key(
64
+ "<sua-api-key>",
65
+ backend_url="http://localhost:5000",
66
+ )
67
+
68
+ @client.on_automation_start
69
+ def handle_automation() -> None:
70
+ itens = buscar_itens_a_processar() # a fonte de dados é sua
71
+
72
+ with client.execution.run(observation="Processamento de itens próprios"):
73
+ for item in itens:
74
+ started_at = utc_now()
75
+ resultado = processar(item)
76
+
77
+ client.execution.report_tasks([
78
+ TaskItem(
79
+ external_id=item.id,
80
+ status=TaskStatus.SUCCESS,
81
+ result=resultado,
82
+ started_at=started_at,
83
+ finished_at=utc_now(),
84
+ )
85
+ ])
86
+
87
+ with client:
88
+ client.listen()
89
+ ```
90
+
91
+ Exemplos completos em [`examples/`](examples/):
92
+
93
+ - [`simulate_robot.py`](examples/simulate_robot.py) - robô completo consumindo tasks de uma
94
+ fila da plataforma (`client.queue.consume()`); também demonstra `client.credentials.reveal()`
95
+ logo após conectar.
96
+ - [`own_tasks_robot.py`](examples/own_tasks_robot.py) - robô que processa a própria fonte de
97
+ dados (planilha, API interna, etc.) e reporta os resultados direto na execução
98
+ (`client.execution.report_tasks()`), sem usar fila.
99
+
100
+ ### Observação da execução
101
+
102
+ `observation` descreve o que está acontecendo numa execução - visível na plataforma. Dá pra
103
+ atualizá-la quantas vezes quiser enquanto a execução está rodando, e sobrescrevê-la no
104
+ `finish()` com o resultado final. `execution.run()` (o atalho com `with`) sempre finaliza com a
105
+ `observation` original, então esse controle mais fino pede o par `start()`/`finish()` manual:
106
+
107
+ ```python
108
+ client.execution.start(observation="Boletagem VCOM")
109
+ try:
110
+ tasks = list(client.queue.consume())
111
+ if not tasks:
112
+ client.execution.finish(observation="Fila vazia, nada a processar")
113
+ else:
114
+ for i, task in enumerate(tasks, start=1):
115
+ client.execution.update_observation(f"Processando {i}/{len(tasks)}")
116
+ ...
117
+ client.execution.finish()
118
+ except Exception:
119
+ client.execution.finish(ExecutionFinishStatus.FAILED)
120
+ raise
121
+ ```
122
+
123
+ ## Credenciais
124
+
125
+ O cofre de credenciais é acessado via REST (`/robot/credentials`), sem precisar de uma conexão
126
+ `/robot` aberta - `client.credentials` funciona mesmo antes de `client.connect()`:
127
+
128
+ ```python
129
+ credencial = client.credentials.reveal("<credential-id>")
130
+ print(credencial.value) # str (TEXT), dict[str, str] (KEY_VALUE) ou JSON (JSON)
131
+
132
+ client.credentials.rotate("<credential-id>", "novo-valor")
133
+
134
+ client.credentials.expire("<credential-id>", "Login rejeitado pelo site do fornecedor")
135
+ ```
136
+
137
+ Veja `log_credential()` em [`examples/simulate_robot.py`](examples/simulate_robot.py) para um
138
+ exemplo em contexto (revela uma credencial e loga o valor assim que conecta).
139
+
140
+ ### Buscando por `key` em vez de `id`
141
+
142
+ `id` é o identificador interno da credencial - regenerado se o banco da plataforma for
143
+ restaurado/migrado, quebrando qualquer robô que o tenha fixo no `.env`. `key` é um identificador
144
+ estável, definido pelo admin na plataforma (ex.: `"financeiro/senha-sap"`) e independente de
145
+ `id`/`name`/pasta - renomear a credencial ou movê-la de pasta nunca muda a `key`. Prefira `key`
146
+ para qualquer credencial referenciada de fora da plataforma:
147
+
148
+ ```python
149
+ credencial = client.credentials.reveal_by_key("financeiro/senha-sap")
150
+
151
+ client.credentials.rotate_by_key("financeiro/senha-sap", "novo-valor")
152
+
153
+ client.credentials.expire_by_key("financeiro/senha-sap", "Login rejeitado pelo site do fornecedor")
154
+ ```
155
+
156
+ ## Arquitetura
157
+
158
+ | Módulo | Responsabilidade |
159
+ | --- | --- |
160
+ | `client.py` | `RobotClient` - fachada única que compõe as peças abaixo |
161
+ | `connection.py` | `SocketConnection` - handshake, heartbeat automático, reconexão, `call`/`emit` |
162
+ | `execution.py` | `ExecutionManager` - ciclo `execution:start` / `execution:task` / `execution:finish` |
163
+ | `queue.py` | `QueueConsumer` - `queue:next` / `queue:task-result`, iteração da fila |
164
+ | `credentials.py` | `CredentialManager` - `reveal`/`rotate`/`expire`, por `id` ou `key`, via REST |
165
+ | `protocol.py` | Constantes do protocolo (namespace, nomes de eventos, rotas REST) |
166
+ | `exceptions.py` | Hierarquia de exceções do SDK (`RobotSDKError` e subclasses) |
167
+ | `utils.py` | Funções utilitárias reutilizáveis (detecção de plataforma, hostname, timestamps) |
168
+ | `loggers.py` | Configuração do `loguru` compartilhada pelo SDK |
169
+ | `models/` | Modelos de dados e enums (Pydantic), espelhando os DTOs/enums do backend |
170
+
171
+ Cada classe tem uma única responsabilidade (SRP): a conexão bruta não sabe o que é uma
172
+ execução, a execução não sabe como a fila funciona, e o `RobotClient` só orquestra as três.
173
+ Novos comportamentos (ex.: outro jeito de processar tasks) se registram via
174
+ `on_automation_start`/`queue.processing`, sem precisar alterar essas classes.
175
+
176
+ ## Modelos e enums (`models/`)
177
+
178
+ Todos os payloads trocados com a plataforma são validados com **Pydantic** e convertidos
179
+ entre `snake_case` (Python) e `camelCase` (protocolo) automaticamente via `SdkBaseModel`.
180
+ Os enums (`TaskStatus`, `RobotInstanceStatus`, `Platform`, ...) espelham exatamente os
181
+ enums TypeScript usados por `RobotGateway` no backend.
182
+
183
+ ## Logs
184
+
185
+ O SDK usa [`loguru`](https://loguru.readthedocs.io/) e já vem configurado com um formato
186
+ legível por padrão. Para customizar (nível, arquivo de destino, JSON estruturado):
187
+
188
+ ```python
189
+ from zsynctech_studio_sdk.loggers import configure_logging
190
+
191
+ configure_logging(level="DEBUG", sink="robot.log")
192
+ ```
193
+
194
+ Chame antes de criar o `RobotClient`.
195
+
196
+ ## Desenvolvimento
197
+
198
+ ```bash
199
+ uv run pytest # testes
200
+ uv run ruff check src examples tests # lint
201
+ uv run ruff format src examples tests # formatação
202
+ uv run mypy src examples tests # checagem de tipos (modo strict)
203
+ ```
204
+
205
+ `ruff` e `mypy` só olham `src`/`examples`/`tests` (não o README) - a partir do ruff 0.12+ o
206
+ `ruff format` também reformata blocos de código Python dentro de Markdown, o que reescreveria
207
+ os exemplos deste arquivo com as convenções de um `.py` (2 linhas em branco antes de decorators
208
+ etc.) toda vez que rodasse. CI usa esse mesmo escopo (veja `.github/workflows/ci.yml`).
@@ -0,0 +1,23 @@
1
+ zsynctech_studio_sdk/__init__.py,sha256=Lh7dG3cv8OfWcfpsDXiboxUeOgl_oaIRqlU6bdliBR0,2429
2
+ zsynctech_studio_sdk/client.py,sha256=kng1FspB9bSPHzRKqQpQ1cd9CEOGNOuPuetuJyCfv44,4926
3
+ zsynctech_studio_sdk/connection.py,sha256=rCDacmK7TUBm_OWShumuy8IAXxzfvFJ3BbH_GBJlB6Q,14309
4
+ zsynctech_studio_sdk/credentials.py,sha256=dOHa1w8T-1f5gP9SPuGwsjhD1JUst1Xotyo7SDeVpDs,5828
5
+ zsynctech_studio_sdk/exceptions.py,sha256=2GUATeFm_YRrCHL-IaLNd5V_GgjHU2nOfVVA7VOspbE,2139
6
+ zsynctech_studio_sdk/execution.py,sha256=Ff87pp7NG8xQizMaJPBLXAp2bdbgGd_6W51VRgbkApY,5044
7
+ zsynctech_studio_sdk/loggers.py,sha256=ndJuGUWxhIf2oeTUJ3DgMJyGsPHe0v1QiUNTBbHVRTU,1342
8
+ zsynctech_studio_sdk/models/__init__.py,sha256=QrCiltXmPV57ksss6uNOmD1Tdjlzqf0FTRb14L23gCo,971
9
+ zsynctech_studio_sdk/models/base.py,sha256=cpk747vvniKQtG60sKrejCo4Q9R9MRpl9sOA7gv4kLY,948
10
+ zsynctech_studio_sdk/models/config.py,sha256=NHzh2sLocql_1sFzL4NPNlukaAfdjp-3HXusGsgNDlI,2252
11
+ zsynctech_studio_sdk/models/credential.py,sha256=z2dKuztBWp4yNT1rYp0_f3Bk51QgZHE4B72v1MrhOBY,1363
12
+ zsynctech_studio_sdk/models/enums.py,sha256=Y3WXvxkP6q2FfYEti38yy8gGw1HD4MSIeolFrHS2dgY,3014
13
+ zsynctech_studio_sdk/models/execution.py,sha256=5kBYdC5bEjAYodvv688JGqh5A95UwRX7C8ku35Bs6NY,495
14
+ zsynctech_studio_sdk/models/robot.py,sha256=whsZGifKZY8U7uq2p8gmF99zodkWXxgLnxw6mqWDlBQ,1148
15
+ zsynctech_studio_sdk/models/task.py,sha256=qwb_jceIVpQZfSZm8CSpzKrxr93LVAVjaFzoZdRbPpo,1084
16
+ zsynctech_studio_sdk/protocol.py,sha256=cqjUfgt4mQrjVN32i684TJzwAv5ELy_CjORrhNJIJsQ,1296
17
+ zsynctech_studio_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ zsynctech_studio_sdk/queue.py,sha256=_nGs9eBHdPTb_bvfoaDQduDevDmZjTtHBYI3-BPKr2Y,4681
19
+ zsynctech_studio_sdk/utils.py,sha256=PIhPhT2M2D0DK3GErFCluGJ4FM-duj1zp_j3gq7fZtI,1526
20
+ zsynctech_sdk-1.4.9.dist-info/WHEEL,sha256=cmC5s21ojypbVslldL7IJq3hjZH-tINy4rziKePFsG0,81
21
+ zsynctech_sdk-1.4.9.dist-info/entry_points.txt,sha256=fSTqevc1T5Xt6E8WXoe9rCIru6z0KuZhEIKwz1cVipM,61
22
+ zsynctech_sdk-1.4.9.dist-info/METADATA,sha256=9f9JzmF4RysSUTPF8wYjXVOGC2c9Pgp6UP8iklrwiAs,8364
23
+ zsynctech_sdk-1.4.9.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.23
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ zsynctech-sdk = zsynctech_studio_sdk:main
3
+
@@ -0,0 +1,95 @@
1
+ """SDK Python para conectar robôs (RPA) à plataforma zsynctech-studio via `/robot` (Socket.IO).
2
+
3
+ Uso rápido:
4
+
5
+ from zsynctech_studio_sdk import RobotClient, TaskStatus
6
+
7
+ client = RobotClient.from_api_key("<sua-api-key>", backend_url="http://localhost:5000")
8
+
9
+ @client.on_automation_start
10
+ def handle_automation() -> None:
11
+ with client.execution.run(observation="Minha automação"):
12
+ for task in client.queue.consume():
13
+ with client.queue.processing(task) as outcome:
14
+ outcome.success(result={"ok": True})
15
+
16
+ with client:
17
+ client.listen()
18
+
19
+ O cofre de credenciais é acessado à parte, via REST (não precisa de conexão aberta):
20
+
21
+ credencial = client.credentials.reveal("<credential-id>")
22
+ print(credencial.value)
23
+
24
+ Veja `examples/simulate_robot.py` para um exemplo completo.
25
+ """
26
+
27
+ from .client import RobotClient
28
+ from .credentials import CredentialManager
29
+ from .exceptions import (
30
+ AckTimeoutError,
31
+ CredentialRequestError,
32
+ NoActiveExecutionError,
33
+ NotConnectedError,
34
+ RobotConnectionError,
35
+ RobotSDKError,
36
+ ServerRejectedError,
37
+ )
38
+ from .execution import ExecutionManager
39
+ from .models import (
40
+ ClaimedTask,
41
+ CredentialInfo,
42
+ CredentialStatus,
43
+ CredentialType,
44
+ ExecutionFinishResult,
45
+ ExecutionFinishStatus,
46
+ ExecutionStartResult,
47
+ Platform,
48
+ RevealedCredential,
49
+ RobotAvailability,
50
+ RobotClientConfig,
51
+ RobotConnection,
52
+ RobotInfo,
53
+ RobotInstanceStatus,
54
+ RobotStatus,
55
+ TaskItem,
56
+ TaskStatus,
57
+ )
58
+ from .queue import QueueConsumer, TaskOutcome
59
+
60
+ __all__ = [
61
+ "RobotClient",
62
+ "RobotClientConfig",
63
+ "ExecutionManager",
64
+ "QueueConsumer",
65
+ "TaskOutcome",
66
+ "CredentialManager",
67
+ "RobotSDKError",
68
+ "RobotConnectionError",
69
+ "NotConnectedError",
70
+ "AckTimeoutError",
71
+ "ServerRejectedError",
72
+ "NoActiveExecutionError",
73
+ "CredentialRequestError",
74
+ "ClaimedTask",
75
+ "TaskItem",
76
+ "RobotInfo",
77
+ "RobotConnection",
78
+ "ExecutionStartResult",
79
+ "ExecutionFinishResult",
80
+ "CredentialInfo",
81
+ "RevealedCredential",
82
+ "Platform",
83
+ "RobotStatus",
84
+ "RobotAvailability",
85
+ "RobotInstanceStatus",
86
+ "TaskStatus",
87
+ "ExecutionFinishStatus",
88
+ "CredentialStatus",
89
+ "CredentialType",
90
+ "main",
91
+ ]
92
+
93
+
94
+ def main() -> None:
95
+ print("zsynctech-studio-sdk - use RobotClient para conectar um robô à plataforma zsynctech-studio.")
@@ -0,0 +1,122 @@
1
+ """`RobotClient`: the SDK's single entry point.
2
+
3
+ Composes `SocketConnection` + `ExecutionManager` + `QueueConsumer` behind one high-level API,
4
+ so a robot script only ever talks to this class:
5
+
6
+ from zsynctech_studio_sdk import RobotClient, TaskStatus
7
+
8
+ client = RobotClient.from_api_key("...", backend_url="https://minha-plataforma.com")
9
+
10
+ @client.on_automation_start
11
+ def handle_automation() -> None:
12
+ with client.execution.run(observation="Processamento diário"):
13
+ for task in client.queue.consume():
14
+ with client.queue.processing(task) as outcome:
15
+ resultado = processar(task.payload)
16
+ outcome.success(result=resultado)
17
+
18
+ with client:
19
+ client.listen()
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from collections.abc import Callable
25
+ from types import TracebackType
26
+
27
+ from .connection import SocketConnection
28
+ from .credentials import CredentialManager
29
+ from .execution import ExecutionManager
30
+ from .loggers import configure_logging, logger
31
+ from .models.config import RobotClientConfig
32
+ from .models.enums import RobotInstanceStatus
33
+ from .models.robot import RobotConnection, RobotInfo
34
+ from .queue import QueueConsumer
35
+
36
+ AutomationStartHandler = Callable[[], None]
37
+
38
+
39
+ class RobotClient:
40
+ """High-level client for a single robot instance's connection to the platform."""
41
+
42
+ def __init__(self, config: RobotClientConfig) -> None:
43
+ configure_logging(level=config.log_level)
44
+ self._config = config
45
+ self._connection = SocketConnection(config)
46
+ self.execution = ExecutionManager(self._connection)
47
+ self.queue = QueueConsumer(self._connection, self.execution)
48
+ self.credentials = CredentialManager(config)
49
+
50
+ @classmethod
51
+ def from_api_key(cls, api_key: str, *, backend_url: str = "http://localhost:5000", **kwargs: object) -> RobotClient:
52
+ """Convenience constructor: `RobotClient.from_api_key("...", backend_url="...")`.
53
+
54
+ Any other `RobotClientConfig` field can be passed as a keyword argument (e.g.
55
+ `heartbeat_interval=30`).
56
+ """
57
+ config = RobotClientConfig(api_key=api_key, backend_url=backend_url, **kwargs)
58
+ return cls(config)
59
+
60
+ def connect(self) -> RobotConnection:
61
+ """Perform the handshake and block until the platform confirms the connection."""
62
+ logger.info("Conectando em {} como '{}'...", self._config.backend_url, self._config.hostname)
63
+ return self._connection.connect()
64
+
65
+ def disconnect(self) -> None:
66
+ self._connection.disconnect()
67
+
68
+ def on_automation_start(self, handler: AutomationStartHandler) -> AutomationStartHandler:
69
+ """Register `handler` to run (in a background thread) whenever the platform triggers
70
+ this robot's "Iniciar" action. Usable as a decorator."""
71
+ self._connection.on_automation_start(handler)
72
+ return handler
73
+
74
+ def set_status(self, status: RobotInstanceStatus) -> RobotInfo:
75
+ """Manually report this instance's status (ONLINE/BUSY) to the platform."""
76
+ return self._connection.set_status(status)
77
+
78
+ def listen(self) -> None:
79
+ """Block, waiting for the platform to trigger `automation:start`, until the connection
80
+ closes for good or the user presses Ctrl+C.
81
+
82
+ `automation:start` handlers keep firing on background threads while blocked here -
83
+ this just keeps the main thread (and the process) alive to host them. A dropped
84
+ connection (e.g. the API restarting) does not return from this call - the SDK keeps
85
+ reconnecting automatically in the background for as long as it's able to.
86
+ """
87
+ logger.info("Aguardando eventos da plataforma (Ctrl+C para encerrar)...")
88
+ try:
89
+ self._connection.wait()
90
+ except KeyboardInterrupt:
91
+ logger.info("Encerrando por solicitação do usuário (Ctrl+C)...")
92
+ finally:
93
+ self.disconnect()
94
+
95
+ @property
96
+ def connection_info(self) -> RobotConnection | None:
97
+ return self._connection.connection_info
98
+
99
+ @property
100
+ def is_connected(self) -> bool:
101
+ return self._connection.is_connected
102
+
103
+ @property
104
+ def is_stopping(self) -> bool:
105
+ """True once the connection is closed for good (Ctrl+C, `disconnect()`, or
106
+ reconnection permanently giving up) - see `SocketConnection.is_stopping`. An
107
+ `automation:start` handler with a loop over multiple units of work should poll this
108
+ between iterations and return early once it's true, instead of running to completion
109
+ with no connection left to report the result to."""
110
+ return self._connection.is_stopping
111
+
112
+ def __enter__(self) -> RobotClient:
113
+ self.connect()
114
+ return self
115
+
116
+ def __exit__(
117
+ self,
118
+ exc_type: type[BaseException] | None,
119
+ exc: BaseException | None,
120
+ tb: TracebackType | None,
121
+ ) -> None:
122
+ self.disconnect()