Utilitarios CISIS y Python

¿Cómo podemos acceder desde Python a las bases ISIS? Lo más simple es hacerlo a través de las herramientas de Bireme: utilitarios cisis y wxis.

Aquí, algunos experimentos para lograr wrappers en Python para estas herramientas.

mx dict

Para obtener algo aproximadamente igual a esto:

mx dict=biblio count=10 now "pft=v1^t,c8,v1^*/"
3 -ANOTACION-ACCESO
4 -ANOTACION-DESCR
656 -ANOTACION-OTRA
3 -ANOTACION-TEMA
18 -BIOGR=YES
1 -CREADO_POR=
2384 -CREADO_POR=FG
74 -CREADO_POR=LG
33 -F=####
1 -F=1854

podemos hacer algo así:

>>> import subprocess, re, os >>> command = 'mx dict=biblio count=10 "pft=v1/" now' >>> p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) >>> out = p.communicate()[0] >>> keys = [tuple(re.split('\^[lstk]', line)) for line in out.split(os.linesep)[:-1]] >>> keys [('-ANOTACION-ACCESO', '2', '17', '3', '1'), ('-ANOTACION-DESCR', '2', '16', '4', '2'), ('-ANOTACION-OTRA', '2', '15', '656', '3'), ('-ANOTACION-TEMA', '2', '15', '3', '4'), ('-BIOGR=YES', '1', '10', '18', '5'), ('-CREADO_POR=', '2', '12', '1', '6'), ('-CREADO_POR=FG', '2', '14', '2384', '7'), ('-CREADO_POR=LG', '2', '14', '74', '8'), ('-F=####', '1', '7', '33', '9'), ('-F=1854', '1', '7', '1', '10')] >>> for k in keys: print "%8s %s" % (k[3],k[0]) ... 3 -ANOTACION-ACCESO 4 -ANOTACION-DESCR 656 -ANOTACION-OTRA 3 -ANOTACION-TEMA 18 -BIOGR=YES 1 -CREADO_POR= 2384 -CREADO_POR=FG 74 -CREADO_POR=LG 33 -F=#### 1 -F=1854 >>>

wxis modules

Idea: usar Python para llamar a wxis y parsear el XML que devuelve:

fernando@ubuntu-fgomez:~/www/bases/cpd/catalis/bibima$ /home/fernando/www/cgi-bin/wxis54 IsisScript=/home/fernando/www/html/wxis_modules/wxis-modules/index.xis database=biblio count=10

Content-type:text/xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<wxis-modules IsisScript="/home/fernando/www/html/wxis_modules/wxis-modules/index.xis" version="0.1">
<term mfn="1">
<Isis_Key>
<occ>-ANOTACION-ACCESO</occ>
</Isis_Key>
<Isis_Postings>
<occ>3</occ>
</Isis_Postings>
<Isis_Posting>
<occ>0</occ>
</Isis_Posting>
</term>
...

También podemos cambiar los .xis para que generen otra cosa en vez de XML: JSON (http://pypi.python.org/pypi/simplejson), Python, ...

Estudiar el contenido de la carpeta wxis-php en wxis-modules, y adaptarlo a Python?

Entendiendo wxis-modules

TO-DO: ver si alguna diferencia entre las versiones Windows y Linux.

Desde el cliente se llama a call.php con method='post', y parámetros:

wxis_parameters (contenido de un textarea, formato xml)

task (una de 7 opciones: list, search, index, edit, write, delete, control)

En el caso particular de task=“write”, se añade un parámetro:

wxis_write_content

Archivo call.php

include('wxis.php')

wxis.php define 2 funciones “privadas”:

wxis_document_post => hace un petición HTTP con método POST

wxis_url => construye una URL para invocar a wxis

y estas 7 funciones “públicas”, una por cada posible valor de task:

wxis_list

wxis_search

wxis_index

wxis_edit

wxis_write

wxis_delete

wxis_control

Dichas funciones son muy simples, sólo contienen una línea:

return wxis_document_post(wxis_url("TASK.xis", $param));

para cada posible valor TASK del parámetro task, se ejecuta esta única línea:

print(wxis_TASK($_REQUEST[“wxis_parameters”]));

donde wxis_TASK es una de las 7 funciones de arriba. La excepción es task=write, que contiene un parámetro extra:

print(wxis_write($_REQUEST["wxis_parameters"], $_REQUEST["wxis_write_content"]));

Alternativamente, se define una clase DB_ISIS (archivo db_isis.php). No aparece usada en los ejemplos.

Esta clase tiene las siguientes funciones (métodos):

* ''getParameterList($list)'': devuelve un string XML, "<parameters>\n<key>value</key>\n...\n</parameters>\n"

Más las 7 funciones, una por tarea:

doList

search

index

edit

write

delete

control

Cada una de estas funciones hace lo siguiente:

return wxis_TASK($this->getParameterList($param));

wxis-modules => JSON => Python

Lo que sigue son notas acerca de la adaptación de los wxis-modules originales para que devuelvan JSON, y el desarrollo de un módulo de Python para comunicarse con esos scripts .xis.

TO-DO

Licencia

Definir la licencia a usar.

JSON

Hacer que WXIS devuelva JSON, para lo cual necesitamos:

[HECHO] Encerrar nombres y valores en comillas dobles.

[HECHO CON REPLACE] Escapar comillas dobles y barra invertida en los valores (mediante replace() o gizmo?). ATENCION: Esto no es solamente para los valores tomados de la base, sino también para los metadatos (e.g. los nombres de bases y archivos pueden contener barras invertidas)

[PENDIENTE] Omitir la coma final (usar iocc dentro de un grupo repetible [lista de campos], e Isis_Total(?) dentro de un <loop> [lista de registros y de términos])

[PARECE ANDAR BIEN] Verificar si Python consume correctamente el JSON devuelto por WXIS (sin necesidad de un json parser). Probar los caracteres escapados.

Renombrar py-wxis-modules como json-wxis-modules o wxis-json-modules o wxis-modules-json

Si queremos procesar el JSON devuelto por wxis desde otra aplicación (p.ej. en un browser mediante JavaScript), pero no queremos permitir acceso directo a wxis desde un browser, tendríamos que poder enviar el JSON crudo vía Python.

Grabación

[FUNCIONA BIEN!] ¿Cómo recibir los datos para grabar? ¿JSON? Estudiar lo que sucede en el write.xis original (insertar un display ALL antes del <write>). Una posibilidad para incorporar los datos es mediante un proc con H

$ mx tmp "proc='H100 10 0123456789'"
mfn= 1
100 «0123456789»

pero ojo, pues:

$ mx tmp "proc='H100 11 0123456789'"
fatal: '

$ mx tmp "proc='H100 9 0123456789'"
fatal: 9

Modificamos la definición del método write():

def write(self, content=()): content = ','.join([ "H%s %s %s" % (f[0], str(len(f[1])), f[1]) for f in content ])

y así podemos llamarlo:

content = ( ('100', 'abcdefghij'), ('200', 'ABC') ) db.write(mfn='291', content=content)

En write.xis podemos tener entonces:

<field action="cgi" tag="32199">content</field> <proc><pft>v32199</pft></proc>

Errores, excepciones

Definir excepciones generales: ConnectionError (cuando no se puede conectar con wxis), WxisResponseError (cuando la respuesta de wxis incluye un mensaje de error fatal o de ejecución, y por lo tanto no permite crear un diccionario, pero también cuando el error está generado desde un .xis, e.g. falta parámetro obligatorio). Definir además excepciones específicas para cada método donde se pueda tener Isis_Status diferente de cero: edit(), delete(), write(). Quizás la única excepción en estos casos es LockedRecordError.

class Error(Exception): pass class LockedRecordError(Error): def __init__(self, resp, message): self.resp = resp self.message = message def getStatus(resp): return resp['metadata']['Isis_Status'] def edit(self, **params): resp = self.__doTask('edit', params) if getStatus(resp) <> '0': raise LockedRecordError(resp, 'Registro bloqueado') else: return resp try: db.edit(mfn=256, lockid='xx') except LockedRecordError: print

Además, incluir siempre un except: como última instancia para capturar errores no previstos.

Ver lista de códigos de error de wxis:

http://ibama2.ibama.gov.br/cnia2/cisis/mensagens%20de%20erro%20do%20wxis-mx.pdf

http://www.elysio.com.br/documentacao/manual_phl81.pdf

http://www.google.com.ar/search?q=%22de+erro+do+CISIS%22&filter=0

Un tipo especial de error es cuando la expresión de búsqueda no está bien armada. Para eso existe un código llamado Isis_ErrorInfo:

<!--
Intento de descubrir qué es Isis_ErrorInfo
2008-03-27
Lo que detecto es que si uso una expresión de búsqueda mal formada,
el campo 1009 está vacío, mientras que si la expresión es correcta
el campo 1009 tiene valor '0'. Por lo tanto, luego de una búsqueda
sin resultados, podemos verificar el valor de Isis_ErrorInfo, y en
caso de no ser '0', incluir el error en la respuesta de wxis, para
luego generar una excepción en Python.
Ejemplos:
expresion Isis_ErrorInfo
--------------------------------------
test 0
test/( <vacio>
test/(4 4
test/(0 0 <= Entonces no nos sirve tanto!
test and 0
test * <vacio>
test + <vacio>
+test +
-->
<IsisScript>
<field action="cgi" tag="1">q</field>
<do task="search">
<parm name="db">G:\httpd_\bases\bibima\bibima</parm>
<parm name="expression"><pft>v1</pft></parm>
<parm name="count">2</parm>
<define>1001 Isis_Current</define>
<define>1002 Isis_Total</define>
<define>1009 Isis_ErrorInfo</define>
<loop>
<display><pft>mfn/</pft></display>
</loop>
<display><pft>'Isis_ErrorInfo: "', v1009,'"'</pft></display>
</do>
</IsisScript>

Testeo

Podemos incluir un conjunto de tests, que sirvan a la vez como ejemplos de uso y como verificación del buen funcionamiento del módulo.

Documentación

Documentar cada .xis y cada método del módulo: qué parámetros reciben, cuáles parámetros son obligatorios, cuáles son los defaults, y qué respuestas producen.

Documentar los requisitos para utilizar estos wxis-modules: wxis (versión), servidor web, permisos sobre los archivos y directorios. Versión de Python requerida.

Documentar cómo se instala/usa todo esto.

Documentar la configuración.

Archivos auxiliares

Incluir una base demo, útil para testeo. Podemos crear la base a partir de un archivo de texto.

Archivos: actab, uctab, stw, fst

Performance

Revisar el código interior de los loops, en particular display-record.xis. Escape de caracteres para JSON: comparar el uso de replace() (campo por campo, o a todos los campos de una vez), vs. el uso de un gizmo.

Otros

# An instance of class Isis may have some associated attributes, like:
# fst, actab, uctab, stw, which may then be passed to wxis in every invocation.
# This set of attributes may be available as a dictionary:
# >>> db.fst = '/home/user/isis/some.fst'
# >>> db.getParams()
# {'name': '/home/user/isis/testdb', 'fst': '/home/user/isis/some.fst', 'actab': '', 'uctab': '', 'stw': ''}

# The creation of a new masterfile is not associated with an existing db, so
# it should be a class/static method, not an instance method.

[HECHO] Quitar los @staticmethod, y llevar esos métodos fuera de la clase, a nivel del módulo.

Revisar la documentación de WXIS, en busca de algo importante que se haya escapado.

[HECHO] extract (usado en el opac para limpiar el query).

# Method to extract keys from a string, using wxis's builtin mechanism, and
# specifying custom fst, stw, actab and uctab parameters.
def extract(self, **params):
return self.__doTask('extract', params)

extract.xis
<do>
<parm name="count">1</parm>
<field action="cgi" tag="3002">technique</field>
<field action="replace" tag="3002">"4"n3002</field> <!-- default: 4 -->
<parm name="fst"><pft>'1 ', v3002,' v3001'</pft></parm> <!-- aplica la técnica al contenido de v3001 y almacena el resultado en v1 -->
<field action="cgi" tag="">stw</field>
<parm name="stw"></parm>
<field action="cgi" tag="">actab</field>
<parm name="actab"></parm>
<field action="cgi" tag="">uctab</field>
<parm name="uctab"></parm>
<field action="cgi" tag="3001">data</field>
<loop>
<field action="import" tag="list">3001</field>
<extract>this</extract>
<!--field action="export" tag="list">1</field-->
<display><pft>
'{"keys":['
(
'"', replace(replace(v1, '\', '\\'), '"', '\"'), '"'
if iocc < nocc(v1) then ',' fi
)
']}'
</pft></display>
</loop>
</do>

[HECHO, pasando un parámetro extra a search.xis] Sería bueno tener una función count que devuelva el número de resultados de una búsqueda, sin devolver los registros.

¿Informar a Bireme sobre este uso/modificación de wxis-modules?

¿Tiene relevancia la Python Database API Specification v2.0, que fue diseñada pensando en bases relacionales?

Parámetros actab y uctab: quitar los valores hardcoded en common.xis. Agregar parámetro stw. Usar archivo de configuración. La fst también debe poder ser compartida entre varias bases (o al menos, su nombre no tiene por qué coincidir con el de la base), agregarla al archivo de config, y modificar los scripts que usan fst. En realidad, los .xis no deben incluir información acerca de estos parámetros (ni hardcoded, ni en archivos de config). Los .xis sólo deben utilizar los parámetros tal como les son pasados por las funciones/métodos que los invocan. En todo caso, es cada aplicación específica que llama a los .xis quien debe decidir qué actab, uctab, stw, fst deben utilizarse.

[HECHO] Redefinir los métodos de la clase Isis para que puedan ser llamados con keyword arguments, e.g.

db.search({'query':'marsden', 'count':5}) => db.search(query='marsden', count=5)

>>> def test(**params):
... print params
...
>>> test(db='test', count=10)
{'count': 10, 'db': 'test'}

o bien

>>> def test(db, **params):
... print db, params
...
>>> test('testdb', count=10)
testdb {'count': 10}

Módulo isis (xispy)

Nombre sugerido por Rubén Mansilla: apysis (API en Python para bases Isis).

Esto es para agregar al ejemplo en isis.py

def remove_sf_marks(field): """ Input: '00^aDon Quijote de la Mancha /^cpor Miguel de Cervantes.' Output: 'Don Quijote de la Mancha / por Miguel de Cervantes.' """ return re.sub('\^\w', ' ', field['value'][4:] titles = [ unicode(remove_sf_marks(field), 'latin1') for rec in res['records'] for field in rec['fields'] if field['tag'] == '245' ] formatted = [ '(%s) %s' % (n, t) for (n, t) in zip(range(1, len(titles)), titles) ] print '\n'.join(formatted)

Manejando registros en Python [ver también: pymarc]

¿Y ahora cómo obtenemos datos de un registro? He aquí una representación de un registro Isis en Python:

{'fields': [{'tag': '001', 'value': '003003'}, {'tag': '905', 'value': 'c'}, {'tag': '906', 'value': 'a'}, {'tag': '907', 'value': 'm'}, {'tag': '908', 'value': '#'}, {'tag': '909', 'value': '#'}, {'tag': '917', 'value': '5'}, {'tag': '918', 'value': 'a'}, {'tag': '919', 'value': '#'}, {'tag': '008', 'value': '840710t19791963riu######b####000#0#eng##'}, {'tag': '245', 'value': '10^aHarmonic analysis of functions of several complex variables in the classical domains /^cby L. K. Hua ; [translated from the Russian by Leo Ebner and Adam Kor\xe1nyi].'}, {'tag': '250', 'value': '##^a[Rev. ed.].'}, {'tag': '260', 'value': '##^aProvidence, R.I. :^bAmerican Mathematical Society,^c1979, c1963.'}, {'tag': '300', 'value': '##^aiv, 164 p. ;^c24 cm.'}, {'tag': '440', 'value': '#0^aTranslations of mathematical monographs ;^vv. 6'}, {'tag': '500', 'value': '##^aTraducci\xf3n de: [To fu pien shu han shu lun chung ti tien hsing y\xfc ti t`iao ho f\xean hsi].'}, {'tag': '500', 'value': '##^aVersi\xf3n original en chino publicada en 1958; versi\xf3n en ruso publicada en 1959.'}, {'tag': '500', 'value': '##^a"Third printing, revised, 1979". Incluye tres ap\xe9ndices.'}, {'tag': '504', 'value': '##^aBibliograf\xeda: p. 183-186.'}, {'tag': '510', 'value': '4#^aMR,^c23 #A3277^3(de la ed. en ruso)'}, {'tag': '100', 'value': '1#^aHua, Lo-keng,^d1910-'}, {'tag': '240', 'value': '10^aTo fu pien shu han shu lun chung ti tien hsing y\xfc ti t`iao ho fen hsi.^lIngl\xe9s'}, {'tag': '650', 'value': '#0^aHarmonic analysis.'}, {'tag': '650', 'value': '#0^aFunctions of several complex variables.'}, {'tag': '084', 'value': '##^a32Mxx (22E30 31-02 43-02)^2MR'}, {'tag': '010', 'value': '##^a###63016769#'}, {'tag': '040', 'value': '##^aDLC/ICU^cICU^dDLC'}, {'tag': '041', 'value': '1#^aeng^hrus^hchi'}, {'tag': '991', 'value': 'FG'}, {'tag': '005', 'value': '20051018144818.0'}, {'tag': '859', 'value': '##^f20051018^hA-5622^pA-5622^uFG'}], 'mfn': '3000'}

Cómo obtener todas las ocurrencias de un campo:

def get(rec, tag): return [field['value'] for field in rec['fields'] if field['tag'] == tag]

>>> print get(rec, '504')[0]
##^aBibliograf�a: p. 183-186.

Para mostrar datos en Unicode:

>>> print unicode(get(rec, '504')[0], 'windows-1252')
##^aBibliografía: p. 183-186.

Probablemente sea mejor usar pymarc, tratando de inventar lo menos posible. Pero aún no lo probé.

Para ver:

r = IsisRecord()
r[245]
r[008]

Expresión Devuelve
----------------------------------------------------------------
r['245']
r['245']['a']
r['245a']
r.245
r.245.a
r.245a

Nota sobre errores en wxis

>>> print db.edit({'mfn': 2, 'lockid': 'FG'})
...
<update>
<write>Lock
WXIS|fatal error|unavoidable|recread/xropn/w|

Causa del error: el servidor web no tiene permiso para escribir la base.

Moraleja:

ajustar permisos

generar mensaje de error amigable

pymarc

wxis <=> xispy <=> myapp

Cada isisscript es totalmente ciego; recibe un conjunto de parámetros y se limita a devolver los datos pedidos, sin añadir ni modificar nada, y sin asumir ningún default, salvos los que ya trae incorporados wxis.

Lo único más o menos arbitrario es la estructura JSON con que se devuelven los datos.

xispy tiene la responsabilidad de:

pasar ciegamente a wxis los parámetros recibidos desde myapp

devolver a myapp un objeto que contenga los datos entregados por wxis

generar excepciones cuando corresponda, en base a la respuesta recibida desde wxis

Una vez que myapp obtiene los datos, tenemos que ver qué hacemos con ellos. Supongamos que recibimos un registro MARC. Una opción es tomar estos datos como input para una instancia de la clase pymarc.Record:

>>> resp = db.list(from=347, count=1) # we need a shortcut: db.list(mfn=347)
>>> from pymarc import Record
>>> record = Record()
>>> record.from_json(resp['record'])

A partir de ahí ya se puede manipular el registro usando los métodos que proporciona pymarc. Si se desea grabar el registro en la base, habrá que recurrir a un método que genere una lista de campos: [(tag, value), ...]

Por lo tanto, parece que necesitamos añadir dos custom methods a pymarc.Record:

def from_json(self): # or from_dict(self)
def to_list(self)

¿Tal vez lo mejor sea trabajar en forma simétrica? from_list() y to_list()

Atención: en alguna parte tiene que estar la responsabilidad sobre cómo tratar los datos del leader (MARC vs Isis)

Sobre cómo llamar a WXIS

¿Necesito un servidor HTTP para llamar localmente al wxis?

No lo sé en el caso de enviar datos para grabar, pero en el resto de los casos debería ser posible evitarlo, llamando directamente a wxis como un comando (local).

Pero hay dos problemas a resolver:

codificación de caracteres en los parámetros pasados en la línea de comandos

cómo pasar un bloque de texto, p.ej. un registro para ser grabado

La mayor ventaja de llamar a wxis vía HTTP es que nos independizamos de la localización del servidor donde se aloja la base de datos. La desventaja es el overhead de una petición a un servidor web, aunque éste sea local. Más aun, si sólo quisiéramos una aplicación que acceda a bases Isis pero sin que involucre la Web, entonces tendríamos que montar un servidor web sólo para poder usar wxis!

Así podemos llamar a wxis como comando desde Python:

def call_wxis_command(params):
import subprocess
args = ''
for k,v in params.iteritems():
args += ' "' + k + '=' + v + '"'
command = '/home/fernando/www/cgi-bin/wxis54 ' + args
print command
p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
out = p.communicate()[0]
return out

CDSOAI: IsisScripts on the fly

Un enfoque alternativo es el de CDSOAI, donde desde un servlet Java se invoca a wxis en “modo comando”, y por cada llamada a wxis se genera on-the-fly un IsisScript que contiene todos los parámetros hardcoded. Esos IsisScripts se generan en base a una plantilla, donde se sustituyen los valores de los parámetros correspondientes a la llamada actual. De esta manera, los archivos .xis en lugar de las líneas

<field action="cgi" tag="...">parametro</field>

tendrían

<field action="add" tag="...">${parametro}</field>

o alguna otra sintaxis apropiada al lenguaje (e.g. Python) desde el cual se harán las sustituciones. El script resultante de dicha sustitución debería guardarse en un archivo (temporal), para que wxis pueda leerlo, y ese archivo luego debería ser eliminado.

Una desventaja de este enfoque es que ya no podemos llamar a los IsisScripts en forma directa, pasándoles los parámetros (vía CGI o línea de comandos); necesariamente tenemos que pasar a través de otro lenguaje.

Test para wxis en línea de comando

Pruebas hechas en Linux con Gnome Terminal configurada para usar encoding Windows-1252

wxis: CISIS Interface v5.2b/GC/M/32767/10/30/I - XML IsisScript WWWISIS 7.1

mx: CISIS Interface v5.2b/GC/W/M/32767/10/30/I - Utility MX

test.xis:

<IsisScript> <field action="cgi" tag="1">query</field> <display><pft>'query: ', v1/</pft></display> <do task="search"> <parm name="db">/home/fer/test</parm> <parm name="expression"><pft>v1</pft></parm> <loop> <display><pft>'v1: ', v1/</pft></display> </loop> </do> </IsisScript>

$ mx tmp "proc='a1#anís#'" create=test count=1 now
$ mx test "fst=1 4 v1" actab=ac-ansi.tab uctab=uc-ansi.tab fullinv=test
$ ifkeys test
1|ANIS
$ wxis IsisScript=test.xis "query=anis"
query: anis
v1: anís
$ wxis IsisScript=test.xis "query=anís"
query: anís
v1: anís

Es el comportamiento esperado. Nótese que no se necesita indicar a wxis las tablas ANSI.

¿Pero qué pasa si esto se hace en un ambiente UTF-8?

Otro enfoque: parámetro ''in''

Recordando que wxis es un software sub-documentado, y teniendo en cuenta su íntima relación con mx, se me ocurre probar esto:

1. Crear un archivo wxis.par:

IsisScript=test.xis
query=anis

2. Ejecutar wxis pasándole un parámetro in:

wxis in=wxis.par

o bien, eliminando en wxis.par la línea de IsisScript:

wxis IsisScript=test.xis in=wxis.par

Cómo hacer una API para bases isis

Podemos tomar como referencia:

Malete + PHP

Python: SQL Database Interfaces (e.g. ver Programming Python, Cap. 19)

pyIsis (Degiorgi)

http://www.codigophp.com/pyisis/pyisis.txt

Django

Sobre uso de bases no relacionales: Using Django with a non-relational back-end data store?

Anything in Django that inherits from django.db.models.Model is fairly
tightly tied to Django's database backend: so SQL databases, etc.
If you want to get your data from another location, you can do that and
then just use Django's views and templates to present your data. Things
like generic views will not work without a bit of work on your part
(since they require something that works exactly like a model's query
interface), but they are just an aid in any case -- you do not lose any
functionality if you do not use generic views.
Before diving too deeply into putting a custom backend into the existing
model infrastructure, you should probably have a think about whether you
really need to (think about whether your are customising the right place
in the hierarchy). If you already have a way of accessing data and
getting it into Python objects, then you can probably live without
Django's ORM layer. After all, the views are just Python code, so they
can work with anything you like. The templates access everything using
attributes or dictionary keys or methods, so you pass them objects or
dictionaries and they do not care whether it's from Django's ORM or not.
Although the various layers in Django (models, views, templates) all
work well together, they are sufficiently orthogonal that they don't
rely on each other to operate, so you can happily use your own "model"
layer and that might be easier than trying to extend Django's model
layer to talk to your own backend.