[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