====== 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
-ANOTACION-ACCESO
3
0
...
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, "\nvalue\n...\n\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 [[http://www.json.org/|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 [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 ). 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:
content
v32199
== 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:
q
G:\httpd_\bases\bibima\bibima
v1
2
1001 Isis_Current
1002 Isis_Total
1009 Isis_ErrorInfo
mfn/
'Isis_ErrorInfo: "', v1009,'"'
== 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
1
technique
"4"n3002
'1 ', v3002,' v3001'
stw
actab
uctab
data
3001
this
'{"keys":['
(
'"', replace(replace(v1, '\', '\\'), '"', '\"'), '"'
if iocc < nocc(v1) then ',' fi
)
']}'
* [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 **[[http://www.python.org/dev/peps/pep-0249/|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'})
...
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 [[http://ncsi-net.ncsi.iisc.ernet.in/pmwiki/?n=Main.Tools|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
parametro
tendrían
${parametro}
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**:
query
'query: ', v1/
/home/fer/test
v1
'v1: ', v1/
$ 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: [[http://groups.google.com/group/django-users/browse_thread/thread/9a426f8fa0a7bc90/708a3278356d025b|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.
{{tag>desarrollo isis python}}