biatoolkit 1.2.0__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.
biatoolkit/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .util import BiaUtil
2
+ from .basic_client import BiaClient
3
+
4
+ __all__ = ["BiaUtil", "BiaClient"]
@@ -0,0 +1,141 @@
1
+ """
2
+ biatoolkit.basic_client
3
+
4
+ Este módulo contém o BiaClient, um cliente HTTP assíncrono para comunicação
5
+ com servidores MCP (Model Context Protocol).
6
+
7
+ Responsabilidades:
8
+ - Abrir e gerenciar conexões HTTP streamable com servidores MCP.
9
+ - Inicializar sessões MCP.
10
+ - Encapsular chamadas comuns (listar tools, executar tool).
11
+
12
+ O objetivo é esconder os detalhes de sessão, streams e inicialização,
13
+ expondo uma API simples para quem consome a biblioteca.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any, Awaitable, Callable, Optional, Dict
19
+
20
+ from mcp import ClientSession
21
+ from mcp.client.streamable_http import streamablehttp_client
22
+
23
+ from .settings import BiaToolkitSettings
24
+
25
+
26
+ class BiaClient:
27
+ """
28
+ Cliente básico para interação com servidores MCP.
29
+
30
+ Esta classe encapsula toda a complexidade envolvida em:
31
+ - Abrir conexões HTTP streamable
32
+ - Criar e inicializar ClientSession
33
+ - Executar chamadas MCP
34
+
35
+ Exemplos de uso:
36
+ client = BiaClient("http://localhost:8000")
37
+ tools = await client.list_tools()
38
+ result = await client.call_tool("minha_tool", {"x": 1})
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ url: str = "http://0.0.0.0:8000/mcp",
44
+ headers: Optional[Dict[str, str]] = None,
45
+ settings: Optional[BiaToolkitSettings] = None,
46
+ ):
47
+ """
48
+ Inicializa o cliente MCP.
49
+
50
+ Args:
51
+ url:
52
+ URL base do servidor MCP.
53
+ - Se não terminar com '/mcp', o sufixo será adicionado automaticamente.
54
+ headers:
55
+ Headers HTTP opcionais enviados em todas as requisições.
56
+ Normalmente usados para enviar contexto (runtime, autenticação, etc.).
57
+ settings:
58
+ Configurações opcionais da biblioteca (timeout, etc.).
59
+ Se None, carrega defaults e overrides via variáveis de ambiente.
60
+ """
61
+ # Carrega configurações globais (timeout, etc.)
62
+ self.settings = settings or BiaToolkitSettings.from_env()
63
+
64
+ # Garante que a URL termine com '/mcp'
65
+ suffix = "/mcp"
66
+ self.url = url if url.endswith(suffix) else f"{url}{suffix}"
67
+
68
+ # Headers HTTP que serão enviados para o servidor MCP
69
+ self.headers = headers
70
+
71
+ async def _with_session(self, fn: Callable[[ClientSession], Awaitable[Any]]) -> Any:
72
+ """
73
+ Executa uma função dentro de uma sessão MCP já inicializada.
74
+
75
+ Este método centraliza todo o boilerplate necessário para:
76
+ - Abrir a conexão HTTP streamable
77
+ - Criar a ClientSession
78
+ - Chamar session.initialize()
79
+ - Garantir fechamento correto dos recursos
80
+
81
+ Ele recebe uma função (callback) que recebe a ClientSession e
82
+ executa a lógica específica (listar tools, chamar tool, etc.).
83
+
84
+ Args:
85
+ fn: Função assíncrona que recebe uma ClientSession.
86
+
87
+ Returns:
88
+ O valor retornado pela função fn.
89
+ """
90
+ async with streamablehttp_client(
91
+ self.url,
92
+ self.headers,
93
+ # Timeout configurável via settings
94
+ timeout=self.settings.client_timeout_seconds,
95
+ # Mantém o servidor ativo mesmo após fechar streams
96
+ terminate_on_close=False,
97
+ ) as (read_stream, write_stream, _):
98
+
99
+ # Cria a sessão MCP usando os streams
100
+ async with ClientSession(read_stream, write_stream) as session:
101
+ # Inicialização obrigatória do protocolo MCP
102
+ await session.initialize()
103
+
104
+ # Executa a lógica específica passada pelo caller
105
+ return await fn(session)
106
+
107
+ async def list_tools(self) -> dict:
108
+ """
109
+ Lista todas as ferramentas (tools) disponíveis no servidor MCP.
110
+
111
+ Returns:
112
+ dict: Estrutura contendo as tools expostas pelo servidor.
113
+ """
114
+
115
+ async def _call(session: ClientSession) -> Any:
116
+ return await session.list_tools()
117
+
118
+ return await self._with_session(_call)
119
+
120
+ async def call_tool(self, tool_name: str, params: dict = None) -> dict:
121
+ """
122
+ Executa uma ferramenta específica disponível no servidor MCP.
123
+
124
+ Args:
125
+ tool_name:
126
+ Nome da tool a ser executada (exatamente como exposta pelo servidor).
127
+ params:
128
+ Parâmetros da tool, enviados como dicionário.
129
+ Pode ser None se a tool não exigir parâmetros.
130
+
131
+ Returns:
132
+ dict: Resultado da execução da tool.
133
+ """
134
+
135
+ async def _call(session: ClientSession) -> Any:
136
+ return await session.call_tool(tool_name, params)
137
+
138
+ return await self._with_session(_call)
139
+
140
+
141
+ __all__ = ["BiaClient"]
@@ -0,0 +1,433 @@
1
+ """
2
+ biatoolkit.sankhya_call
3
+
4
+ Classe utilitária para chamadas HTTP a serviços Sankhya (ou gateway),
5
+ autenticando via JSESSIONID (cookie), preferencialmente obtido do header
6
+ do runtime (AgentCore) via BiaUtil.
7
+
8
+ Objetivo:
9
+ - O dev do MCP Tool não precisa recriar autenticação, headers e session.
10
+ - Basta chamar Sankhya(...).call_json(...) ou Sankhya.Call(...) (compat).
11
+
12
+ Requisitos:
13
+ - requests
14
+ - urllib3 (vem com requests)
15
+ - boto3 (já usado no biatoolkit.util)
16
+ - mcp.server.fastmcp.FastMCP (se usar no server; opcional para modo local)
17
+
18
+ Config via env (todas opcionais):
19
+ - SANKHYA_TIMEOUT_CONNECT: default 3.05
20
+ - SANKHYA_TIMEOUT_READ: default 12
21
+ - SANKHYA_RETRIES_TOTAL: default 3
22
+ - SANKHYA_RETRY_BACKOFF: default 0.5
23
+ - SANKHYA_VERIFY_SSL: default "1"
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from dataclasses import dataclass
29
+ from typing import Any, Dict, Optional, Tuple
30
+ import os
31
+ import json
32
+ import logging
33
+
34
+ import requests
35
+ from requests.adapters import HTTPAdapter
36
+ from urllib3.util.retry import Retry
37
+
38
+ try:
39
+ # FastMCP só existe no runtime/servidor MCP. Em testes locais, pode faltar.
40
+ from mcp.server.fastmcp import FastMCP
41
+ except Exception: # pragma: no cover
42
+ FastMCP = Any # type: ignore
43
+
44
+ from .util import BiaUtil
45
+ from .settings import BiaToolkitSettings
46
+
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+
52
+
53
+ # Caminho fixo do serviço Sankhya
54
+ SERVICE_PATH = "/mge/service.sbr"
55
+
56
+ @dataclass(frozen=True)
57
+ class SankhyaSettings:
58
+ """
59
+ Configurações específicas da integração Sankhya (timeouts, retries, SSL).
60
+ Não armazena base_url nem service_path.
61
+ """
62
+ timeout_connect: float
63
+ timeout_read: float
64
+ retries_total: int
65
+ retry_backoff: float
66
+ verify_ssl: bool
67
+
68
+ @staticmethod
69
+ def from_env() -> "SankhyaSettings":
70
+ def _f(name: str, default: float) -> float:
71
+ try:
72
+ return float(os.getenv(name, str(default)))
73
+ except Exception:
74
+ return default
75
+
76
+ def _i(name: str, default: int) -> int:
77
+ try:
78
+ return int(os.getenv(name, str(default)))
79
+ except Exception:
80
+ return default
81
+
82
+ def _b(name: str, default: bool) -> bool:
83
+ raw = os.getenv(name, "1" if default else "0").strip().lower()
84
+ return raw in ("1", "true", "yes", "y", "on")
85
+
86
+ return SankhyaSettings(
87
+ timeout_connect=_f("SANKHYA_TIMEOUT_CONNECT", 3.05),
88
+ timeout_read=_f("SANKHYA_TIMEOUT_READ", 12.0),
89
+ retries_total=_i("SANKHYA_RETRIES_TOTAL", 3),
90
+ retry_backoff=_f("SANKHYA_RETRY_BACKOFF", 0.5),
91
+ verify_ssl=_b("SANKHYA_VERIFY_SSL", True),
92
+ )
93
+
94
+ def build_url(
95
+ *,
96
+ url: Optional[str] = None,
97
+ base_url: Optional[str] = None,
98
+ query: Optional[str] = None,
99
+ ) -> str:
100
+ """
101
+ Resolve URL final para chamada Sankhya.
102
+ - Se url for completa (http/https), usa como está.
103
+ - Se url for relativo, concatena base_url + url.
104
+ - Se não houver url, monta base_url + SERVICE_PATH.
105
+ - Se não houver base_url, lança erro.
106
+ """
107
+ if url and url.strip():
108
+ u = url.strip()
109
+ if u.startswith("http://") or u.startswith("https://"):
110
+ final = u
111
+ else:
112
+ if not base_url:
113
+ raise ValueError("base_url deve ser informado para url relativo.")
114
+ b = base_url.strip().rstrip("/")
115
+ if not u.startswith("/"):
116
+ u = "/" + u
117
+ final = b + u
118
+ else:
119
+ if not base_url:
120
+ raise ValueError("base_url deve ser informado para montar a URL Sankhya.")
121
+ b = base_url.strip().rstrip("/")
122
+ final = f"{b}{SERVICE_PATH}"
123
+ if query:
124
+ if "?" in final:
125
+ return f"{final}&{query.lstrip('?')}"
126
+ return f"{final}?{query.lstrip('?')}"
127
+ return final
128
+
129
+
130
+
131
+ class SankhyaHTTPError(RuntimeError):
132
+ def __init__(self, message: str, status_code: Optional[int] = None, response_text: str = ""):
133
+ super().__init__(message)
134
+ self.status_code = status_code
135
+ self.response_text = response_text
136
+
137
+
138
+ class Sankhya:
139
+ """
140
+ Classe responsável por integrar e chamar serviços da plataforma Sankhya.
141
+
142
+ Uso recomendado (em MCP server):
143
+ sk = Sankhya(mcp)
144
+ out = sk.load_view("BIA_VW_MB_RULES", "CODPROD_A = 123", fields="*")
145
+
146
+ Uso compatível com o scaffold (estático):
147
+ out = Sankhya.Call(jsessionID="...", payload={...})
148
+ # ou sem jsessionID se você passar mcp:
149
+ out = Sankhya.Call(jsessionID=None, mcp=mcp, payload={...})
150
+ """
151
+
152
+ def __init__(
153
+ self,
154
+ mcp: Optional[FastMCP] = None,
155
+ *,
156
+ toolkit_settings: Optional[BiaToolkitSettings] = None,
157
+ sankhya_settings: Optional[SankhyaSettings] = None,
158
+ default_headers: Optional[Dict[str, str]] = None,
159
+ ):
160
+ """
161
+ Inicializa a instância Sankhya.
162
+
163
+ Args:
164
+ mcp: Instância opcional do FastMCP para contexto do runtime.
165
+ toolkit_settings: Configurações globais do toolkit (opcional).
166
+ sankhya_settings: Configurações específicas da integração Sankhya (opcional).
167
+ default_headers: Headers HTTP padrão para todas as requisições (opcional).
168
+ """
169
+ self.mcp = mcp
170
+ self.toolkit_settings = toolkit_settings or BiaToolkitSettings.from_env()
171
+ self.sankhya_settings = sankhya_settings or SankhyaSettings.from_env()
172
+ self.default_headers = default_headers or {}
173
+
174
+ # Cria uma sessão HTTP com política de retries configurada
175
+ self._session = self._build_session()
176
+
177
+ # -------------------------
178
+ # Sessão HTTP + retries
179
+ # -------------------------
180
+ def _build_session(self) -> requests.Session:
181
+ """
182
+ Cria uma sessão HTTP configurada com política de retries.
183
+ Utiliza as configurações de timeout e retries do Sankhya.
184
+ """
185
+ s = requests.Session()
186
+
187
+ # Configura política de retries para falhas temporárias
188
+ retries = Retry(
189
+ total=max(0, int(self.sankhya_settings.retries_total)),
190
+ connect=max(0, int(self.sankhya_settings.retries_total)),
191
+ read=max(0, int(self.sankhya_settings.retries_total)),
192
+ backoff_factor=float(self.sankhya_settings.retry_backoff),
193
+ status_forcelist=(429, 500, 502, 503, 504),
194
+ allowed_methods=("POST", "GET"),
195
+ raise_on_status=False,
196
+ )
197
+ adapter = HTTPAdapter(max_retries=retries)
198
+ s.mount("https://", adapter)
199
+ s.mount("http://", adapter)
200
+ return s
201
+
202
+ # -------------------------
203
+ # JSESSIONID resolve
204
+ # -------------------------
205
+ def _resolve_jsessionid(self, jsessionid: Optional[str] = None) -> str:
206
+ """
207
+ Resolve o JSESSIONID a ser usado na autenticação.
208
+ Prioriza o valor explícito, depois tenta extrair do header do runtime via BiaUtil.
209
+
210
+ Args:
211
+ jsessionid: Token de sessão explícito (opcional).
212
+
213
+ Returns:
214
+ str: JSESSIONID válido.
215
+
216
+ Raises:
217
+ ValueError: Se não for possível obter o JSESSIONID.
218
+ """
219
+ # 1) parâmetro explícito
220
+ if jsessionid and str(jsessionid).strip():
221
+ return str(jsessionid).strip()
222
+
223
+ # 2) header do runtime via BiaUtil (se tiver mcp)
224
+ if self.mcp is not None:
225
+ try:
226
+ util = BiaUtil(self.mcp, self.toolkit_settings)
227
+ h = util.get_header()
228
+ if h and getattr(h, "jsessionid", None):
229
+ return str(h.jsessionid).strip()
230
+ except Exception as e:
231
+ logger.debug("Falha ao ler jsessionid do header via BiaUtil: %s", e)
232
+
233
+ raise ValueError(
234
+ "JSESSIONID ausente. Passe jsessionid explicitamente ou forneça 'mcp' para ler do header."
235
+ )
236
+
237
+
238
+ # -------------------------
239
+ # Métodos públicos
240
+ # -------------------------
241
+ def call_json(
242
+ self,
243
+ *,
244
+ payload: Optional[Dict[str, Any]] = None,
245
+ jsessionid: Optional[str] = None,
246
+ url: Optional[str] = None,
247
+ base_url: Optional[str] = None,
248
+ query: Optional[str] = None,
249
+ method: str = "POST",
250
+ extra_headers: Optional[Dict[str, str]] = None,
251
+ timeout: Optional[Tuple[float, float]] = None,
252
+ raise_for_http_error: bool = True,
253
+ ) -> Dict[str, Any]:
254
+ """
255
+ Faz uma chamada HTTP ao serviço Sankhya e retorna JSON (ou erro detalhado).
256
+
257
+ Args:
258
+ payload: Corpo JSON (para POST). Pode ser None.
259
+ jsessionid: Se None, tenta resolver do header (mcp) automaticamente.
260
+ url: URL completa (se quiser ignorar base_url/service_path).
261
+ base_url: Base URL alternativa (opcional).
262
+ query: Querystring adicional (ex: "serviceName=...&outputType=json")
263
+ method: "POST" ou "GET"
264
+ extra_headers: Headers adicionais.
265
+ timeout: (connect, read) override
266
+ raise_for_http_error: Se True, lança SankhyaHTTPError em status != 200
267
+
268
+ Returns:
269
+ dict: Resposta JSON (ou {"raw_text": "..."} se não for JSON)
270
+ """
271
+ # Resolve o token de sessão (JSESSIONID)
272
+ sid = self._resolve_jsessionid(jsessionid)
273
+
274
+ # Monta a querystring, sempre incluindo o mgeSession
275
+ query_parts = []
276
+ if query:
277
+ query_parts.append(query.lstrip("?"))
278
+ query_parts.append(f"mgeSession={sid}")
279
+ final_query = "&".join(query_parts)
280
+
281
+ # Monta a URL final da requisição
282
+ final_url = build_url(url=url, query=final_query, base_url=base_url)
283
+ print("FINAL URL:", final_url)
284
+
285
+ # Monta os headers da requisição
286
+ headers: Dict[str, str] = {
287
+ "Content-Type": "application/json",
288
+ **self.default_headers,
289
+ }
290
+ if extra_headers:
291
+ headers.update(extra_headers)
292
+
293
+ # Define o timeout da requisição
294
+ t = timeout or (self.sankhya_settings.timeout_connect, self.sankhya_settings.timeout_read)
295
+
296
+ # Realiza a chamada HTTP (GET ou POST)
297
+ if method.upper() == "GET":
298
+ resp = self._session.get(final_url, headers=headers, timeout=t, verify=self.sankhya_settings.verify_ssl)
299
+ else:
300
+ print("\n" + "="*40)
301
+ print("Sankhya POST Request:")
302
+ print(f"URL: {final_url}")
303
+ print(f"Headers: {json.dumps(headers, ensure_ascii=False, indent=2)}")
304
+ print(f"Payload: {json.dumps(payload, ensure_ascii=False, indent=2)}")
305
+ print(f"Timeout: {t}")
306
+ print(f"Verify SSL: {self.sankhya_settings.verify_ssl}")
307
+ print("="*40 + "\n")
308
+ resp = self._session.post(final_url, headers=headers, json=payload, timeout=t, verify=self.sankhya_settings.verify_ssl)
309
+
310
+ # Trata erros HTTP
311
+ if resp.status_code != 200:
312
+ msg = f"Falha HTTP {resp.status_code} ao chamar Sankhya/gateway."
313
+ if raise_for_http_error:
314
+ raise SankhyaHTTPError(msg, status_code=resp.status_code, response_text=resp.text)
315
+ return {"error": True, "status_code": resp.status_code, "message": msg, "response_text": resp.text}
316
+
317
+ # Tenta decodificar JSON; se não der, retorna texto puro
318
+ try:
319
+ return resp.json()
320
+ except Exception:
321
+ return {"raw_text": resp.text}
322
+
323
+ def load_view(
324
+ self,
325
+ view_name: str,
326
+ where_sql: str,
327
+ *,
328
+ fields: str = "*",
329
+ jsessionid: Optional[str] = None,
330
+ url: Optional[str] = None,
331
+ base_url: Optional[str] = None,
332
+ output_type: str = "json",
333
+ extra_headers: Optional[Dict[str, str]] = None,
334
+ ) -> Dict[str, Any]:
335
+ """
336
+ Helper para o CRUDServiceProvider.loadView (mge/service.sbr).
337
+ Monta o payload e a querystring automaticamente para facilitar consultas a views Sankhya.
338
+
339
+ Args:
340
+ view_name: Nome da view no Sankhya.
341
+ where_sql: Cláusula WHERE (string).
342
+ fields: Campos a retornar (string).
343
+ jsessionid: Se None, pega do header (mcp).
344
+ url: URL completa opcional (override).
345
+ base_url: Base URL alternativa (opcional).
346
+ output_type: "json" (default).
347
+ extra_headers: Headers adicionais.
348
+
349
+ Returns:
350
+ dict: Resposta da consulta à view.
351
+ """
352
+ # Monta a querystring para o serviço
353
+ query = f"serviceName=CRUDServiceProvider.loadView&outputType={output_type}"
354
+
355
+ # Monta o corpo do payload conforme esperado pelo serviço
356
+ body = {
357
+ "serviceName": "CRUDServiceProvider.loadView",
358
+ "requestBody": {
359
+ "query": {
360
+ "viewName": view_name,
361
+ "where": {"$": where_sql},
362
+ "fields": {"field": {"$": fields}},
363
+ }
364
+ },
365
+ }
366
+
367
+
368
+ util = BiaUtil(self.mcp)
369
+ # Se base_url não foi fornecido, tenta obter do util.get_header().current_host
370
+ if not base_url:
371
+ base_url = util.get_header().current_host
372
+ if not base_url:
373
+ raise ValueError("base_url não definida e não foi possível obter current_host do header.")
374
+
375
+ return self.call_json(
376
+ payload=body,
377
+ jsessionid=jsessionid,
378
+ url=url,
379
+ base_url=base_url,
380
+ query=query,
381
+ method="POST",
382
+ extra_headers=extra_headers,
383
+ )
384
+
385
+ # -------------------------
386
+ # Compat com scaffold
387
+ # -------------------------
388
+ @staticmethod
389
+ def Call(
390
+ jsessionID: Optional[str] = None,
391
+ *,
392
+ payload: Optional[Dict[str, Any]] = None,
393
+ mcp: Optional[FastMCP] = None,
394
+ url: Optional[str] = None,
395
+ base_url: Optional[str] = None,
396
+ query: Optional[str] = None,
397
+ method: str = "POST",
398
+ extra_headers: Optional[Dict[str, str]] = None,
399
+ ) -> Optional[dict]:
400
+ """
401
+ Método estático compatível com o scaffold original.
402
+ Permite chamada rápida ao serviço Sankhya sem instanciar manualmente a classe.
403
+
404
+ Exemplos de uso:
405
+ Sankhya.Call(jsessionID="...", payload={...}, query="serviceName=...&outputType=json")
406
+ Sankhya.Call(jsessionID=None, mcp=mcp, payload={...}, query="...")
407
+
408
+ Args:
409
+ jsessionID: Token de sessão JSESSIONID (opcional).
410
+ payload: Corpo da requisição (dict).
411
+ mcp: Instância opcional do FastMCP para contexto do runtime.
412
+ url: URL completa (opcional).
413
+ base_url: Base URL alternativa (opcional).
414
+ query: Querystring adicional (opcional).
415
+ method: "POST" ou "GET".
416
+ extra_headers: Headers adicionais.
417
+
418
+ Returns:
419
+ dict: Resposta do serviço ou lança erro se HTTP != 200.
420
+ """
421
+ sk = Sankhya(mcp=mcp)
422
+ return sk.call_json(
423
+ payload=payload,
424
+ jsessionid=jsessionID,
425
+ url=url,
426
+ base_url=base_url,
427
+ query=query,
428
+ method=method,
429
+ extra_headers=extra_headers,
430
+ )
431
+
432
+
433
+ __all__ = ["Sankhya", "SankhyaSettings", "SankhyaHTTPError"]
@@ -0,0 +1 @@
1
+ # biatoolkit/schema/__init__.py
@@ -0,0 +1,19 @@
1
+
2
+ class Header:
3
+ def __init__(self,
4
+ current_host: str = None,
5
+ user_email: str = None,
6
+ jwt_token: str = None,
7
+ jsessionid: str = None,
8
+ organization_id: int = None,
9
+ codparc: int = None,
10
+ iam_user_id: int = None,
11
+ gateway_token: str = None):
12
+ self.current_host = current_host
13
+ self.user_email = user_email
14
+ self.jwt_token = jwt_token
15
+ self.jsessionid = jsessionid
16
+ self.organization_id = organization_id
17
+ self.codparc = codparc
18
+ self.iam_user_id = iam_user_id
19
+ self.gateway_token = gateway_token