Recipes
Code to copy, tested against a real workspace. Replace the address, the database, the login and the key: the rest works.
01Python
Nothing to install: xmlrpc.client is in the standard
library.
import os
import xmlrpc.client
class Madata:
"""Minimal client for the data API of a MADATA workspace."""
def __init__(self, workspace, login, key):
self.url = f"https://{workspace}.madata.app"
self.db = f"prod_{workspace}"
self.key = key
common = xmlrpc.client.ServerProxy(f"{self.url}/xmlrpc/2/common")
self.uid = common.authenticate(self.db, login, key, {})
if not self.uid:
raise RuntimeError("credentials refused")
self.models = xmlrpc.client.ServerProxy(f"{self.url}/xmlrpc/2/object")
def call(self, model, method, *args, **kw):
return self.models.execute_kw(
self.db, self.uid, self.key, model, method, list(args), kw)
def search(self, model, domain, fields, **kw):
return self.call(model, 'search_read', domain, fields=fields, **kw)
ma = Madata(
workspace=os.environ["MADATA_WORKSPACE"],
login=os.environ["MADATA_LOGIN"],
key=os.environ["MADATA_KEY"],
)
for c in ma.search('res.partner', [['customer_rank', '>', 0]],
['name', 'email'], limit=5):
print(c['id'], c['name'], c['email'] or '-')
02Node.js
In JSON-RPC, fetch is enough: no dependency.
const WORKSPACE = process.env.MADATA_WORKSPACE;
const ENDPOINT = `https://${WORKSPACE}.madata.app/jsonrpc`;
const DB = `prod_${WORKSPACE}`;
const KEY = process.env.MADATA_KEY;
async function rpc(service, method, args) {
const r = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0', method: 'call',
params: { service, method, args },
}),
});
const data = await r.json();
// An error arrives with HTTP 200: the "error" key is what counts.
if (data.error) {
const d = data.error.data || {};
throw new Error(`${d.name || 'error'}: ${d.message || data.error.message}`);
}
return data.result;
}
const uid = await rpc('common', 'authenticate',
[DB, process.env.MADATA_LOGIN, KEY, {}]);
const call = (model, method, args = [], kw = {}) =>
rpc('object', 'execute_kw', [DB, uid, KEY, model, method, args, kw]);
const customers = await call('res.partner', 'search_read',
[[['customer_rank', '>', 0]]],
{ fields: ['name', 'email'], limit: 5 });
console.table(customers);
03PHP
<?php
require_once 'ripcord.php';
$workspace = getenv('MADATA_WORKSPACE');
$url = "https://{$workspace}.madata.app";
$db = "prod_{$workspace}";
$key = getenv('MADATA_KEY');
$common = ripcord::client("$url/xmlrpc/2/common");
$uid = $common->authenticate($db, getenv('MADATA_LOGIN'), $key, []);
$models = ripcord::client("$url/xmlrpc/2/object");
$customers = $models->execute_kw($db, $uid, $key,
'res.partner', 'search_read',
[[['customer_rank', '>', 0]]],
['fields' => ['name', 'email'], 'limit' => 5]);
foreach ($customers as $c) {
echo $c['name'], ' ', ($c['email'] ?: '-'), PHP_EOL;
}
04curl
For a quick check, or to debug from a server where you will install nothing.
WORKSPACE=kouassi KEY=your-api-key [email protected] # server version curl -s https://$WORKSPACE.madata.app/jsonrpc -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"call","params":{ "service":"common","method":"version","args":[]}}' # uid curl -s https://$WORKSPACE.madata.app/jsonrpc -H "Content-Type: application/json" \ -d "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{ \"service\":\"common\",\"method\":\"authenticate\", \"args\":[\"prod_$WORKSPACE\",\"$LOGIN\",\"$KEY\",{}]}}" # the 5 most recent contacts (uid = 7 in this example) curl -s https://$WORKSPACE.madata.app/jsonrpc -H "Content-Type: application/json" \ -d "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{ \"service\":\"object\",\"method\":\"execute_kw\", \"args\":[\"prod_$WORKSPACE\",7,\"$KEY\",\"res.partner\", \"search_read\",[[]], {\"fields\":[\"name\"],\"limit\":5}]}}"
05Recipe: today’s overdue invoices
from datetime import date
overdue = ma.search('account.move',
[['move_type', '=', 'out_invoice'],
['state', '=', 'posted'],
['payment_state', 'in', ['not_paid', 'partial']],
['invoice_date_due', '<', date.today().isoformat()]],
['name', 'partner_id', 'invoice_date_due', 'amount_residual'],
order='invoice_date_due')
total = sum(i['amount_residual'] for i in overdue)
print(f"{len(overdue)} overdue invoices, {total:,.0f} FCFA")
for i in overdue[:20]:
print(i['invoice_date_due'], i['partner_id'][1], i['amount_residual'])
06Recipe: sync a catalogue
The "look up, otherwise create" pattern applied to a product feed from another system. Replayable without duplicating anything.
def sync(product):
"""product = {'ref': 'SKU-001', 'name': '...', 'price': 12000}"""
existing = ma.call('product.template', 'search',
[['default_code', '=', product['ref']]], limit=1)
values = {
'name': product['name'],
'default_code': product['ref'],
'list_price': product['price'],
}
if existing:
ma.call('product.template', 'write', existing, values)
return existing[0], 'updated'
return ma.call('product.template', 'create', values), 'created'
for p in external_feed():
id_, action = sync(p)
print(p['ref'], action, id_)
07Recipe: attach a document
import base64
with open('delivery_note.pdf', 'rb') as f:
content = base64.b64encode(f.read()).decode()
ma.call('ir.attachment', 'create', {
'name': 'delivery_note.pdf',
'datas': content,
'res_model': 'account.move',
'res_id': invoice_id,
'mimetype': 'application/pdf',
})
08Going to production
- A dedicated key
- One per program, named after it, held by a user whose permissions match the job.
- Secrets out of the code
- Environment variables or a secret manager. Never in a repository, even a private one.
- A planned retry
- Backing off on transient errors, a hard stop on permission errors, and a lookup before every replayable write.
- A log
- Model, method, arguments, full response or error. That is what turns an incident into a ten minute fix.
- A smoke test
- A
search_countat startup: if the key is revoked you know right away, not halfway through an import. - Batches
- Group reads and writes. The network costs more than the computation.
A doubt, a blocker
Go through support from your workspace, with the exact call and the response you got. And if what you want to do fits in a sentence rather than a program, the MCP server may already do it.