Documenta como hacer requests RPC de prueba contra Tryton con
sabatron-tryton-rpc-client:
- .opencode/skills/tryton-rpc-probe/SKILL.md: comando base con docker compose
dev (--no-deps), credenciales desde .env, como revelar estados HTTP
(401/403/429) que la libreria enmascara, convencion de agrupacion de campos
con punto ('template.' -> 'categories.') y sondas de ejemplo
- .opencode/agents/tryton-rpc-probe.md: subagente que obliga a cargar el skill
- AGENTS.md: referencia al skill para todos los agents del proyecto
5.9 KiB
Probes RPC a Tryton con sabatron-tryton-rpc-client
Guía para ejecutar requests RPC de prueba (probes) contra Tryton dentro de este
proyecto, usando el paquete sabatron-tryton-rpc-client y el contenedor de
desarrollo. Sirve para verificar campos, modelos, credenciales y respuestas
antes de planear o implementar cambios de integración (TDD: confirmar empíricamente
antes de asumir).
Reglas obligatorias
- Nunca usar curl. Solo el paquete python
sabatron-tryton-rpc-client(el RPC de Tryton enmascara/muestra estados HTTP distintos que confunden a curl). - No hardcodear credenciales: leer los valores
TRYTON_HOST,TRYTON_DATABASE,TRYTON_USERNAME,TRYTON_PASSWORDde.env(raíz del proyecto) y pasarlos al contenedor con-e. No imprimir la contraseña (a lo sumo imprimir el nombre de la base de datos). - Respetar rate-limit: ante
HTTP 429esperar (p. ej. ~120s) antes de reintentar; no hacer ráfagas de logins fallidos. - Verificar primero el login/credenciales con una sonda mínima antes de asumir que falla por campos o datos.
Comando base (plantilla)
Todos los probes se ejecutan con docker compose dev sin levantar dependencias
(--no-deps):
docker compose -f docker-compose.dev.yml run --rm --no-deps \
-e TRYTON_HOST=<host de .env> \
-e TRYTON_DATABASE=<db de .env> \
-e TRYTON_USERNAME=<usuario de .env> \
-e TRYTON_PASSWORD=<password de .env> \
django python -c '
import os
from sabatron_tryton_rpc_client.client import Client
c = Client(hostname=os.environ["TRYTON_HOST"], database=os.environ["TRYTON_DATABASE"], username=os.environ["TRYTON_USERNAME"], password=os.environ["TRYTON_PASSWORD"])
c.connect()
print("CONNECTED OK", os.environ["TRYTON_DATABASE"])
ctx = {"company": 1}
print(c.call("model.product.product.search", [[["salable", "=", True]], 0, 5, [["rec_name", "ASC"], ["id", None]], ctx]))
'
Patrón de la librería:
from sabatron_tryton_rpc_client.client import Client
c = Client(hostname=..., database=..., username=..., password=...)
c.connect() # login
resultado = c.call("modelo.metodo", (args..., contexto)) # contexto {"company": 1}
Ver estados HTTP reales (401/403/429 enmascarados)
La librería enmascara los errores HTTP del login como
UnboundLocalError: cannot access local variable 'response'... (bug en
jsonrpc.py:__request). Para ver el código real, monkeypatchear el transporte
XML-RPC y capturar ProtocolError:
import xmlrpc.client
from sabatron_tryton_rpc_client import jsonrpc
orig = xmlrpc.client.Transport.request
def wrapped(self, host, handler, body, verbose=False):
try:
return orig(self, host, handler, body, verbose=verbose)
except xmlrpc.client.ProtocolError as e:
print("HTTP_STATUS", e.errcode, repr(e.errmsg))
raise
jsonrpc.Transport.request = wrapped
# luego Client(...).connect() / ServerProxy(...)
Guía rápida de códigos vistos en el proyecto:
HTTP 200+ fault JSON (p. ej.FATAL: database "..." does not exist): la DB no existe o el endpoint responde con error de aplicacion.HTTP 401: credenciales rechazadas (usuario/password) en esa base de datos.HTTP 403: ruta/método denegado (p. ej.common.db.list).HTTP 429: rate-limit, esperar antes de reintentar.
Convención de campos RPC de Tryton (agrupación con punto)
Los campos con punto (ej. template.categories.name) NO llegan como una clave
plana: Tryton los agrupa bajo la clave del modelo padre con puntos.
En model.product.product.read, con los fields
["id", "name", "template.categories", "template.categories.name"], la
respuesta llega así:
{
"id": 488,
"name": "Cloruro de magnesio",
"template.": {
"id": 1248,
"categories": [6],
"categories.": [{"id": 6, "name": "Alimentos Transformados Y Granos"}]
}
}
Combinaciones verificadas en staging (Tryton 8):
| fields solicitados | claves resultantes |
|---|---|
template.categories |
categories: [ids] (sin categories.) |
template.categories.id |
categories.: [{"id": ...}] |
template.categories.name |
categories.: [{"id", "name"}] (id viene incluido) |
ambos .id + .name |
igual que solo .name |
Conclusión operativa: pedir template.categories.name basta para traer ids y
nombres; template.categories.id es redundante. No hay active en
product.category (fields_get lo confirma) → activar/desactivar categorías es
solo local.
Ejemplos de sondas útiles
Login + datos de productos con categorias anidadas:
ctx = {"company": 1}
pids = c.call("model.product.product.search", [[["salable", "=", True]], 0, 1000, [["rec_name", "ASC"], ["id", None]], ctx])
detalles = c.call("model.product.product.read", (pids, ["id", "name", "default_uom.id", "default_uom.rec_name", "list_price", "template.categories", "template.categories.name"], ctx))
Listar los campos reales de un modelo (evita asumir campos):
campos = c.call("model.product.category.fields_get", ([], ctx))
print(sorted(campos))
Listar bases de datos (suele estar bloqueado con 403):
from sabatron_tryton_rpc_client.jsonrpc import ServerProxy
p = ServerProxy(hostname, 8000, "")
print(p.common.db.list())
Integración con tests
- Los tests de integración NO llaman Tryton real: mockean
sabatron_tryton_rpc_client.client.Client.cally.connect(verdon_confiao/tests/test_products_from_tryton.py,test_categories_from_tryton.py). - Los probes de esta skill son solo para verificación empírica/planeación contra staging, no para la suite de tests.