[open-ils-commits] r249 - in servres/trunk: conifer/libsystems conifer/libsystems/sip sip/py (gfawcett)
svn at svn.open-ils.org
svn at svn.open-ils.org
Thu Apr 2 21:31:36 EDT 2009
Author: gfawcett
Date: 2009-04-02 21:31:34 -0400 (Thu, 02 Apr 2009)
New Revision: 249
Added:
servres/trunk/conifer/libsystems/sip/
servres/trunk/conifer/libsystems/sip/README
servres/trunk/conifer/libsystems/sip/__init__.py
servres/trunk/conifer/libsystems/sip/sipclient.py
servres/trunk/conifer/libsystems/sip/sipconstants.py
servres/trunk/conifer/libsystems/sip/siptest1.py
Removed:
servres/trunk/sip/py/README
servres/trunk/sip/py/sipclient.py
servres/trunk/sip/py/sipconstants.py
Log:
moved SIP client into conifer.libsystems.sip
Copied: servres/trunk/conifer/libsystems/sip/README (from rev 248, servres/trunk/sip/py/README)
===================================================================
--- servres/trunk/conifer/libsystems/sip/README (rev 0)
+++ servres/trunk/conifer/libsystems/sip/README 2009-04-03 01:31:34 UTC (rev 249)
@@ -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/conifer/libsystems/sip/__init__.py
===================================================================
Copied: servres/trunk/conifer/libsystems/sip/sipclient.py (from rev 248, servres/trunk/sip/py/sipclient.py)
===================================================================
--- servres/trunk/conifer/libsystems/sip/sipclient.py (rev 0)
+++ servres/trunk/conifer/libsystems/sip/sipclient.py 2009-04-03 01:31:34 UTC (rev 249)
@@ -0,0 +1,433 @@
+# 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
+
+# some common fields
+
+
+fld_localtime = localtime('localtime')
+fld_INST_ID = field('inst', FID_INST_ID)
+fld_ITEM_ID = field('item', FID_ITEM_ID)
+fld_PATRON_ID = field('patron', FID_PATRON_ID)
+ofld_TERMINAL_PWD = optfield('termpwd', FID_TERMINAL_PWD)
+fld_proto_version = charfield('version', default='2.00')
+ofld_print_line = optfield('print_line', FID_PRINT_LINE)
+ofld_screen_msg = optfield('screenmsg', FID_SCREEN_MSG)
+
+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'),
+ fld_proto_version),
+
+ 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'),
+ fld_localtime,
+ charfield('protocol', default='2.00'),
+ fld_INST_ID,
+ optfield('instname', FID_LIBRARY_NAME),
+ field('supported', FID_SUPPORTED_MSGS),
+ optfield('ttylocn', FID_TERMINAL_LOCN),
+ ofld_screen_msg,
+ ofld_print_line),
+ PATRON_INFO : message(
+ PATRON_INFO,
+ charfield('lang', width=3, default=1),
+ fld_localtime,
+ charfield('holditemsreq', default='Y '),
+ fld_INST_ID,
+ fld_PATRON_ID,
+ ofld_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),
+ fld_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),
+ fld_INST_ID,
+ ofld_screen_msg,
+ ofld_print_line,
+ optfield('instname', FID_LIBRARY_NAME),
+ fld_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,
+ fld_localtime,
+ field('inst', FID_INST_ID),
+ field('patron', FID_PATRON_ID)),
+
+ END_SESSION_RESP : message(
+ END_SESSION_RESP,
+ yn('session_ended'),
+ fld_localtime,
+ fld_INST_ID,
+ fld_PATRON_ID,
+ ofld_print_line,
+ ofld_screen_msg),
+
+ ITEM_INFORMATION : message(
+ ITEM_INFORMATION,
+ fld_localtime,
+ fld_INST_ID,
+ fld_ITEM_ID,
+ ofld_TERMINAL_PWD),
+
+ ITEM_INFO_RESP : message(
+ ITEM_INFO_RESP,
+ charfield('circstat', width=2),
+ charfield('security', width=2),
+ charfield('feetype', width=2),
+ fld_localtime,
+ fld_ITEM_ID,
+ field('title', FID_TITLE_ID),
+ optfield('mediatype', FID_MEDIA_TYPE),
+ optfield('perm_locn', FID_PERM_LOCN),
+ optfield('current_locn', FID_CURRENT_LOCN),
+ optfield('item_props', FID_ITEM_PROPS),
+ optfield('currency', FID_CURRENCY),
+ optfield('fee', FID_FEE_AMT),
+ optfield('owner', FID_OWNER),
+ optfield('hold_queue_len', FID_HOLD_QUEUE_LEN),
+ optfield('due_date', FID_DUE_DATE),
+
+ optfield('recall_date', FID_RECALL_DATE),
+ optfield('hold_pickup_date', FID_HOLD_PICKUP_DATE),
+ ofld_screen_msg,
+ ofld_print_line),
+
+ 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)
+
+
+# ------------------------------------------------------------
+# Test code.
+
+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',
+ 'startitem':1, 'enditem':2}))
+
+ # these are items from openncip's test database.
+ item_ids = ['1565921879', '0440242746', '660']
+ bad_ids = ['xx' + i for i in item_ids]
+ for item in (item_ids + bad_ids):
+ result = sip.send(ITEM_INFORMATION, ITEM_INFO_RESP,
+ {'item':item})
+ print '%-12s: %s' % (item, result['title'] or '????')
+
+ pprint(sip.send(END_PATRON_SESSION, END_SESSION_RESP,
+ {'patron':'scclient',
+ 'inst':'UWOLS'}))
+
+
Copied: servres/trunk/conifer/libsystems/sip/sipconstants.py (from rev 248, servres/trunk/sip/py/sipconstants.py)
===================================================================
--- servres/trunk/conifer/libsystems/sip/sipconstants.py (rev 0)
+++ servres/trunk/conifer/libsystems/sip/sipconstants.py 2009-04-03 01:31:34 UTC (rev 249)
@@ -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
Added: servres/trunk/conifer/libsystems/sip/siptest1.py
===================================================================
--- servres/trunk/conifer/libsystems/sip/siptest1.py (rev 0)
+++ servres/trunk/conifer/libsystems/sip/siptest1.py 2009-04-03 01:31:34 UTC (rev 249)
@@ -0,0 +1,15 @@
+from pprint import pprint
+from sipclient import *
+
+sip = SipClient('dwarf.cs.uoguelph.ca', 8080)
+resp = sip.login(uid='sipclient',
+ pwd='c0n1fi3', locn='fawcett laptop')
+pprint(resp)
+pprint(sip.status())
+
+pprint(sip.send(PATRON_INFO, PATRON_INFO_RESP,
+ {'patron':'21862000380830',
+ 'startitem':1, 'enditem':2}))
+
+pprint(sip.send(ITEM_INFORMATION, ITEM_INFO_RESP,
+ {'item': '31862017122801'}))
Deleted: servres/trunk/sip/py/README
===================================================================
--- servres/trunk/sip/py/README 2009-04-03 01:31:27 UTC (rev 248)
+++ servres/trunk/sip/py/README 2009-04-03 01:31:34 UTC (rev 249)
@@ -1,9 +0,0 @@
-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.
Deleted: servres/trunk/sip/py/sipclient.py
===================================================================
--- servres/trunk/sip/py/sipclient.py 2009-04-03 01:31:27 UTC (rev 248)
+++ servres/trunk/sip/py/sipclient.py 2009-04-03 01:31:34 UTC (rev 249)
@@ -1,433 +0,0 @@
-# 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
-
-# some common fields
-
-
-fld_localtime = localtime('localtime')
-fld_INST_ID = field('inst', FID_INST_ID)
-fld_ITEM_ID = field('item', FID_ITEM_ID)
-fld_PATRON_ID = field('patron', FID_PATRON_ID)
-ofld_TERMINAL_PWD = optfield('termpwd', FID_TERMINAL_PWD)
-fld_proto_version = charfield('version', default='2.00')
-ofld_print_line = optfield('print_line', FID_PRINT_LINE)
-ofld_screen_msg = optfield('screenmsg', FID_SCREEN_MSG)
-
-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'),
- fld_proto_version),
-
- 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'),
- fld_localtime,
- charfield('protocol', default='2.00'),
- fld_INST_ID,
- optfield('instname', FID_LIBRARY_NAME),
- field('supported', FID_SUPPORTED_MSGS),
- optfield('ttylocn', FID_TERMINAL_LOCN),
- ofld_screen_msg,
- ofld_print_line),
- PATRON_INFO : message(
- PATRON_INFO,
- charfield('lang', width=3, default=1),
- fld_localtime,
- charfield('holditemsreq', default='Y '),
- fld_INST_ID,
- fld_PATRON_ID,
- ofld_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),
- fld_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),
- fld_INST_ID,
- ofld_screen_msg,
- ofld_print_line,
- optfield('instname', FID_LIBRARY_NAME),
- fld_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,
- fld_localtime,
- field('inst', FID_INST_ID),
- field('patron', FID_PATRON_ID)),
-
- END_SESSION_RESP : message(
- END_SESSION_RESP,
- yn('session_ended'),
- fld_localtime,
- fld_INST_ID,
- fld_PATRON_ID,
- ofld_print_line,
- ofld_screen_msg),
-
- ITEM_INFORMATION : message(
- ITEM_INFORMATION,
- fld_localtime,
- fld_INST_ID,
- fld_ITEM_ID,
- ofld_TERMINAL_PWD),
-
- ITEM_INFO_RESP : message(
- ITEM_INFO_RESP,
- charfield('circstat', width=2),
- charfield('security', width=2),
- charfield('feetype', width=2),
- fld_localtime,
- fld_ITEM_ID,
- field('title', FID_TITLE_ID),
- optfield('mediatype', FID_MEDIA_TYPE),
- optfield('perm_locn', FID_PERM_LOCN),
- optfield('current_locn', FID_CURRENT_LOCN),
- optfield('item_props', FID_ITEM_PROPS),
- optfield('currency', FID_CURRENCY),
- optfield('fee', FID_FEE_AMT),
- optfield('owner', FID_OWNER),
- optfield('hold_queue_len', FID_HOLD_QUEUE_LEN),
- optfield('due_date', FID_DUE_DATE),
-
- optfield('recall_date', FID_RECALL_DATE),
- optfield('hold_pickup_date', FID_HOLD_PICKUP_DATE),
- ofld_screen_msg,
- ofld_print_line),
-
- 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)
-
-
-# ------------------------------------------------------------
-# Test code.
-
-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',
- 'startitem':1, 'enditem':2}))
-
- # these are items from openncip's test database.
- item_ids = ['1565921879', '0440242746', '660']
- bad_ids = ['xx' + i for i in item_ids]
- for item in (item_ids + bad_ids):
- result = sip.send(ITEM_INFORMATION, ITEM_INFO_RESP,
- {'item':item})
- print '%-12s: %s' % (item, result['title'] or '????')
-
- pprint(sip.send(END_PATRON_SESSION, END_SESSION_RESP,
- {'patron':'scclient',
- 'inst':'UWOLS'}))
-
-
Deleted: servres/trunk/sip/py/sipconstants.py
===================================================================
--- servres/trunk/sip/py/sipconstants.py 2009-04-03 01:31:27 UTC (rev 248)
+++ servres/trunk/sip/py/sipconstants.py 2009-04-03 01:31:34 UTC (rev 249)
@@ -1,168 +0,0 @@
-# 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