[open-ils-commits] r127 - in servres/trunk: . sip sip/py (gfawcett)
svn at svn.open-ils.org
svn at svn.open-ils.org
Fri Feb 20 13:55:28 EST 2009
Author: gfawcett
Date: 2009-02-20 13:55:25 -0500 (Fri, 20 Feb 2009)
New Revision: 127
Added:
servres/trunk/sip/
servres/trunk/sip/py/
servres/trunk/sip/py/README
servres/trunk/sip/py/sipclient.py
servres/trunk/sip/py/sipconstants.py
Modified:
servres/trunk/.gitignore
Log:
baby steps toward a Python SIP client for ereserve/ils interaction.
Modified: servres/trunk/.gitignore
===================================================================
--- servres/trunk/.gitignore 2009-02-19 02:29:35 UTC (rev 126)
+++ servres/trunk/.gitignore 2009-02-20 18:55:25 UTC (rev 127)
@@ -1 +1,4 @@
conifer/static/uploads
+sip/openncip/*
+sip/doc/sip2_developers_guide.pdf
+sip/java/*
Added: servres/trunk/sip/py/README
===================================================================
--- servres/trunk/sip/py/README (rev 0)
+++ servres/trunk/sip/py/README 2009-02-20 18:55:25 UTC (rev 127)
@@ -0,0 +1,9 @@
+A Python implementation of the 3M Standard Interchange Protocol
+(SIP). It's a client, but I've tried to be agnostic when defining the
+protocol itself.
+
+My goals are to implement enough of SIP to enable our e-reserve system
+to interact with an ILS.
+
+Many thanks to Georgia Public Library Service and to David Fiander for
+the openncip project, which has been an invaluable source of insight.
Added: servres/trunk/sip/py/sipclient.py
===================================================================
--- servres/trunk/sip/py/sipclient.py (rev 0)
+++ servres/trunk/sip/py/sipclient.py 2009-02-20 18:55:25 UTC (rev 127)
@@ -0,0 +1,381 @@
+# Small portions are borrowed from David Fiander's acstest.py, in the
+# openncip project. David's license is below:
+
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+
+
+from sipconstants import *
+import socket
+import sys
+from datetime import datetime
+import re
+
+DEBUG = False
+
+# ------------------------------------------------------------
+# helper functions
+
+def split_n(n):
+ """Return a function that splits a string into two parts at index N."""
+ return lambda s: (s[:n], s[n:])
+
+split2 = split_n(2)
+
+
+
+
+# ------------------------------------------------------------
+# Messages
+
+# First we build up a little language for defining SIP messages, so
+# that we can define the protocol in a declarative style.
+
+
+class basefield(object):
+
+ def encode(self, dct):
+ """Take a dict, and return the wire representation of this field."""
+ raise NotImplementedError, repr(self)
+
+ def decode(self, bytes):
+ """
+ Take a wire representation and return a pair (V,R) where V is
+ the translated value of the current field, and R is the
+ remaining bytes after the field has been read. If this is an
+ optional field, then decode should return None for V, and
+ return the input bytes for R.
+ """
+ raise NotImplementedError, repr(self)
+
+
+class field(basefield):
+
+ def __init__(self, name, code, width=None):
+ self.name = name
+ self.code = code
+ self.width = None # don't use this yet.
+
+ def encode(self, dct):
+ return '%s%s|' % (self.code, dct.get(self.name, ''))
+
+ def decode(self, bytes):
+ bcode, rest = split2(bytes)
+ if bcode != self.code:
+ raise 'BadDecode', \
+ 'Wrong field! Expected %r (%s) got %r (%s), in %r.' % (
+ self.code, lookup_constant(self.code),
+ bcode, lookup_constant(bcode),
+ bytes)
+ data, rest = rest.split('|', 1)
+ return data, rest
+
+
+class optfield(field): # an optional field
+
+ def decode(self, bytes):
+ tmp = bytes + ' '
+ bcode, rest = split2(tmp)
+ if bcode == self.code:
+ return field.decode(self, bytes)
+ else:
+ return None, bytes
+
+
+class charfield(basefield):
+
+ def __init__(self, name, width=None, default=None):
+ self.name = name
+ self.dflt = str(default)
+ self.width = width or len(self.dflt) # give at least one
+ self.pad = ' ' * self.width
+
+ self.decode = split_n(self.width)
+
+ def encode(self, dct):
+ v = dct.get(self.name, self.dflt)
+ assert v is not None
+ return ('%s%s' % (self.pad, v))[-self.width:]
+
+
+class yn(basefield):
+ def __init__(self, name):
+ self.name = name
+
+ def encode(self, dct):
+ return 'NY'[bool(dct.get(self.name))]
+
+ def decode(self, bytes):
+ return (bytes[0] == 'Y'), bytes[1:]
+
+
+class localtime(charfield):
+ def __init__(self, name):
+ self.name = name
+ self.width = 18
+
+ def encode(self, dct):
+ return datetime.now().strftime('%Y%m%d %H%M%S')
+
+ def decode(self, bytes):
+ return split_n(self.width)(bytes)
+
+RAW = -55
+class raw(basefield):
+ name = 'raw'
+ # for debugging.
+ def decode(self, bytes):
+ return bytes, '\r'
+
+# We define a protocol Message as a list of fields. For now,
+# message(A, B, C) is equivalent to the tuple (A,B,C).
+
+message = lambda *args: args
+
+# Encoding a message on to the wire. Args is a dict of field-values.
+
+def encode_msg(msg, args):
+ out = []
+ add = out.append
+ for thing in msg:
+ if isinstance(thing, basefield):
+ add(thing.encode(args))
+ else:
+ add(str(thing))
+ return ''.join(out)
+
+# Decoding from the wire:
+
+def decode_msg(msg, bytes):
+ out = {}
+ add = out.__setitem__
+ rest = bytes
+
+ # Proper 'fields' have variable position in the tail of the
+ # message. So we treat them differently.
+ varposn = set([p for p in msg if isinstance(p, field)])
+ varlookup = dict((x.code, x) for x in varposn)
+ fixedposn = [p for p in msg if not p in varposn]
+
+ for part in fixedposn:
+ if isinstance(part, basefield):
+ good, rest = part.decode(rest)
+ if good is not None:
+ add(part.name, good)
+ else:
+ v = str(part)
+ good, rest = rest[:len(v)], rest[len(v):]
+ assert v == good
+ if DEBUG: print '%s == %r\n==== %r' % (getattr(part, 'name',''), good, rest)
+
+ # Now we take what's left, chunk it, and try to resolve each one
+ # against a variable-position field.
+ segments = re.findall(r'(.*?\|)', rest)
+
+ if DEBUG: print segments
+
+ for segment in segments:
+ fld = varlookup.get(segment[:2])
+ if fld:
+ good, rest = fld.decode(segment)
+ add(fld.name, good)
+ varposn.remove(fld)
+ else:
+ raise 'FieldNotProcessed', (segment, lookup_constant(segment[:2]))
+
+ # Let's make sure that any "required" fields were not missing.
+ notpresent = set(f for f in varposn if not isinstance(f, optfield))
+ if notpresent:
+ for f in notpresent:
+ print 'MISSING: %-12s %s %s' % (f.name, f.code, lookup_constant(f.code))
+ raise 'MandatoryFieldsNotPresent'
+
+ return out
+
+# The SIP checksum. Borrowed from djfiander.
+
+def checksum(msg):
+ return '%04X' % ((0 - sum(map(ord, msg))) & 0xFFFF)
+
+
+#------------------------------------------------------------
+# SIP Message Definitions
+
+MESSAGES = {
+ LOGIN : message(
+ LOGIN,
+ '00',
+ field('uid', FID_LOGIN_UID),
+ field('pwd', FID_LOGIN_PWD),
+ field('locn', FID_LOCATION_CODE)),
+
+ LOGIN_RESP : message(
+ LOGIN_RESP,
+ charfield('okay', width=1)),
+
+ SC_STATUS : message(
+ SC_STATUS,
+ charfield('online', default='1'),
+ charfield('width', default='040'),
+ charfield('version', default='2.00')),
+
+ ACS_STATUS : message(
+ ACS_STATUS,
+ yn('online'),
+ yn('checkin_OK'),
+ yn('checkout_OK'),
+ yn('renewal_OK'),
+ yn('status_update_OK'),
+ yn('offline_OK'),
+ charfield('timeout', default='01'),
+ charfield('retries', default='9999'),
+ charfield('localtime', default='YYYYMMDD HHMMSS'),
+ charfield('protocol', default='2.00'),
+ field('inst', FID_INST_ID),
+ optfield('instname', FID_LIBRARY_NAME),
+ field('supported', FID_SUPPORTED_MSGS),
+ optfield('ttylocn', FID_TERMINAL_LOCN),
+ optfield('screenmsg', FID_SCREEN_MSG),
+ optfield('printline', FID_PRINT_LINE)),
+ PATRON_INFO : message(
+ PATRON_INFO,
+ charfield('lang', width=3, default=1),
+ localtime('localtime'),
+ charfield('holditemsreq', default='Y '),
+ field('inst', FID_INST_ID),
+ field('patron', FID_PATRON_ID),
+ optfield('termpwd', FID_TERMINAL_PWD),
+ optfield('patronpwd', FID_PATRON_PWD),
+ optfield('startitem', FID_START_ITEM, width=5),
+ optfield('enditem', FID_END_ITEM, width=5)),
+
+ PATRON_INFO_RESP : message(
+ PATRON_INFO_RESP,
+ charfield('hmmm', width=14),
+ charfield('lang', width=3, default=1),
+ localtime('localtime'),
+ charfield('onhold', width=4),
+ charfield('overdue', width=4),
+ charfield('charged', width=4),
+ charfield('fine', width=4),
+ charfield('recall', width=4),
+ charfield('unavail_holds', width=4),
+
+ field('inst', FID_INST_ID),
+ optfield('screenmsg', FID_SCREEN_MSG),
+ optfield('printline', FID_PRINT_LINE),
+ optfield('instname', FID_LIBRARY_NAME),
+ field('patron', FID_PATRON_ID),
+ field('personal', FID_PERSONAL_NAME),
+
+ optfield('hold_limit', FID_HOLD_ITEMS_LMT, width=4),
+ optfield('overdue_limit', FID_OVERDUE_ITEMS_LMT, width=4),
+ optfield('charged_limit', FID_OVERDUE_ITEMS_LMT, width=4),
+
+ optfield('hold_items', FID_HOLD_ITEMS),
+ optfield('valid_patron_pwd', FID_VALID_PATRON_PWD),
+
+ optfield('valid_patron', FID_VALID_PATRON),
+ optfield('currency', FID_CURRENCY),
+ optfield('fee_amt', FID_FEE_AMT),
+ optfield('fee_limit', FID_FEE_LMT),
+ optfield('home_addr', FID_HOME_ADDR),
+ optfield('email', FID_EMAIL),
+ optfield('home_phone', FID_HOME_PHONE),
+ optfield('patron_birthdate', FID_PATRON_BIRTHDATE),
+ optfield('patron_class', FID_PATRON_CLASS),
+ optfield('inet_profile', FID_INET_PROFILE),
+ optfield('home_library', FID_HOME_LIBRARY)),
+
+ END_PATRON_SESSION : message(
+ END_PATRON_SESSION,
+ localtime('localtime'),
+ field('inst', FID_INST_ID),
+ field('patron', FID_PATRON_ID)),
+
+ END_SESSION_RESP : message(
+ END_SESSION_RESP,
+ yn('session_ended'),
+ localtime('localtime'),
+ field('inst', FID_INST_ID),
+ field('patron', FID_PATRON_ID),
+ optfield('printline', FID_PRINT_LINE),
+ optfield('screenmsg', FID_SCREEN_MSG)),
+
+ RAW : message(raw()),
+}
+
+
+
+class SipClient(object):
+ def __init__(self, host, port, error_detect=False):
+ self.hostport = (host, port)
+ self.error_detect = error_detect
+ self.connect()
+
+ def connect(self):
+ so = socket.socket()
+ so.connect(self.hostport)
+ self.socket = so
+ self.seqno = self.error_detect and 1 or 0
+
+ def send(self, outmsg, inmsg, args=None):
+ msg_template = MESSAGES[outmsg]
+ resp_template = MESSAGES[inmsg]
+ msg = encode_msg(msg_template, args or {})
+ if self.error_detect:
+ # add the checksum
+ msg += 'AY%dAZ' % (self.seqno % 10)
+ self.seqno += 1
+ msg += checksum(msg)
+ msg += '\r'
+ if DEBUG: print '>>> %r' % msg
+ self.socket.send(msg)
+ resp = self.socket.recv(1000)
+ if DEBUG: print '<<< %r' % resp
+ return decode_msg(resp_template, resp)
+
+
+ # --------------------------------------------------
+ # Common protocol methods
+
+ def login(self, uid, pwd, locn):
+ return self.send(LOGIN, LOGIN_RESP,
+ dict(uid=uid, pwd=pwd, locn=locn))
+
+ def status(self):
+ return self.send(SC_STATUS, ACS_STATUS)
+
+
+if __name__ == '__main__':
+ from pprint import pprint
+
+ sip = SipClient('localhost', 6001)
+ resp = sip.login(uid='scclient',
+ pwd='clientpwd', locn='The basement')
+ pprint(resp)
+ pprint(sip.status())
+ pprint(sip.send(PATRON_INFO, PATRON_INFO_RESP,
+ {'patron':'scclient'}))
+
+
+ pprint(sip.send(END_PATRON_SESSION, END_SESSION_RESP,
+ {'patron':'scclient',
+ 'inst':'UWOLS'}))
+
+ #{'raw': '36Y20090220 133339AOUWOLS|AAscclient|AFThank you for using Evergreen!|\r'}
Added: servres/trunk/sip/py/sipconstants.py
===================================================================
--- servres/trunk/sip/py/sipconstants.py (rev 0)
+++ servres/trunk/sip/py/sipconstants.py 2009-02-20 18:55:25 UTC (rev 127)
@@ -0,0 +1,168 @@
+# This is the work of David Fiander, from the openncip project. We
+# have transformed it into a Python library. David's license appears
+# below.
+
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+
+constants = [
+ # Messages from SC to ACS
+ ('PATRON_STATUS_REQ', '23'),
+ ('CHECKOUT', '11'),
+ ('CHECKIN', '09'),
+ ('BLOCK_PATRON', '01'),
+ ('SC_STATUS', '99'),
+ ('REQUEST_ACS_RESEND', '97'),
+ ('LOGIN', '93'),
+ ('PATRON_INFO', '63'),
+ ('END_PATRON_SESSION', '35'),
+ ('FEE_PAID', '37'),
+ ('ITEM_INFORMATION', '17'),
+ ('ITEM_STATUS_UPDATE', '19'),
+ ('PATRON_ENABLE', '25'),
+ ('HOLD', '15'),
+ ('RENEW', '29'),
+ ('RENEW_ALL', '65'),
+
+ # Message responses from ACS to SC
+ ('PATRON_STATUS_RESP', '24'),
+ ('CHECKOUT_RESP', '12'),
+ ('CHECKIN_RESP', '10'),
+ ('ACS_STATUS', '98'),
+ ('REQUEST_SC_RESEND', '96'),
+ ('LOGIN_RESP', '94'),
+ ('PATRON_INFO_RESP', '64'),
+ ('END_SESSION_RESP', '36'),
+ ('FEE_PAID_RESP', '38'),
+ ('ITEM_INFO_RESP', '18'),
+ ('ITEM_STATUS_UPDATE_RESP', '20'),
+ ('PATRON_ENABLE_RESP', '26'),
+ ('HOLD_RESP', '16'),
+ ('RENEW_RESP', '30'),
+ ('RENEW_ALL_RESP', '66'),
+
+ #
+ # Some messages are short and invariant, so they're constant's too
+ #
+ ('REQUEST_ACS_RESEND_CKSUM', '97AZFEF5'),
+ ('REQUEST_SC_RESEND_CKSUM', '96AZFEF6'),
+
+ #
+ # Field Identifiers
+ #
+ ('FID_PATRON_ID', 'AA'),
+ ('FID_ITEM_ID', 'AB'),
+ ('FID_TERMINAL_PWD', 'AC'),
+ ('FID_PATRON_PWD', 'AD'),
+ ('FID_PERSONAL_NAME', 'AE'),
+ ('FID_SCREEN_MSG', 'AF'),
+ ('FID_PRINT_LINE', 'AG'),
+ ('FID_DUE_DATE', 'AH'),
+ # UNUSED AI
+ ('FID_TITLE_ID', 'AJ'),
+ # UNUSED AK
+ ('FID_BLOCKED_CARD_MSG', 'AL'),
+ ('FID_LIBRARY_NAME', 'AM'),
+ ('FID_TERMINAL_LOCN', 'AN'),
+ ('FID_INST_ID', 'AO'),
+ ('FID_CURRENT_LOCN', 'AP'),
+ ('FID_PERM_LOCN', 'AQ'),
+ ('FID_HOME_LIBRARY', 'AQ'), # Extension: AQ in patron info
+ # UNUSED AR
+ ('FID_HOLD_ITEMS', 'AS'), # SIP 2.0
+ ('FID_OVERDUE_ITEMS', 'AT'), # SIP 2.0
+ ('FID_CHARGED_ITEMS', 'AU'), # SIP 2.0
+ ('FID_FINE_ITEMS', 'AV'), # SIP 2.0
+ # UNUSED AW
+ # UNUSED AX
+ ('FID_SEQNO', 'AY'),
+ ('FID_CKSUM', 'AZ'),
+
+ # SIP 2.0 Fields
+ # UNUSED BA
+ # UNUSED BB
+ # UNUSED BC
+ ('FID_HOME_ADDR', 'BD'),
+ ('FID_EMAIL', 'BE'),
+ ('FID_HOME_PHONE', 'BF'),
+ ('FID_OWNER', 'BG'),
+ ('FID_CURRENCY', 'BH'),
+ ('FID_CANCEL', 'BI'),
+ # UNUSED BJ
+ ('FID_TRANSACTION_ID', 'BK'),
+ ('FID_VALID_PATRON', 'BL'),
+ ('FID_RENEWED_ITEMS', 'BM'),
+ ('FID_UNRENEWED_ITEMS', 'BN'),
+ ('FID_FEE_ACK', 'BO'),
+ ('FID_START_ITEM', 'BP'),
+ ('FID_END_ITEM', 'BQ'),
+ ('FID_QUEUE_POS', 'BR'),
+ ('FID_PICKUP_LOCN', 'BS'),
+ ('FID_FEE_TYPE', 'BT'),
+ ('FID_RECALL_ITEMS', 'BU'),
+ ('FID_FEE_AMT', 'BV'),
+ ('FID_EXPIRATION', 'BW'),
+ ('FID_SUPPORTED_MSGS', 'BX'),
+ ('FID_HOLD_TYPE', 'BY'),
+ ('FID_HOLD_ITEMS_LMT', 'BZ'),
+ ('FID_OVERDUE_ITEMS_LMT', 'CA'),
+ ('FID_CHARGED_ITEMS_LMT', 'CB'),
+ ('FID_FEE_LMT', 'CC'),
+ ('FID_UNAVAILABLE_HOLD_ITEMS', 'CD'),
+ # UNUSED CE
+ ('FID_HOLD_QUEUE_LEN', 'CF'),
+ ('FID_FEE_ID', 'CG'),
+ ('FID_ITEM_PROPS', 'CH'),
+ ('FID_SECURITY_INHIBIT', 'CI'),
+ ('FID_RECALL_DATE', 'CJ'),
+ ('FID_MEDIA_TYPE', 'CK'),
+ ('FID_SORT_BIN', 'CL'),
+ ('FID_HOLD_PICKUP_DATE', 'CM'),
+ ('FID_LOGIN_UID', 'CN'),
+ ('FID_LOGIN_PWD', 'CO'),
+ ('FID_LOCATION_CODE', 'CP'),
+ ('FID_VALID_PATRON_PWD', 'CQ'),
+
+ # SIP Extensions used by Envisionware Terminals
+ ('FID_PATRON_BIRTHDATE', 'PB'),
+ ('FID_PATRON_CLASS', 'PC'),
+
+ # SIP Extension for reporting patron internet privileges
+ ('FID_INET_PROFILE', 'PI'),
+
+ #
+ # SC Status Codes
+ #
+ ('SC_STATUS_OK', '0'),
+ ('SC_STATUS_PAPER', '1'),
+ ('SC_STATUS_SHUTDOWN', '2'),
+
+ #
+ # Various format strings
+ #
+ ('SIP_DATETIME', "%Y%m%d %H%M%S"),
+]
+
+# make them toplevel variables.
+for k,v in constants:
+ locals()[k] = v
+
+def lookup_constant(x):
+ for k, v in constants:
+ if v == x:
+ return k
More information about the open-ils-commits
mailing list