¿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.
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 >>>
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?
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
wxis.php define 2 funciones “privadas”:
y estas 7 funciones “públicas”, una por cada posible valor de task:
Dichas funciones son muy simples, sólo contienen una línea:
return wxis_document_post(wxis_url("TASK.xis", $param));
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:
Cada una de estas funciones hace lo siguiente:
return wxis_TASK($this->getParameterList($param));
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
.
Definir la licencia a usar.
$ 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>
.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:
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>
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.
.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.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.# 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.
@staticmethod
, y llevar esos métodos fuera de la clase, a nivel del módulo.# 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>
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.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}
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)
¿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
>>> 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:
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:
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)
¿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:
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
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.
Pruebas hechas en Linux con Gnome Terminal configurada para usar encoding Windows-1252
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?
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
Podemos tomar como referencia:
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.