biatoolkit 1.1.2__tar.gz → 1.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: biatoolkit
3
- Version: 1.1.2
3
+ Version: 1.2.0
4
4
  Summary: Biblioteca para desenvolvedores que utilizam o BiaAgentBuilder
5
5
  Author: Bia Platform Team
6
6
  Author-email: data.platform@sankhya.com.br
@@ -348,4 +348,145 @@ O método `get_parameter(parameter_name: str)` busca o parâmetro informado em d
348
348
  - Um arquivo `.env` para testes locais.
349
349
  - No cofre de segredos do Bia Agent Builder para usar em ambiente produtivo.
350
350
 
351
- Assim, o **MESMO CÓDIGO** do Servidor MCP pode ser executado em ambos os ambientes.
351
+
352
+ ---
353
+
354
+ ## Validação e Coerção de Dados
355
+
356
+ O Bia Toolkit oferece funções utilitárias para normalização, sanitização e coerção de dados, facilitando o consumo seguro de entradas vindas de APIs, headers, payloads e integrações legadas.
357
+
358
+
359
+ ### Validação de Dados (`biatoolkit.validation.validation`)
360
+
361
+ Utilize a classe **BiaValidation** para acessar as funções de validação de forma padronizada:
362
+
363
+ - **BiaValidation.parse_int_list(value, dedupe=True, keep_order=True)**
364
+ - Normaliza um valor arbitrário em uma lista de inteiros.
365
+ - Suporta: None, int, str (extrai números), list/tuple (recursivo).
366
+ - Ignora valores inválidos.
367
+ - Deduplica e mantém ordem por padrão.
368
+ - Exemplo:
369
+ ```python
370
+ from biatoolkit.validation.validation import BiaValidation
371
+ BiaValidation.parse_int_list("SKU=300231 x2") # [300231, 2]
372
+ BiaValidation.parse_int_list([1, 2, 1, 3], dedupe=False) # [1, 2, 1, 3]
373
+ ```
374
+
375
+ - **BiaValidation.sanitize_like(value, max_len=80, upper=True)**
376
+ - Sanitiza texto para uso seguro em filtros LIKE/search SQL.
377
+ - Remove caracteres fora da allowlist, limita tamanho, escapa aspas simples, %, _ e normaliza espaços.
378
+ - Converte para maiúsculas por padrão.
379
+ - Exemplo:
380
+ ```python
381
+ from biatoolkit.validation.validation import BiaValidation
382
+ BiaValidation.sanitize_like("O'Reilly") # "O''REILLY"
383
+ BiaValidation.sanitize_like("100%_OK") # "100\\%\\_OK"
384
+ ```
385
+
386
+
387
+ ### Coerção de Dados (`biatoolkit.validation.coercion`)
388
+
389
+ Utilize a classe **BiaCoercion** para acessar as funções de coerção de forma padronizada:
390
+
391
+ - **BiaCoercion.ensure_list(value)**
392
+ - Garante que o valor seja retornado como lista.
393
+ - None → [], list → list, tuple/set → list, dict → [dict], outro → [valor].
394
+ - Exemplo:
395
+ ```python
396
+ from biatoolkit.validation.coercion import BiaCoercion
397
+ BiaCoercion.ensure_list(None) # []
398
+ BiaCoercion.ensure_list({"a": 1}) # [{"a": 1}]
399
+ BiaCoercion.ensure_list(5) # [5]
400
+ ```
401
+
402
+ - **BiaCoercion.unwrap_dollar_value(value)**
403
+ - Desembrulha valores no formato {"$": ...} (comum em integrações legadas).
404
+ - Exemplo:
405
+ ```python
406
+ from biatoolkit.validation.coercion import BiaCoercion
407
+ BiaCoercion.unwrap_dollar_value({"$": 123}) # 123
408
+ BiaCoercion.unwrap_dollar_value("abc") # "abc"
409
+ ```
410
+
411
+ - **BiaCoercion.to_int(value, default=0)**
412
+ - Converte valor para int de forma segura.
413
+ - None, "", bool, NaN/inf → default.
414
+ - Aceita strings numéricas, floats (trunca), etc.
415
+ - Exemplo:
416
+ ```python
417
+ from biatoolkit.validation.coercion import BiaCoercion
418
+ BiaCoercion.to_int(" 42 ") # 42
419
+ BiaCoercion.to_int(None, default=-1) # -1
420
+ ```
421
+
422
+ - **BiaCoercion.to_float(value, default=0.0)**
423
+ - Converte valor para float de forma segura.
424
+ - None, "", bool, NaN/inf → default.
425
+ - Aceita strings com vírgula decimal.
426
+ - Exemplo:
427
+ ```python
428
+ from biatoolkit.validation.coercion import BiaCoercion
429
+ BiaCoercion.to_float("12,34") # 12.34
430
+ BiaCoercion.to_float("abc", default=-1.0) # -1.0
431
+ ```
432
+
433
+
434
+ Essas funções são úteis para garantir robustez e previsibilidade ao tratar dados vindos de múltiplas fontes, reduzindo erros e if/else espalhados pelo código.
435
+
436
+ ---
437
+
438
+ ## Integração com Sankhya (`biatoolkit.sankhya_call`)
439
+
440
+
441
+ O Bia Toolkit oferece integração pronta para consumo de serviços HTTP da plataforma Sankhya (ou gateway), com autenticação via JSESSIONID e configuração de timeouts/retries via variáveis de ambiente.
442
+
443
+ ### Objetivo
444
+
445
+ - Facilitar chamadas autenticadas a serviços Sankhya sem que o desenvolvedor precise lidar com autenticação, headers ou gerenciamento de sessão HTTP.
446
+ - Fornecer métodos prontos para chamadas genéricas (`call_json`) e para consultas a views (`load_view`).
447
+ - Permitir uso estático compatível com scaffolds legados via `Sankhya.Call(...)`.
448
+
449
+ ### Configuração
450
+
451
+ - O caminho do serviço (`/mge/service.sbr`) é fixo no toolkit.
452
+ - O parâmetro `base_url` é obrigatório e pode ser passado explicitamente ou extraído automaticamente do header do runtime (`current_host` via `BiaUtil`).
453
+ - Não existe mais configuração via variável de ambiente para base_url ou service_path.
454
+ - As variáveis de ambiente opcionais são apenas para timeout, retries e SSL:
455
+ - `SANKHYA_TIMEOUT_CONNECT`: timeout de conexão (default: 3.05)
456
+ - `SANKHYA_TIMEOUT_READ`: timeout de leitura (default: 12)
457
+ - `SANKHYA_RETRIES_TOTAL`: número de tentativas em falha (default: 3)
458
+ - `SANKHYA_RETRY_BACKOFF`: backoff entre tentativas (default: 0.5)
459
+ - `SANKHYA_VERIFY_SSL`: se valida SSL (default: "1")
460
+
461
+ ### Principais classes e métodos
462
+
463
+ - **SankhyaSettings**: Dataclass de configuração de timeouts/retries/SSL. Use `SankhyaSettings.from_env()` para obter as configurações do ambiente.
464
+ - **Sankhya**: Classe principal de integração. Permite instanciar com contexto MCP (`FastMCP`) ou usar métodos estáticos.
465
+ - `call_json(...)`: Realiza chamada HTTP autenticada, retorna JSON.
466
+ - `load_view(...)`: Helper para consultas a views Sankhya (`CRUDServiceProvider.loadView`).
467
+ - `Call(...)`: Método estático compatível com scaffolds legados.
468
+ - **SankhyaHTTPError**: Exceção lançada em caso de erro HTTP (status != 200), contendo status_code e response_text.
469
+
470
+ ### Exemplo de uso (instanciado)
471
+
472
+ ```python
473
+ from biatoolkit.sankhya_call import Sankhya
474
+ sk = Sankhya(mcp=mcp)
475
+ result = sk.load_view("BIA_VW_MB_RULES", "CODPROD_A = 123", fields="*")
476
+ # O base_url será extraído automaticamente do header do runtime (current_host)
477
+ ```
478
+
479
+ ### Exemplo de uso (estático)
480
+
481
+ ```python
482
+ from biatoolkit.sankhya_call import Sankhya
483
+ result = Sankhya.Call(jsessionID="...", payload={...}, base_url="https://meu.sankhya.com.br", query="serviceName=...&outputType=json")
484
+ ```
485
+
486
+ ### Observações
487
+
488
+ - O JSESSIONID pode ser passado explicitamente ou extraído automaticamente do header do runtime (se rodando em MCP Server).
489
+ - O método `load_view` facilita consultas a views Sankhya, montando o payload e a querystring automaticamente, e resolve o base_url do header se não for passado.
490
+ - O método `call_json` permite chamadas genéricas a qualquer serviço Sankhya, com controle total sobre headers, método HTTP, payload e querystring, mas exige base_url explícito se não estiver em contexto MCP.
491
+
492
+ ---
@@ -381,4 +381,82 @@ fornecido em ambiente produtivo pelo Bia Agent Builder.
381
381
  - Um arquivo `.env` para testes locais.
382
382
  - No cofre de segredos do Bia Agent Builder para usar em ambiente produtivo.
383
383
 
384
- Assim, o **MESMO CÓDIGO** do Servidor MCP pode ser executado em ambos os ambientes.
384
+
385
+ ## **Validação e Coerção de Dados**
386
+
387
+ O Bia Toolkit oferece funções utilitárias para normalização, sanitização e coerção de dados, facilitando o consumo seguro de entradas vindas de APIs, headers, payloads e integrações legadas.
388
+
389
+
390
+ ### **Validação de Dados** (`biatoolkit.validation.validation`)
391
+
392
+ Utilize a classe **BiaValidation** para acessar as funções de validação de forma padronizada:
393
+
394
+ - **BiaValidation.parse_int_list(value, dedupe=True, keep_order=True)**
395
+ - Normaliza um valor arbitrário em uma lista de inteiros.
396
+ - Suporta: None, int, str (extrai números), list/tuple (recursivo).
397
+ - Ignora valores inválidos.
398
+ - Deduplica e mantém ordem por padrão.
399
+ - Exemplo:
400
+ ```python
401
+ from biatoolkit.validation.validation import BiaValidation
402
+ BiaValidation.parse_int_list("SKU=300231 x2") # [300231, 2]
403
+ BiaValidation.parse_int_list([1, 2, 1, 3], dedupe=False) # [1, 2, 1, 3]
404
+ ```
405
+
406
+ - **BiaValidation.sanitize_like(value, max_len=80, upper=True)**
407
+ - Sanitiza texto para uso seguro em filtros LIKE/search SQL.
408
+ - Remove caracteres fora da allowlist, limita tamanho, escapa aspas simples, %, _ e normaliza espaços.
409
+ - Converte para maiúsculas por padrão.
410
+ - Exemplo:
411
+ ```python
412
+ from biatoolkit.validation.validation import BiaValidation
413
+ BiaValidation.sanitize_like("O'Reilly") # "O''REILLY"
414
+ BiaValidation.sanitize_like("100%_OK") # "100\\%\\_OK"
415
+ ```
416
+
417
+ ### **Coerção de Dados** (`biatoolkit.validation.coercion`)
418
+
419
+ Utilize a classe **BiaCoercion** para acessar as funções de coerção de forma padronizada:
420
+
421
+ - **BiaCoercion.ensure_list(value)**
422
+ - Garante que o valor seja retornado como lista.
423
+ - None → [], list → list, tuple/set → list, dict → [dict], outro → [valor].
424
+ - Exemplo:
425
+ ```python
426
+ from biatoolkit.validation.coercion import BiaCoercion
427
+ BiaCoercion.ensure_list(None) # []
428
+ BiaCoercion.ensure_list({"a": 1}) # [{"a": 1}]
429
+ BiaCoercion.ensure_list(5) # [5]
430
+ ```
431
+
432
+ - **BiaCoercion.unwrap_dollar_value(value)**
433
+ - Desembrulha valores no formato {"$": ...} (comum em integrações legadas).
434
+ - Exemplo:
435
+ ```python
436
+ from biatoolkit.validation.coercion import BiaCoercion
437
+ BiaCoercion.unwrap_dollar_value({"$": 123}) # 123
438
+ BiaCoercion.unwrap_dollar_value("abc") # "abc"
439
+ ```
440
+
441
+ - **BiaCoercion.to_int(value, default=0)**
442
+ - Converte valor para int de forma segura.
443
+ - None, "", bool, NaN/inf → default.
444
+ - Aceita strings numéricas, floats (trunca), etc.
445
+ - Exemplo:
446
+ ```python
447
+ from biatoolkit.validation.coercion import BiaCoercion
448
+ BiaCoercion.to_int(" 42 ") # 42
449
+ BiaCoercion.to_int(None, default=-1) # -1
450
+ ```
451
+
452
+ - **BiaCoercion.to_float(value, default=0.0)**
453
+ - Converte valor para float de forma segura.
454
+ - None, "", bool, NaN/inf → default.
455
+ - Aceita strings com vírgula decimal.
456
+ - Exemplo:
457
+ ```python
458
+ to_float("12,34") # 12.34
459
+ to_float("abc", default=-1.0) # -1.0
460
+ ```
461
+
462
+ Essas funções são úteis para garantir robustez e previsibilidade ao tratar dados vindos de múltiplas fontes, reduzindo erros e if/else espalhados pelo código.
@@ -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,39 @@
1
+ import sys
2
+ import os
3
+ #sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/..'))
4
+ from biatoolkit.sankhya_call import Sankhya, SankhyaHTTPError
5
+ import json
6
+
7
+ # Opcional: não depende mais disso, mas pode deixar.
8
+ os.environ.setdefault("SANKHYA_SERVICE_PATH", "/mge/service.sbr")
9
+
10
+ # ⚠️ COLE AQUI seu JSESSIONID em UMA LINHA (sem ENTER no meio)
11
+ REAL = "CrK0qgoMYcnfZsURMi7iarpFKWSVqS-qO3HWMiu8.sankhya-w-78598f67f-n7mcp"
12
+
13
+ if not REAL or REAL == "COLE_SEU_JSESSIONID_AQUI":
14
+ raise SystemExit("Cole seu JSESSIONID na variável REAL (uma linha só) e rode: python -m biatoolkit.test_sankhya")
15
+
16
+ # Base URL do ambiente correto (o novo)
17
+ BASE_URL = "https://ecossistema2.sankhyacloud.com.br"
18
+
19
+ sk = Sankhya()
20
+
21
+ try:
22
+ out = sk.load_view(
23
+ "BIA_VW_MB_RULES",
24
+ "1=1",
25
+ fields="COMPLDESC_A,COMPLDESC_B,CONFIDENCE_A_B,LIFT_A_B,CNT_AB,DISPONIVEL_B,DTGERACAO",
26
+ jsessionid=REAL,
27
+ base_url=BASE_URL, # <-- NOVO: injeta a base url aqui
28
+ # url="/mge/service.sbr", # opcional: se quiser passar só o path
29
+ # url="https://ecossistema2.sankhyacloud.com.br/mge/service.sbr", # opcional: URL completa
30
+ )
31
+ print("OK! Resposta (top-level keys):", list(out.keys()))
32
+ print(out)
33
+ if isinstance(out, dict):
34
+ print(json.dumps(out, indent=2, ensure_ascii=False))
35
+
36
+ except SankhyaHTTPError as e:
37
+ print("HTTP ERROR:", e.status_code)
38
+ print("BODY (primeiros 500 chars):")
39
+ print((e.response_text or "")[:500])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: biatoolkit
3
- Version: 1.1.2
3
+ Version: 1.2.0
4
4
  Summary: Biblioteca para desenvolvedores que utilizam o BiaAgentBuilder
5
5
  Author: Bia Platform Team
6
6
  Author-email: data.platform@sankhya.com.br
@@ -348,4 +348,145 @@ O método `get_parameter(parameter_name: str)` busca o parâmetro informado em d
348
348
  - Um arquivo `.env` para testes locais.
349
349
  - No cofre de segredos do Bia Agent Builder para usar em ambiente produtivo.
350
350
 
351
- Assim, o **MESMO CÓDIGO** do Servidor MCP pode ser executado em ambos os ambientes.
351
+
352
+ ---
353
+
354
+ ## Validação e Coerção de Dados
355
+
356
+ O Bia Toolkit oferece funções utilitárias para normalização, sanitização e coerção de dados, facilitando o consumo seguro de entradas vindas de APIs, headers, payloads e integrações legadas.
357
+
358
+
359
+ ### Validação de Dados (`biatoolkit.validation.validation`)
360
+
361
+ Utilize a classe **BiaValidation** para acessar as funções de validação de forma padronizada:
362
+
363
+ - **BiaValidation.parse_int_list(value, dedupe=True, keep_order=True)**
364
+ - Normaliza um valor arbitrário em uma lista de inteiros.
365
+ - Suporta: None, int, str (extrai números), list/tuple (recursivo).
366
+ - Ignora valores inválidos.
367
+ - Deduplica e mantém ordem por padrão.
368
+ - Exemplo:
369
+ ```python
370
+ from biatoolkit.validation.validation import BiaValidation
371
+ BiaValidation.parse_int_list("SKU=300231 x2") # [300231, 2]
372
+ BiaValidation.parse_int_list([1, 2, 1, 3], dedupe=False) # [1, 2, 1, 3]
373
+ ```
374
+
375
+ - **BiaValidation.sanitize_like(value, max_len=80, upper=True)**
376
+ - Sanitiza texto para uso seguro em filtros LIKE/search SQL.
377
+ - Remove caracteres fora da allowlist, limita tamanho, escapa aspas simples, %, _ e normaliza espaços.
378
+ - Converte para maiúsculas por padrão.
379
+ - Exemplo:
380
+ ```python
381
+ from biatoolkit.validation.validation import BiaValidation
382
+ BiaValidation.sanitize_like("O'Reilly") # "O''REILLY"
383
+ BiaValidation.sanitize_like("100%_OK") # "100\\%\\_OK"
384
+ ```
385
+
386
+
387
+ ### Coerção de Dados (`biatoolkit.validation.coercion`)
388
+
389
+ Utilize a classe **BiaCoercion** para acessar as funções de coerção de forma padronizada:
390
+
391
+ - **BiaCoercion.ensure_list(value)**
392
+ - Garante que o valor seja retornado como lista.
393
+ - None → [], list → list, tuple/set → list, dict → [dict], outro → [valor].
394
+ - Exemplo:
395
+ ```python
396
+ from biatoolkit.validation.coercion import BiaCoercion
397
+ BiaCoercion.ensure_list(None) # []
398
+ BiaCoercion.ensure_list({"a": 1}) # [{"a": 1}]
399
+ BiaCoercion.ensure_list(5) # [5]
400
+ ```
401
+
402
+ - **BiaCoercion.unwrap_dollar_value(value)**
403
+ - Desembrulha valores no formato {"$": ...} (comum em integrações legadas).
404
+ - Exemplo:
405
+ ```python
406
+ from biatoolkit.validation.coercion import BiaCoercion
407
+ BiaCoercion.unwrap_dollar_value({"$": 123}) # 123
408
+ BiaCoercion.unwrap_dollar_value("abc") # "abc"
409
+ ```
410
+
411
+ - **BiaCoercion.to_int(value, default=0)**
412
+ - Converte valor para int de forma segura.
413
+ - None, "", bool, NaN/inf → default.
414
+ - Aceita strings numéricas, floats (trunca), etc.
415
+ - Exemplo:
416
+ ```python
417
+ from biatoolkit.validation.coercion import BiaCoercion
418
+ BiaCoercion.to_int(" 42 ") # 42
419
+ BiaCoercion.to_int(None, default=-1) # -1
420
+ ```
421
+
422
+ - **BiaCoercion.to_float(value, default=0.0)**
423
+ - Converte valor para float de forma segura.
424
+ - None, "", bool, NaN/inf → default.
425
+ - Aceita strings com vírgula decimal.
426
+ - Exemplo:
427
+ ```python
428
+ from biatoolkit.validation.coercion import BiaCoercion
429
+ BiaCoercion.to_float("12,34") # 12.34
430
+ BiaCoercion.to_float("abc", default=-1.0) # -1.0
431
+ ```
432
+
433
+
434
+ Essas funções são úteis para garantir robustez e previsibilidade ao tratar dados vindos de múltiplas fontes, reduzindo erros e if/else espalhados pelo código.
435
+
436
+ ---
437
+
438
+ ## Integração com Sankhya (`biatoolkit.sankhya_call`)
439
+
440
+
441
+ O Bia Toolkit oferece integração pronta para consumo de serviços HTTP da plataforma Sankhya (ou gateway), com autenticação via JSESSIONID e configuração de timeouts/retries via variáveis de ambiente.
442
+
443
+ ### Objetivo
444
+
445
+ - Facilitar chamadas autenticadas a serviços Sankhya sem que o desenvolvedor precise lidar com autenticação, headers ou gerenciamento de sessão HTTP.
446
+ - Fornecer métodos prontos para chamadas genéricas (`call_json`) e para consultas a views (`load_view`).
447
+ - Permitir uso estático compatível com scaffolds legados via `Sankhya.Call(...)`.
448
+
449
+ ### Configuração
450
+
451
+ - O caminho do serviço (`/mge/service.sbr`) é fixo no toolkit.
452
+ - O parâmetro `base_url` é obrigatório e pode ser passado explicitamente ou extraído automaticamente do header do runtime (`current_host` via `BiaUtil`).
453
+ - Não existe mais configuração via variável de ambiente para base_url ou service_path.
454
+ - As variáveis de ambiente opcionais são apenas para timeout, retries e SSL:
455
+ - `SANKHYA_TIMEOUT_CONNECT`: timeout de conexão (default: 3.05)
456
+ - `SANKHYA_TIMEOUT_READ`: timeout de leitura (default: 12)
457
+ - `SANKHYA_RETRIES_TOTAL`: número de tentativas em falha (default: 3)
458
+ - `SANKHYA_RETRY_BACKOFF`: backoff entre tentativas (default: 0.5)
459
+ - `SANKHYA_VERIFY_SSL`: se valida SSL (default: "1")
460
+
461
+ ### Principais classes e métodos
462
+
463
+ - **SankhyaSettings**: Dataclass de configuração de timeouts/retries/SSL. Use `SankhyaSettings.from_env()` para obter as configurações do ambiente.
464
+ - **Sankhya**: Classe principal de integração. Permite instanciar com contexto MCP (`FastMCP`) ou usar métodos estáticos.
465
+ - `call_json(...)`: Realiza chamada HTTP autenticada, retorna JSON.
466
+ - `load_view(...)`: Helper para consultas a views Sankhya (`CRUDServiceProvider.loadView`).
467
+ - `Call(...)`: Método estático compatível com scaffolds legados.
468
+ - **SankhyaHTTPError**: Exceção lançada em caso de erro HTTP (status != 200), contendo status_code e response_text.
469
+
470
+ ### Exemplo de uso (instanciado)
471
+
472
+ ```python
473
+ from biatoolkit.sankhya_call import Sankhya
474
+ sk = Sankhya(mcp=mcp)
475
+ result = sk.load_view("BIA_VW_MB_RULES", "CODPROD_A = 123", fields="*")
476
+ # O base_url será extraído automaticamente do header do runtime (current_host)
477
+ ```
478
+
479
+ ### Exemplo de uso (estático)
480
+
481
+ ```python
482
+ from biatoolkit.sankhya_call import Sankhya
483
+ result = Sankhya.Call(jsessionID="...", payload={...}, base_url="https://meu.sankhya.com.br", query="serviceName=...&outputType=json")
484
+ ```
485
+
486
+ ### Observações
487
+
488
+ - O JSESSIONID pode ser passado explicitamente ou extraído automaticamente do header do runtime (se rodando em MCP Server).
489
+ - O método `load_view` facilita consultas a views Sankhya, montando o payload e a querystring automaticamente, e resolve o base_url do header se não for passado.
490
+ - O método `call_json` permite chamadas genéricas a qualquer serviço Sankhya, com controle total sobre headers, método HTTP, payload e querystring, mas exige base_url explícito se não estiver em contexto MCP.
491
+
492
+ ---
@@ -2,7 +2,9 @@ README.md
2
2
  setup.py
3
3
  biatoolkit/__init__.py
4
4
  biatoolkit/basic_client.py
5
+ biatoolkit/sankhya_call.py
5
6
  biatoolkit/settings.py
7
+ biatoolkit/test_sankhya.py
6
8
  biatoolkit/util.py
7
9
  biatoolkit.egg-info/PKG-INFO
8
10
  biatoolkit.egg-info/SOURCES.txt
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='biatoolkit',
5
- version='1.1.2',
5
+ version='1.2.0',
6
6
  packages=find_packages(),
7
7
  install_requires=[],
8
8
  author='Bia Platform Team',
File without changes