From: gfawcett Date: Fri, 3 Apr 2009 01:31:34 +0000 (+0000) Subject: moved SIP client into conifer.libsystems.sip X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=659e0f680a2025b127e613c24d847955d90650de;p=syrup%2Fmasslnc.git moved SIP client into conifer.libsystems.sip git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@249 6d9bc8c9-1ec2-4278-b937-99fde70a366f --- diff --git a/conifer/libsystems/sip/README b/conifer/libsystems/sip/README new file mode 100644 index 0000000..3a5825f --- /dev/null +++ b/conifer/libsystems/sip/README @@ -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. diff --git a/conifer/libsystems/sip/__init__.py b/conifer/libsystems/sip/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/conifer/libsystems/sip/sipclient.py b/conifer/libsystems/sip/sipclient.py new file mode 100644 index 0000000..b7ceaab --- /dev/null +++ b/conifer/libsystems/sip/sipclient.py @@ -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'})) + + diff --git a/conifer/libsystems/sip/sipconstants.py b/conifer/libsystems/sip/sipconstants.py new file mode 100644 index 0000000..db46694 --- /dev/null +++ b/conifer/libsystems/sip/sipconstants.py @@ -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 diff --git a/conifer/libsystems/sip/siptest1.py b/conifer/libsystems/sip/siptest1.py new file mode 100644 index 0000000..351daf5 --- /dev/null +++ b/conifer/libsystems/sip/siptest1.py @@ -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'})) diff --git a/sip/py/README b/sip/py/README deleted file mode 100644 index 3a5825f..0000000 --- a/sip/py/README +++ /dev/null @@ -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. diff --git a/sip/py/sipclient.py b/sip/py/sipclient.py deleted file mode 100644 index b7ceaab..0000000 --- a/sip/py/sipclient.py +++ /dev/null @@ -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'})) - - diff --git a/sip/py/sipconstants.py b/sip/py/sipconstants.py deleted file mode 100644 index db46694..0000000 --- a/sip/py/sipconstants.py +++ /dev/null @@ -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