grid402 2.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.
- grid402/__init__.py +50 -0
- grid402/agentkit.py +186 -0
- grid402/client.py +156 -0
- grid402/exceptions.py +34 -0
- grid402/models.py +119 -0
- grid402/py.typed +1 -0
- grid402-2.2.0.dist-info/METADATA +165 -0
- grid402-2.2.0.dist-info/RECORD +11 -0
- grid402-2.2.0.dist-info/WHEEL +5 -0
- grid402-2.2.0.dist-info/licenses/LICENSE +22 -0
- grid402-2.2.0.dist-info/top_level.txt +1 -0
grid402/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Grid402 Python SDK - Biblioteca oficial para a Internet Agente e Micropagamentos HTTP 402.
|
|
3
|
+
|
|
4
|
+
Suporte para Python 3.9+, Coinbase Developer Platform (CDP) AgentKit,
|
|
5
|
+
LangChain, CrewAI e modo Sandbox gratuito.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from grid402.agentkit import Grid402ActionProvider
|
|
9
|
+
from grid402.client import DEFAULT_ENDPOINT, Grid402Client
|
|
10
|
+
from grid402.exceptions import Grid402Error, Grid402PaymentRequiredError
|
|
11
|
+
from grid402.models import (
|
|
12
|
+
AsnInput,
|
|
13
|
+
CepInput,
|
|
14
|
+
CnpjInput,
|
|
15
|
+
CryptoInput,
|
|
16
|
+
DnsInput,
|
|
17
|
+
EconomicIndicators,
|
|
18
|
+
HolidayItem,
|
|
19
|
+
HolidaysInput,
|
|
20
|
+
HolidaysResponse,
|
|
21
|
+
IndicatorsResponse,
|
|
22
|
+
SettlementInfo,
|
|
23
|
+
SslInput,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__version__ = "2.2.0"
|
|
27
|
+
__author__ = "Grid402 Team"
|
|
28
|
+
__license__ = "MIT"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Grid402Client",
|
|
32
|
+
"Grid402ActionProvider",
|
|
33
|
+
"Grid402Error",
|
|
34
|
+
"Grid402PaymentRequiredError",
|
|
35
|
+
"CnpjInput",
|
|
36
|
+
"CepInput",
|
|
37
|
+
"DnsInput",
|
|
38
|
+
"SslInput",
|
|
39
|
+
"AsnInput",
|
|
40
|
+
"CryptoInput",
|
|
41
|
+
"HolidaysInput",
|
|
42
|
+
"EconomicIndicators",
|
|
43
|
+
"IndicatorsResponse",
|
|
44
|
+
"HolidayItem",
|
|
45
|
+
"HolidaysResponse",
|
|
46
|
+
"SettlementInfo",
|
|
47
|
+
"DEFAULT_ENDPOINT",
|
|
48
|
+
"__version__",
|
|
49
|
+
]
|
|
50
|
+
|
grid402/agentkit.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Grid402 Action Provider para Coinbase AgentKit, LangChain e CrewAI.
|
|
3
|
+
|
|
4
|
+
Permite que agentes autonomos com carteiras CDP (Coinbase Developer Platform)
|
|
5
|
+
na rede Base consultem dados cadastrais e utilitarios de rede com auto-liquidacao x402.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
import json
|
|
10
|
+
import ssl
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
|
|
15
|
+
from grid402.client import DEFAULT_ENDPOINT
|
|
16
|
+
from grid402.models import AsnInput, CepInput, CnpjInput, CryptoInput, DnsInput, SslInput
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
GRID402_ORACLE_WALLET = "0xa31c31069c43ac1c0075a93c325b0dcd7744356c"
|
|
20
|
+
BASE_USDC_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Grid402ActionProvider:
|
|
24
|
+
"""
|
|
25
|
+
Provedor de acoes do Grid402 compativel com Coinbase AgentKit, LangChain e CrewAI.
|
|
26
|
+
|
|
27
|
+
Gerencia o ciclo de vida completo de micropagamentos x402 de forma autonoma,
|
|
28
|
+
detectando desafios HTTP 402, efetuando a transferencia de USDC on-chain
|
|
29
|
+
na Base e reenviando a requisicao com o comprovante criptografico.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, base_url: str = DEFAULT_ENDPOINT, sandbox: bool = False):
|
|
33
|
+
"""
|
|
34
|
+
Inicializa o provedor de acoes Grid402.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
base_url: URL base da API do Grid402.
|
|
38
|
+
sandbox: Se True, opera em modo Sandbox gratuito sem consumir USDC real.
|
|
39
|
+
"""
|
|
40
|
+
self.base_url = base_url.rstrip("/")
|
|
41
|
+
self.sandbox = sandbox
|
|
42
|
+
|
|
43
|
+
def _get_ssl_context(self) -> ssl.SSLContext:
|
|
44
|
+
ctx = ssl.create_default_context()
|
|
45
|
+
if "hostingersite.com" in self.base_url:
|
|
46
|
+
ctx.check_hostname = False
|
|
47
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
48
|
+
return ctx
|
|
49
|
+
|
|
50
|
+
def _execute_with_settlement(
|
|
51
|
+
self,
|
|
52
|
+
path: str,
|
|
53
|
+
params: Optional[Dict[str, Any]] = None,
|
|
54
|
+
wallet_provider: Any = None,
|
|
55
|
+
) -> Dict[str, Any]:
|
|
56
|
+
"""
|
|
57
|
+
Executa a requisicao HTTP com auto-liquidacao de desafio x402.
|
|
58
|
+
|
|
59
|
+
Se o endpoint retornar HTTP 402 e um wallet_provider estiver disponivel,
|
|
60
|
+
executa a transferencia de micropagamento em USDC na Base e repete a chamada.
|
|
61
|
+
"""
|
|
62
|
+
filtered_params = {k: v for k, v in (params or {}).items() if v is not None}
|
|
63
|
+
query_string = urllib.parse.urlencode(filtered_params)
|
|
64
|
+
|
|
65
|
+
url = f"{self.base_url}{path}"
|
|
66
|
+
if query_string:
|
|
67
|
+
url += f"?{query_string}"
|
|
68
|
+
|
|
69
|
+
headers = {
|
|
70
|
+
"Accept": "application/json",
|
|
71
|
+
"User-Agent": "Grid402-CoinbaseAgentKit-Python/1.0",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if self.sandbox:
|
|
75
|
+
headers["X-Payment"] = "demo"
|
|
76
|
+
|
|
77
|
+
req = urllib.request.Request(url, headers=headers)
|
|
78
|
+
ctx = self._get_ssl_context()
|
|
79
|
+
|
|
80
|
+
# 1. Primeira tentativa de requisicao
|
|
81
|
+
try:
|
|
82
|
+
with urllib.request.urlopen(req, context=ctx, timeout=10) as response:
|
|
83
|
+
return json.loads(response.read().decode("utf-8"))
|
|
84
|
+
except urllib.error.HTTPError as exc:
|
|
85
|
+
raw_body = exc.read().decode("utf-8")
|
|
86
|
+
try:
|
|
87
|
+
challenge = json.loads(raw_body)
|
|
88
|
+
except Exception:
|
|
89
|
+
challenge = {"error": raw_body, "status_code": exc.code}
|
|
90
|
+
|
|
91
|
+
# 2. Se retornou HTTP 402 Payment Required
|
|
92
|
+
if exc.code == 402:
|
|
93
|
+
# Se temos wallet_provider e nao estamos no sandbox, auto-liquida on-chain
|
|
94
|
+
if wallet_provider is not None and not self.sandbox:
|
|
95
|
+
price_usdc = challenge.get("price_usdc", "0.0005")
|
|
96
|
+
recipient = challenge.get("recipient_wallet", GRID402_ORACLE_WALLET)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
# Executa transferencia de USDC na Base usando a carteira CDP
|
|
100
|
+
transfer_result = wallet_provider.transfer(
|
|
101
|
+
to=recipient,
|
|
102
|
+
amount=price_usdc,
|
|
103
|
+
asset_id="usdc",
|
|
104
|
+
)
|
|
105
|
+
tx_hash = getattr(transfer_result, "transaction_hash", str(transfer_result))
|
|
106
|
+
|
|
107
|
+
# Reenvia com cabecalho X-Payment
|
|
108
|
+
headers["X-Payment"] = tx_hash
|
|
109
|
+
retry_req = urllib.request.Request(url, headers=headers)
|
|
110
|
+
with urllib.request.urlopen(retry_req, context=ctx, timeout=10) as retry_res:
|
|
111
|
+
data = json.loads(retry_res.read().decode("utf-8"))
|
|
112
|
+
data["_x402_settlement"] = {
|
|
113
|
+
"settled": True,
|
|
114
|
+
"tx_hash": tx_hash,
|
|
115
|
+
"price_usdc": price_usdc,
|
|
116
|
+
"network": "Base",
|
|
117
|
+
"token": "USDC",
|
|
118
|
+
}
|
|
119
|
+
return data
|
|
120
|
+
except Exception as pay_err:
|
|
121
|
+
challenge["payment_error"] = str(pay_err)
|
|
122
|
+
challenge["payment_required"] = True
|
|
123
|
+
return challenge
|
|
124
|
+
|
|
125
|
+
challenge["payment_required"] = True
|
|
126
|
+
return challenge
|
|
127
|
+
raise
|
|
128
|
+
|
|
129
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
130
|
+
"""Resumo operacional consolidado e disponibilidade do hub Grid402 (gratuito)."""
|
|
131
|
+
return self._execute_with_settlement("/status")
|
|
132
|
+
|
|
133
|
+
def query_forex(self, wallet_provider: Any = None) -> Dict[str, Any]:
|
|
134
|
+
"""Consulta cotacoes fiduciarias de USD, EUR e GBP para BRL em tempo real."""
|
|
135
|
+
return self._execute_with_settlement("/api/v1/forex", {}, wallet_provider)
|
|
136
|
+
|
|
137
|
+
def query_crypto(
|
|
138
|
+
self, symbol: Optional[str] = None, wallet_provider: Any = None
|
|
139
|
+
) -> Dict[str, Any]:
|
|
140
|
+
"""Consulta precos de mercado spot de criptoativos em USD e BRL."""
|
|
141
|
+
params = {"symbol": symbol} if symbol else {}
|
|
142
|
+
return self._execute_with_settlement("/api/v1/crypto", params, wallet_provider)
|
|
143
|
+
|
|
144
|
+
def query_dns(self, domain: str, wallet_provider: Any = None) -> Dict[str, Any]:
|
|
145
|
+
"""Executa resolucao autoritativa de registros DNS para qualquer dominio."""
|
|
146
|
+
return self._execute_with_settlement("/api/v1/dns", {"domain": domain}, wallet_provider)
|
|
147
|
+
|
|
148
|
+
def query_ssl(self, host: str, wallet_provider: Any = None) -> Dict[str, Any]:
|
|
149
|
+
"""Inspeciona e audita certificado TLS e SSL na porta 443 de um host."""
|
|
150
|
+
return self._execute_with_settlement("/api/v1/ssl", {"host": host}, wallet_provider)
|
|
151
|
+
|
|
152
|
+
def query_asn(self, ip: Optional[str] = None, wallet_provider: Any = None) -> Dict[str, Any]:
|
|
153
|
+
"""Consulta inteligencia de rede BGP, numero de ASN e organizacao titular."""
|
|
154
|
+
params = {"ip": ip} if ip else {}
|
|
155
|
+
return self._execute_with_settlement("/api/v1/asn", params, wallet_provider)
|
|
156
|
+
|
|
157
|
+
def query_cep(self, code: str, wallet_provider: Any = None) -> Dict[str, Any]:
|
|
158
|
+
"""Consulta endereco postal brasileiro e codigo oficial IBGE por CEP."""
|
|
159
|
+
return self._execute_with_settlement("/api/v1/cep", {"code": code}, wallet_provider)
|
|
160
|
+
|
|
161
|
+
def query_cnpj(self, cnpj: str, wallet_provider: Any = None) -> Dict[str, Any]:
|
|
162
|
+
"""Consulta dados cadastrais de Pessoa Juridica na Receita Federal."""
|
|
163
|
+
return self._execute_with_settlement("/api/v1/cnpj", {"cnpj": cnpj}, wallet_provider)
|
|
164
|
+
|
|
165
|
+
def query_indicators(self, wallet_provider: Any = None) -> Dict[str, Any]:
|
|
166
|
+
"""Consulta taxas oficiais Selic meta e diaria, CDI anual e diario, IPCA 12m e mensal."""
|
|
167
|
+
return self._execute_with_settlement("/api/v1/indicators", {}, wallet_provider)
|
|
168
|
+
|
|
169
|
+
def query_holidays(
|
|
170
|
+
self, year: Optional[int] = None, wallet_provider: Any = None
|
|
171
|
+
) -> Dict[str, Any]:
|
|
172
|
+
"""Consulta o calendario oficial de feriados nacionais e dias sem expediente bancario."""
|
|
173
|
+
params = {"year": year} if year is not None else {}
|
|
174
|
+
return self._execute_with_settlement("/api/v1/holidays", params, wallet_provider)
|
|
175
|
+
|
|
176
|
+
# Aliases no padrao snake_case das ferramentas MCP
|
|
177
|
+
get_cnpj = query_cnpj
|
|
178
|
+
get_cep = query_cep
|
|
179
|
+
get_forex = query_forex
|
|
180
|
+
get_crypto = query_crypto
|
|
181
|
+
get_dns = query_dns
|
|
182
|
+
get_ssl = query_ssl
|
|
183
|
+
get_asn = query_asn
|
|
184
|
+
get_indicators = query_indicators
|
|
185
|
+
get_holidays = query_holidays
|
|
186
|
+
|
grid402/client.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cliente HTTP oficial para a API de Micropagamentos Grid402.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
import json
|
|
7
|
+
import ssl
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
|
|
12
|
+
from grid402.exceptions import Grid402Error, Grid402PaymentRequiredError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_ENDPOINT = "https://grid402.com.br"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Grid402Client:
|
|
19
|
+
"""
|
|
20
|
+
Cliente Python para consumo das ferramentas públicas do Grid402.
|
|
21
|
+
|
|
22
|
+
Suporta modo de producao na Base Mainnet, modo Sandbox gratuito para testes
|
|
23
|
+
e tratamento estruturado de desafios HTTP 402.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
base_url: str = DEFAULT_ENDPOINT,
|
|
29
|
+
sandbox: bool = False,
|
|
30
|
+
payment_tx_hash: Optional[str] = None,
|
|
31
|
+
timeout: int = 10,
|
|
32
|
+
raise_for_payment: bool = False,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Inicializa uma nova instancia do Grid402Client.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
base_url: Endereco base da API do Grid402.
|
|
39
|
+
sandbox: Se True, ativa o modo Sandbox gratuito para homologacao sem custo.
|
|
40
|
+
payment_tx_hash: Hash de transacao USDC opcional padrao para as requisicoes.
|
|
41
|
+
timeout: Tempo limite em segundos para cada requisicao HTTP.
|
|
42
|
+
raise_for_payment: Se True, lanca Grid402PaymentRequiredError ao receber HTTP 402.
|
|
43
|
+
Se False, retorna o dicionario com o desafio e dados parciais.
|
|
44
|
+
"""
|
|
45
|
+
self.base_url = base_url.rstrip("/")
|
|
46
|
+
self.sandbox = sandbox
|
|
47
|
+
self.payment_tx_hash = payment_tx_hash
|
|
48
|
+
self.timeout = timeout
|
|
49
|
+
self.raise_for_payment = raise_for_payment
|
|
50
|
+
|
|
51
|
+
def _get_ssl_context(self) -> ssl.SSLContext:
|
|
52
|
+
ctx = ssl.create_default_context()
|
|
53
|
+
if "hostingersite.com" in self.base_url:
|
|
54
|
+
ctx.check_hostname = False
|
|
55
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
56
|
+
return ctx
|
|
57
|
+
|
|
58
|
+
def _request(
|
|
59
|
+
self,
|
|
60
|
+
path: str,
|
|
61
|
+
params: Optional[Dict[str, Any]] = None,
|
|
62
|
+
payment_tx_hash: Optional[str] = None,
|
|
63
|
+
) -> Dict[str, Any]:
|
|
64
|
+
"""Executa a requisicao HTTP para a API Grid402."""
|
|
65
|
+
filtered_params = {k: v for k, v in (params or {}).items() if v is not None}
|
|
66
|
+
query_string = urllib.parse.urlencode(filtered_params)
|
|
67
|
+
|
|
68
|
+
url = f"{self.base_url}{path}"
|
|
69
|
+
if query_string:
|
|
70
|
+
url += f"?{query_string}"
|
|
71
|
+
|
|
72
|
+
headers = {
|
|
73
|
+
"Accept": "application/json",
|
|
74
|
+
"User-Agent": "Grid402-Python-SDK/2.2",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
active_tx_hash = payment_tx_hash or self.payment_tx_hash
|
|
78
|
+
if active_tx_hash:
|
|
79
|
+
headers["X-Payment"] = active_tx_hash
|
|
80
|
+
elif self.sandbox:
|
|
81
|
+
headers["X-Payment"] = "demo"
|
|
82
|
+
|
|
83
|
+
req = urllib.request.Request(url, headers=headers)
|
|
84
|
+
ctx = self._get_ssl_context()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
with urllib.request.urlopen(req, context=ctx, timeout=self.timeout) as response:
|
|
88
|
+
return json.loads(response.read().decode("utf-8"))
|
|
89
|
+
except urllib.error.HTTPError as exc:
|
|
90
|
+
raw_body = exc.read().decode("utf-8")
|
|
91
|
+
try:
|
|
92
|
+
error_data = json.loads(raw_body)
|
|
93
|
+
except Exception:
|
|
94
|
+
error_data = {"error": raw_body, "status_code": exc.code}
|
|
95
|
+
|
|
96
|
+
if exc.code == 402:
|
|
97
|
+
if self.raise_for_payment:
|
|
98
|
+
raise Grid402PaymentRequiredError(error_data) from exc
|
|
99
|
+
error_data["payment_required"] = True
|
|
100
|
+
return error_data
|
|
101
|
+
|
|
102
|
+
raise Grid402Error(
|
|
103
|
+
f"Erro na API Grid402 (HTTP {exc.code}): {error_data.get('error') or raw_body}"
|
|
104
|
+
) from exc
|
|
105
|
+
except urllib.error.URLError as exc:
|
|
106
|
+
raise Grid402Error(f"Falha de conexao com {self.base_url}: {exc.reason}") from exc
|
|
107
|
+
|
|
108
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
109
|
+
"""Consulta o resumo e status operacional consolidado do Grid402 (gratuito)."""
|
|
110
|
+
return self._request("/status")
|
|
111
|
+
|
|
112
|
+
def get_forex(self, payment_tx_hash: Optional[str] = None) -> Dict[str, Any]:
|
|
113
|
+
"""Consulta cotacoes fiduciarias oficiais em tempo real de USD, EUR e GBP para BRL."""
|
|
114
|
+
return self._request("/api/v1/forex", payment_tx_hash=payment_tx_hash)
|
|
115
|
+
|
|
116
|
+
def get_crypto(
|
|
117
|
+
self, symbol: Optional[str] = None, payment_tx_hash: Optional[str] = None
|
|
118
|
+
) -> Dict[str, Any]:
|
|
119
|
+
"""Consulta precos de mercado spot de criptoativos em tempo real."""
|
|
120
|
+
params = {"symbol": symbol} if symbol else {}
|
|
121
|
+
return self._request("/api/v1/crypto", params=params, payment_tx_hash=payment_tx_hash)
|
|
122
|
+
|
|
123
|
+
def get_dns(self, domain: str, payment_tx_hash: Optional[str] = None) -> Dict[str, Any]:
|
|
124
|
+
"""Executa resolucao DNS autoritativa de registros A, AAAA, MX, TXT, NS e CNAME."""
|
|
125
|
+
return self._request("/api/v1/dns", params={"domain": domain}, payment_tx_hash=payment_tx_hash)
|
|
126
|
+
|
|
127
|
+
def get_ssl(self, host: str, payment_tx_hash: Optional[str] = None) -> Dict[str, Any]:
|
|
128
|
+
"""Inspeciona e valida o certificado TLS e SSL na porta 443 de um host remoto."""
|
|
129
|
+
return self._request("/api/v1/ssl", params={"host": host}, payment_tx_hash=payment_tx_hash)
|
|
130
|
+
|
|
131
|
+
def get_asn(
|
|
132
|
+
self, ip: Optional[str] = None, payment_tx_hash: Optional[str] = None
|
|
133
|
+
) -> Dict[str, Any]:
|
|
134
|
+
"""Consulta inteligencia de rede BGP, identificando numero de ASN e organizacao."""
|
|
135
|
+
params = {"ip": ip} if ip else {}
|
|
136
|
+
return self._request("/api/v1/asn", params=params, payment_tx_hash=payment_tx_hash)
|
|
137
|
+
|
|
138
|
+
def get_cep(self, code: str, payment_tx_hash: Optional[str] = None) -> Dict[str, Any]:
|
|
139
|
+
"""Consulta a base publica de enderecamento postal brasileiro e codigo IBGE por CEP."""
|
|
140
|
+
return self._request("/api/v1/cep", params={"code": code}, payment_tx_hash=payment_tx_hash)
|
|
141
|
+
|
|
142
|
+
def get_cnpj(self, cnpj: str, payment_tx_hash: Optional[str] = None) -> Dict[str, Any]:
|
|
143
|
+
"""Consulta dados cadastrais publicos de Pessoa Juridica na Receita Federal."""
|
|
144
|
+
return self._request("/api/v1/cnpj", params={"cnpj": cnpj}, payment_tx_hash=payment_tx_hash)
|
|
145
|
+
|
|
146
|
+
def get_indicators(self, payment_tx_hash: Optional[str] = None) -> Dict[str, Any]:
|
|
147
|
+
"""Consulta indicadores economicos oficiais (Selic, CDI e IPCA) do Banco Central e IBGE."""
|
|
148
|
+
return self._request("/api/v1/indicators", payment_tx_hash=payment_tx_hash)
|
|
149
|
+
|
|
150
|
+
def get_holidays(
|
|
151
|
+
self, year: Optional[int] = None, payment_tx_hash: Optional[str] = None
|
|
152
|
+
) -> Dict[str, Any]:
|
|
153
|
+
"""Consulta calendario oficial de feriados nacionais e bancarios (ANBIMA e Febraban)."""
|
|
154
|
+
params = {"year": year} if year is not None else {}
|
|
155
|
+
return self._request("/api/v1/holidays", params=params, payment_tx_hash=payment_tx_hash)
|
|
156
|
+
|
grid402/exceptions.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Excecoes oficiais do SDK Grid402.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Grid402Error(Exception):
|
|
9
|
+
"""Excecao base para erros do SDK Grid402."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Grid402PaymentRequiredError(Grid402Error):
|
|
14
|
+
"""
|
|
15
|
+
Excecao levantada quando um endpoint do Grid402 retorna HTTP 402 Payment Required.
|
|
16
|
+
|
|
17
|
+
Contem todos os metadados do desafio de pagamento x402 e pre-visualizacao parcial.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, challenge: Dict[str, Any], message: Optional[str] = None):
|
|
21
|
+
self.challenge = challenge
|
|
22
|
+
self.price_usdc: str = challenge.get("price_usdc", "0.0005")
|
|
23
|
+
self.recipient: str = challenge.get("recipient_wallet", "")
|
|
24
|
+
self.network: str = challenge.get("network", "Base")
|
|
25
|
+
self.token: str = challenge.get("token", "USDC")
|
|
26
|
+
self.preview_data: Optional[Dict[str, Any]] = challenge.get("preview_data")
|
|
27
|
+
self.payment_details: Optional[Dict[str, Any]] = challenge.get("payment_details")
|
|
28
|
+
|
|
29
|
+
default_msg = (
|
|
30
|
+
f"Pagamento obrigatorio de {self.price_usdc} {self.token} na rede {self.network} "
|
|
31
|
+
f"para a carteira {self.recipient}."
|
|
32
|
+
)
|
|
33
|
+
super().__init__(message or challenge.get("message") or default_msg)
|
|
34
|
+
|
grid402/models.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modelos tipados Pydantic para entradas e saidas do Grid402.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CnpjInput(BaseModel):
|
|
10
|
+
"""Parametros para consulta cadastral de Pessoa Juridica na Receita Federal."""
|
|
11
|
+
cnpj: str = Field(
|
|
12
|
+
...,
|
|
13
|
+
description="Numero do CNPJ com 14 digitos numericos, com ou sem mascara de pontuacao.",
|
|
14
|
+
examples=["00.000.000/0001-91", "00000000000191"],
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CepInput(BaseModel):
|
|
19
|
+
"""Parametros para consulta de endereco postal e codigo IBGE por CEP."""
|
|
20
|
+
code: str = Field(
|
|
21
|
+
...,
|
|
22
|
+
description="Codigo de CEP brasileiro com 8 digitos numericos, com ou sem hifen.",
|
|
23
|
+
examples=["01310-100", "01310100"],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DnsInput(BaseModel):
|
|
28
|
+
"""Parametros para resolucao DNS autoritativa."""
|
|
29
|
+
domain: str = Field(
|
|
30
|
+
...,
|
|
31
|
+
description="Nome de dominio da internet para resolucao de registros A, AAAA, MX, TXT, NS e CNAME.",
|
|
32
|
+
examples=["base.org", "ethereum.org"],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SslInput(BaseModel):
|
|
37
|
+
"""Parametros para inspecao e auditoria de certificado TLS e SSL."""
|
|
38
|
+
host: str = Field(
|
|
39
|
+
...,
|
|
40
|
+
description="Hostname ou dominio para inspecao de certificado TLS na porta 443.",
|
|
41
|
+
examples=["base.org", "google.com"],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AsnInput(BaseModel):
|
|
46
|
+
"""Parametros para consulta de BGP e numero ASN."""
|
|
47
|
+
ip: Optional[str] = Field(
|
|
48
|
+
None,
|
|
49
|
+
description="Endereco IPv4 ou IPv6 a consultar. Se omitido, inspeciona o IP publico requisitante.",
|
|
50
|
+
examples=["1.1.1.1", "8.8.8.8"],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CryptoInput(BaseModel):
|
|
55
|
+
"""Parametros opcionais para cotacao spot de criptoativos."""
|
|
56
|
+
symbol: Optional[str] = Field(
|
|
57
|
+
None,
|
|
58
|
+
description="Simbolo opcional do criptoativo (ex: BTC, ETH, SOL, USDC, BASE).",
|
|
59
|
+
examples=["BTC", "ETH"],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class HolidaysInput(BaseModel):
|
|
64
|
+
"""Parametros opcionais para consulta de feriados nacionais e bancarios."""
|
|
65
|
+
year: Optional[int] = Field(
|
|
66
|
+
None,
|
|
67
|
+
description="Ano calendario de 4 digitos entre 1900 e 2100. Se omitido, utiliza o ano corrente.",
|
|
68
|
+
ge=1900,
|
|
69
|
+
le=2100,
|
|
70
|
+
examples=[2026],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class EconomicIndicators(BaseModel):
|
|
75
|
+
selic_target: float = Field(..., description="Taxa Selic meta anual em percentual.")
|
|
76
|
+
selic_daily: float = Field(..., description="Taxa Selic acumulada diaria anualizada.")
|
|
77
|
+
cdi_annual: float = Field(..., description="Taxa CDI anualizada em percentual.")
|
|
78
|
+
cdi_daily: float = Field(..., description="Taxa CDI diaria efetiva em percentual.")
|
|
79
|
+
ipca_12m: float = Field(..., description="IPCA acumulado nos ultimos 12 meses.")
|
|
80
|
+
ipca_monthly: float = Field(..., description="IPCA do ultimo mes fechado.")
|
|
81
|
+
reference_date: str = Field(..., description="Data de referencia dos indicadores diarios.")
|
|
82
|
+
ipca_reference_month: str = Field(..., description="Mes de referencia da divulgacao do IPCA.")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class IndicatorsResponse(BaseModel):
|
|
86
|
+
status: str = Field(..., description="Status da consulta.")
|
|
87
|
+
service: str = Field("indicators", description="Nome do micro-servico.")
|
|
88
|
+
timestamp: str = Field(..., description="Data e hora ISO 8601 da resposta.")
|
|
89
|
+
indicators: EconomicIndicators = Field(..., description="Conjunto de indicadores economicos.")
|
|
90
|
+
source: str = Field(..., description="Fontes oficiais primarias.")
|
|
91
|
+
cached: Optional[bool] = Field(None, description="Indica se os dados foram servidos do cache.")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class HolidayItem(BaseModel):
|
|
95
|
+
date: str = Field(..., description="Data do feriado no formato AAAA-MM-DD.")
|
|
96
|
+
name: str = Field(..., description="Nome oficial do feriado ou celebracao.")
|
|
97
|
+
type: str = Field(..., description="Tipo do feriado: national, banking ou religious.")
|
|
98
|
+
day_of_week: str = Field(..., description="Dia da semana correspondente em portugues.")
|
|
99
|
+
is_banking_holiday: bool = Field(..., description="Indica se nao ha expediente bancario ao publico.")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class HolidaysResponse(BaseModel):
|
|
103
|
+
status: str = Field(..., description="Status da consulta.")
|
|
104
|
+
service: str = Field("holidays", description="Nome do micro-servico.")
|
|
105
|
+
year: int = Field(..., description="Ano calendario consultado.")
|
|
106
|
+
total_holidays: int = Field(..., description="Contagem total de feriados cadastrados no ano.")
|
|
107
|
+
holidays: list[HolidayItem] = Field(..., description="Lista de feriados ordenada cronologicamente.")
|
|
108
|
+
source: str = Field(..., description="Fontes oficiais primarias.")
|
|
109
|
+
cached: Optional[bool] = Field(None, description="Indica se os dados foram servidos do cache.")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class SettlementInfo(BaseModel):
|
|
113
|
+
"""Informacoes de liquidacao on-chain de micropagamento x402."""
|
|
114
|
+
settled: bool = Field(True, description="Indica se o pagamento foi liquidado com sucesso.")
|
|
115
|
+
tx_hash: str = Field(..., description="Hash da transacao confirmada na rede Base.")
|
|
116
|
+
price_usdc: str = Field(..., description="Valor pago em USDC.")
|
|
117
|
+
network: str = Field("Base", description="Rede blockchain da liquidacao.")
|
|
118
|
+
token: str = Field("USDC", description="Token utilizado no pagamento.")
|
|
119
|
+
|
grid402/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: grid402
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: SDK Python oficial do Grid402: Micropagamentos HTTP 402 na Base para Agentes Autonomos e IA
|
|
5
|
+
Author-email: Grid402 Team <contato@grid402.com.br>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://grid402.com.br
|
|
8
|
+
Project-URL: Documentation, https://grid402.com.br/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/thiagobg90/Grid402
|
|
10
|
+
Project-URL: Issues, https://github.com/thiagobg90/Grid402/issues
|
|
11
|
+
Keywords: x402,http-402,micropayments,base,coinbase-agentkit,agentic-web,mcp,ai-agents
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build; extra == "dev"
|
|
29
|
+
Requires-Dist: twine; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Grid402 - SDK Python Oficial
|
|
33
|
+
|
|
34
|
+
Biblioteca Python oficial para consumo dos servicos do ecossistema **Grid402** e liquidacao autonoma de micropagamentos sob o protocolo HTTP 402 na rede Base.
|
|
35
|
+
|
|
36
|
+
Projetada especificamente para agentes autonomos de inteligencia artificial, frameworks como Coinbase AgentKit, LangChain, CrewAI e desenvolvedores que buscam APIs abertas sem contas, cadastros ou cartoes de credito.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Recursos Principais
|
|
41
|
+
|
|
42
|
+
- **10 Ferramentas Integradas:** Indicadores macroeconomicos (Selic, CDI, IPCA), feriados nacionais e bancarios ANBIMA, CNPJ da Receita Federal, CEP postal e IBGE, cotacoes Forex e Cripto spot, resolucao DNS, auditoria TLS e SSL, analise ASN e BGP e status do ecossistema.
|
|
43
|
+
- **Zero Cadastro e Zero Friccao:** Sem necessidade de chaves de API estaticas ou contratos mensais.
|
|
44
|
+
- **Modo Sandbox Gratuito:** Teste e homologue localmente sem custos informando `sandbox=True`.
|
|
45
|
+
- **Integracao Nativa com IA:** Compativel com Coinbase Developer Platform (CDP) AgentKit, LangChain e CrewAI.
|
|
46
|
+
- **Validacao Estrita Pydantic:** Schemas tipados e compativeis com PEP 561 (`py.typed`).
|
|
47
|
+
- **Arquitetura Resiliente:** Construido com a biblioteca padrao do Python para maxima leveza e portabilidade.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Instalacao
|
|
52
|
+
|
|
53
|
+
Instale o pacote diretamente do PyPI:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install grid402
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Inicio Rapido
|
|
62
|
+
|
|
63
|
+
### 1. Consultas em Modo Sandbox (Sem Custo)
|
|
64
|
+
|
|
65
|
+
Ideal para testes locais, ambientes de integracao continua e desenvolvimento:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from grid402 import Grid402Client
|
|
69
|
+
|
|
70
|
+
# Inicializa o cliente apontando para o ambiente de testes gratuito
|
|
71
|
+
client = Grid402Client(sandbox=True)
|
|
72
|
+
|
|
73
|
+
# Consulta cadastral de Pessoa Juridica na Receita Federal
|
|
74
|
+
cnpj_data = client.get_cnpj("00.000.000/0001-91")
|
|
75
|
+
print("Razao Social:", cnpj_data.get("razao_social"))
|
|
76
|
+
|
|
77
|
+
# Resolucao de endereco postal e codigo IBGE
|
|
78
|
+
cep_data = client.get_cep("01310-100")
|
|
79
|
+
print("Logradouro:", cep_data.get("logradouro"), "-", cep_data.get("localidade"))
|
|
80
|
+
|
|
81
|
+
# Cotacoes fiduciarias oficiais
|
|
82
|
+
forex_data = client.get_forex()
|
|
83
|
+
print("USD para BRL:", forex_data.get("USD", {}).get("bid"))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### 2. Tratamento Estruturado de HTTP 402
|
|
89
|
+
|
|
90
|
+
Quando chamado sem comprovante de pagamento, o Grid402 retorna uma resposta estruturada contendo o desafio com pre-visualizacao parcial dos dados (teaser data):
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from grid402 import Grid402Client, Grid402PaymentRequiredError
|
|
94
|
+
|
|
95
|
+
# Cliente com raise_for_payment ativado
|
|
96
|
+
client = Grid402Client(raise_for_payment=True)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
dados = client.get_cnpj("00000000000191")
|
|
100
|
+
except Grid402PaymentRequiredError as erro:
|
|
101
|
+
print("Desafio recebido:")
|
|
102
|
+
print("Valor exigido:", erro.price_usdc, erro.token)
|
|
103
|
+
print("Carteira recebedora:", erro.recipient)
|
|
104
|
+
print("Rede:", erro.network)
|
|
105
|
+
print("Dados parciais (amostra gratuita):", erro.preview_data)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### 3. Utilizacao com Coinbase AgentKit e Auto-Liquidacao
|
|
111
|
+
|
|
112
|
+
Agentes autonomos equipados com carteiras CDP na rede Base podem liquidar chamadas M2M (Machine-to-Machine) de forma transparente:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from grid402 import Grid402ActionProvider
|
|
116
|
+
|
|
117
|
+
# Provedor compativel com Coinbase AgentKit
|
|
118
|
+
provider = Grid402ActionProvider()
|
|
119
|
+
|
|
120
|
+
# Ao fornecer a instancia de carteira CDP do agente (wallet_provider),
|
|
121
|
+
# o provedor detecta o 402, transfere o USDC e repassa os dados completos
|
|
122
|
+
resultado = provider.query_cnpj("00000000000191", wallet_provider=meu_wallet_provider)
|
|
123
|
+
print("Dados completos:", resultado)
|
|
124
|
+
print("Comprovante on-chain:", resultado.get("_x402_settlement"))
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### 4. Schemas Tipados Pydantic
|
|
130
|
+
|
|
131
|
+
Para fluxos de trabalho tipados e validacao antecipada:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from grid402 import CnpjInput, CepInput, DnsInput, SslInput, AsnInput, IndicatorsInput, HolidaysInput
|
|
135
|
+
|
|
136
|
+
# Validacao de entrada antes do envio
|
|
137
|
+
payload_cnpj = CnpjInput(cnpj="00.000.000/0001-91")
|
|
138
|
+
payload_cep = CepInput(code="01310100")
|
|
139
|
+
payload_dns = DnsInput(domain="base.org")
|
|
140
|
+
payload_holidays = HolidaysInput(year=2026)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Catalogo de Ferramentas
|
|
146
|
+
|
|
147
|
+
| Metodo | Descricao | Preco Base (USDC) |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `get_summary()` | Resumo operacional consolidado de saude | Gratuito |
|
|
150
|
+
| `get_forex()` | Cotacoes cambiais oficiais de USD, EUR e GBP para BRL | 0.0005 |
|
|
151
|
+
| `get_crypto(symbol)` | Precos spot de BTC, ETH, SOL e USDC | 0.0005 |
|
|
152
|
+
| `get_dns(domain)` | Resolucao DNS com registros A, AAAA, MX, TXT, NS e CNAME | 0.0005 |
|
|
153
|
+
| `get_ssl(host)` | Inspecao e auditoria de certificado TLS e SSL na porta 443 | 0.0005 |
|
|
154
|
+
| `get_asn(ip)` | Inteligencia de roteamento BGP, bloco alocado e ASN | 0.0005 |
|
|
155
|
+
| `get_cep(code)` | Consulta de logradouro postal brasileiro e codigo IBGE | 0.0005 |
|
|
156
|
+
| `get_cnpj(cnpj)` | Dados cadastrais completos de PJ na Receita Federal | 0.0020 |
|
|
157
|
+
| `get_indicators()` | Indicadores macroeconomicos oficiais (Selic meta e diaria, CDI anual e IPCA) | 0.0005 |
|
|
158
|
+
| `get_holidays(year)` | Calendario oficial de feriados nacionais e bancarios ANBIMA | 0.0005 |
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Licenca
|
|
163
|
+
|
|
164
|
+
Distribuido sob os termos da licenca MIT. Consulte o arquivo `LICENSE` para detalhes.
|
|
165
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
grid402/__init__.py,sha256=p1HZHRS9O0uzEnkDmT3AVupBNuNvrz27AIv7QUfPw4s,1115
|
|
2
|
+
grid402/agentkit.py,sha256=omtsTx_HKb9fl8hAUj-FzIP5H1arYT_xJTQb9WTXsrU,8126
|
|
3
|
+
grid402/client.py,sha256=LB5ywWrCw_-uqsAMPr8t_dici3M4CvcBRV2_YSHYJrI,6652
|
|
4
|
+
grid402/exceptions.py,sha256=VLcNcFvMOj4LiacXDaZHgGaFtKSk2QJ_TZ1hwB6WG70,1224
|
|
5
|
+
grid402/models.py,sha256=BqS4IVxc1ya1TDleqoDbul2wNkRXJd_FheNYKVHHYIg,5092
|
|
6
|
+
grid402/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
7
|
+
grid402-2.2.0.dist-info/licenses/LICENSE,sha256=AygoXt4UY-kO63c6vOPbjBDt_5pr7-fPv8qbaP6kvTk,1078
|
|
8
|
+
grid402-2.2.0.dist-info/METADATA,sha256=c6w1sM9_TL9toCZgp91_Jn36pAXNRPy0h9WDC3quaQc,6453
|
|
9
|
+
grid402-2.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
10
|
+
grid402-2.2.0.dist-info/top_level.txt,sha256=_7wfocziRC4JW_lvgWdBsKMB-1Uf3gBOiSa3fFSpGEk,8
|
|
11
|
+
grid402-2.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Grid402 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.
|
|
22
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
grid402
|