diff --git a/.gitignore b/.gitignore
index 9bec050..87bf718 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@
 __pycache__/
 *.py[cod]
 *$py.class
+*.egg
 *.egg-info/
 .eggs/
 .tox/
diff --git a/LICENSE.txt b/LICENSE.txt
index 65c5ca8..11b3d94 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,165 +1,201 @@
-                   GNU LESSER GENERAL PUBLIC LICENSE
-                       Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-
-  This version of the GNU Lesser General Public License incorporates
-the terms and conditions of version 3 of the GNU General Public
-License, supplemented by the additional permissions listed below.
-
-  0. Additional Definitions.
-
-  As used herein, "this License" refers to version 3 of the GNU Lesser
-General Public License, and the "GNU GPL" refers to version 3 of the GNU
-General Public License.
-
-  "The Library" refers to a covered work governed by this License,
-other than an Application or a Combined Work as defined below.
-
-  An "Application" is any work that makes use of an interface provided
-by the Library, but which is not otherwise based on the Library.
-Defining a subclass of a class defined by the Library is deemed a mode
-of using an interface provided by the Library.
-
-  A "Combined Work" is a work produced by combining or linking an
-Application with the Library.  The particular version of the Library
-with which the Combined Work was made is also called the "Linked
-Version".
-
-  The "Minimal Corresponding Source" for a Combined Work means the
-Corresponding Source for the Combined Work, excluding any source code
-for portions of the Combined Work that, considered in isolation, are
-based on the Application, and not on the Linked Version.
-
-  The "Corresponding Application Code" for a Combined Work means the
-object code and/or source code for the Application, including any data
-and utility programs needed for reproducing the Combined Work from the
-Application, but excluding the System Libraries of the Combined Work.
-
-  1. Exception to Section 3 of the GNU GPL.
-
-  You may convey a covered work under sections 3 and 4 of this License
-without being bound by section 3 of the GNU GPL.
-
-  2. Conveying Modified Versions.
-
-  If you modify a copy of the Library, and, in your modifications, a
-facility refers to a function or data to be supplied by an Application
-that uses the facility (other than as an argument passed when the
-facility is invoked), then you may convey a copy of the modified
-version:
-
-   a) under this License, provided that you make a good faith effort to
-   ensure that, in the event an Application does not supply the
-   function or data, the facility still operates, and performs
-   whatever part of its purpose remains meaningful, or
-
-   b) under the GNU GPL, with none of the additional permissions of
-   this License applicable to that copy.
-
-  3. Object Code Incorporating Material from Library Header Files.
-
-  The object code form of an Application may incorporate material from
-a header file that is part of the Library.  You may convey such object
-code under terms of your choice, provided that, if the incorporated
-material is not limited to numerical parameters, data structure
-layouts and accessors, or small macros, inline functions and templates
-(ten or fewer lines in length), you do both of the following:
-
-   a) Give prominent notice with each copy of the object code that the
-   Library is used in it and that the Library and its use are
-   covered by this License.
-
-   b) Accompany the object code with a copy of the GNU GPL and this license
-   document.
-
-  4. Combined Works.
-
-  You may convey a Combined Work under terms of your choice that,
-taken together, effectively do not restrict modification of the
-portions of the Library contained in the Combined Work and reverse
-engineering for debugging such modifications, if you also do each of
-the following:
-
-   a) Give prominent notice with each copy of the Combined Work that
-   the Library is used in it and that the Library and its use are
-   covered by this License.
-
-   b) Accompany the Combined Work with a copy of the GNU GPL and this license
-   document.
-
-   c) For a Combined Work that displays copyright notices during
-   execution, include the copyright notice for the Library among
-   these notices, as well as a reference directing the user to the
-   copies of the GNU GPL and this license document.
-
-   d) Do one of the following:
-
-       0) Convey the Minimal Corresponding Source under the terms of this
-       License, and the Corresponding Application Code in a form
-       suitable for, and under terms that permit, the user to
-       recombine or relink the Application with a modified version of
-       the Linked Version to produce a modified Combined Work, in the
-       manner specified by section 6 of the GNU GPL for conveying
-       Corresponding Source.
-
-       1) Use a suitable shared library mechanism for linking with the
-       Library.  A suitable mechanism is one that (a) uses at run time
-       a copy of the Library already present on the user's computer
-       system, and (b) will operate properly with a modified version
-       of the Library that is interface-compatible with the Linked
-       Version.
-
-   e) Provide Installation Information, but only if you would otherwise
-   be required to provide such information under section 6 of the
-   GNU GPL, and only to the extent that such information is
-   necessary to install and execute a modified version of the
-   Combined Work produced by recombining or relinking the
-   Application with a modified version of the Linked Version. (If
-   you use option 4d0, the Installation Information must accompany
-   the Minimal Corresponding Source and Corresponding Application
-   Code. If you use option 4d1, you must provide the Installation
-   Information in the manner specified by section 6 of the GNU GPL
-   for conveying Corresponding Source.)
-
-  5. Combined Libraries.
-
-  You may place library facilities that are a work based on the
-Library side by side in a single library together with other library
-facilities that are not Applications and are not covered by this
-License, and convey such a combined library under terms of your
-choice, if you do both of the following:
-
-   a) Accompany the combined library with a copy of the same work based
-   on the Library, uncombined with any other library facilities,
-   conveyed under the terms of this License.
-
-   b) Give prominent notice with the combined library that part of it
-   is a work based on the Library, and explaining where to find the
-   accompanying uncombined form of the same work.
-
-  6. Revised Versions of the GNU Lesser General Public License.
-
-  The Free Software Foundation may publish revised and/or new versions
-of the GNU Lesser General Public License from time to time. Such new
-versions will be similar in spirit to the present version, but may
-differ in detail to address new problems or concerns.
-
-  Each version is given a distinguishing version number. If the
-Library as you received it specifies that a certain numbered version
-of the GNU Lesser General Public License "or any later version"
-applies to it, you have the option of following the terms and
-conditions either of that published version or of any later version
-published by the Free Software Foundation. If the Library as you
-received it does not specify a version number of the GNU Lesser
-General Public License, you may choose any version of the GNU Lesser
-General Public License ever published by the Free Software Foundation.
-
-  If the Library as you received it specifies that a proxy can decide
-whether future versions of the GNU Lesser General Public License shall
-apply, that proxy's public statement of acceptance of any version is
-permanent authorization for you to choose that version for the
-Library.
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2016 FundersClub Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/lockbox/exceptions.py b/lockbox/exceptions.py
index c414d89..5d5c9db 100644
--- a/lockbox/exceptions.py
+++ b/lockbox/exceptions.py
@@ -1,19 +1,3 @@
-# This file is part of bai-lockbox.
-
-# bai-lockbox is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-
-# bai-lockbox 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 Lesser General Public License for more details.
-
-# You should have received a copy of the GNU Lesser General Public
-# License along with bai-lockbox.  If not, see
-# <http://www.gnu.org/licenses/>.
-
 class LockboxError(Exception):
     pass
 
@@ -29,7 +13,7 @@ class LockboxParseError(LockboxError):
     '''Base exception for problems related to reading a BAI Lockbox
     record.
     '''
-    raw_line = ''
+    pass
 
 
 class LockboxConsistencyError(LockboxError):
diff --git a/lockbox/parser.py b/lockbox/parser.py
index 9eb6120..43407fd 100644
--- a/lockbox/parser.py
+++ b/lockbox/parser.py
@@ -1,19 +1,3 @@
-# This file is part of bai-lockbox.
-
-# bai-lockbox is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-
-# bai-lockbox 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 Lesser General Public License for more details.
-
-# You should have received a copy of the GNU Lesser General Public
-# License along with bai-lockbox.  If not, see
-# <http://www.gnu.org/licenses/>.
-
 import six
 import sys
 
@@ -42,6 +26,8 @@ class Check(object):
         self.number = detail.check_number
         self.amount = detail.check_amount
         self.memo = detail.memo
+        self.sender_routing_number=detail.transit_routing_number
+        self.sender_account_number=detail.dd_account_number
 
 
 class LockboxDetail(object):
@@ -66,8 +52,8 @@ class LockboxDetail(object):
     def __getattr__(self, attr):
         if attr in dir(self.record):
             return getattr(self.record, attr)
-
-        return self.get(attr)
+        else:
+            return super(LockboxDetail, self).__getattr__(attr)
 
 
 class LockboxBatch(object):
@@ -78,13 +64,8 @@ class LockboxBatch(object):
 
     @property
     def checks(self):
-        checks = []
-
-        for detail in self.details:
-            checks.append(Check(detail))
+        return [Check(d) for d in self.details]
 
-        return checks
-            
     def validate(self):
         if self.summary is None:
             raise LockboxParseError(
@@ -201,7 +182,7 @@ class LockboxFile(object):
         self.destination_trailer_record = None
 
         self.cur_lockbox = None
-    
+
     @property
     def checks(self):
         checks = []
@@ -210,7 +191,7 @@ class LockboxFile(object):
             checks.extend(lockbox.checks)
 
         return checks
-    
+
     def validate(self):
         for lockbox in self.lockboxes:
             lockbox.validate()
@@ -262,51 +243,49 @@ class LockboxFile(object):
         else:
             self.cur_lockbox.add_record(record)
 
-
-def read_lockbox_lines(lines):
-    lockbox_file = LockboxFile()
-
-    for line, line_num in zip(lines, range(1, len(lines)+1)):
-        try:
-            if line[0] == str(LockboxBatchTotalRecord.RECORD_TYPE_NUM):
-                rec = LockboxBatchTotalRecord(line)
-            elif line[0] == str(LockboxDestinationTrailerRecord.RECORD_TYPE_NUM):
-                rec = LockboxDestinationTrailerRecord(line)
-            elif line[0] == str(LockboxDetailHeader.RECORD_TYPE_NUM):
-                rec = LockboxDetailHeader(line)
-            elif line[0] == str(LockboxDetailOverflowRecord.RECORD_TYPE_NUM):
-                rec = LockboxDetailOverflowRecord(line)
-            elif line[0] == str(LockboxDetailRecord.RECORD_TYPE_NUM):
-                rec = LockboxDetailRecord(line)
-            elif line[0] == str(LockboxImmediateAddressHeader.RECORD_TYPE_NUM):
-                rec = LockboxImmediateAddressHeader(line)
-            elif line[0] == str(LockboxServiceRecord.RECORD_TYPE_NUM):
-                rec = LockboxServiceRecord(line)
-            elif line[0] == str(LockboxServiceTotalRecord.RECORD_TYPE_NUM):
-                rec = LockboxServiceTotalRecord(line)
-            else:
-                raise LockboxParseError(
-                    'unknown record type {}'.format(line[0])
+    @classmethod
+    def from_lines(cls, lines):
+        lines = [l.strip() for l in lines]
+        lockbox_file = cls()
+
+        for line_num, line in enumerate(lines, start=1):
+            try:
+                rec_type = int(line[0])
+                record_type_to_constructor = {
+                    LockboxBatchTotalRecord.RECORD_TYPE_NUM: LockboxBatchTotalRecord,
+                    LockboxDestinationTrailerRecord.RECORD_TYPE_NUM: LockboxDestinationTrailerRecord,
+                    LockboxDetailHeader.RECORD_TYPE_NUM: LockboxDetailHeader,
+                    LockboxDetailOverflowRecord.RECORD_TYPE_NUM: LockboxDetailOverflowRecord,
+                    LockboxDetailRecord.RECORD_TYPE_NUM: LockboxDetailRecord,
+                    LockboxImmediateAddressHeader.RECORD_TYPE_NUM: LockboxImmediateAddressHeader,
+                    LockboxServiceRecord.RECORD_TYPE_NUM: LockboxServiceRecord,
+                    LockboxServiceTotalRecord.RECORD_TYPE_NUM: LockboxServiceTotalRecord,
+                }
+
+                if rec_type not in record_type_to_constructor:
+                    raise LockboxParseError(
+                        'unknown record type {}'.format(rec_type)
+                    )
+
+                rec = record_type_to_constructor[rec_type](line)
+                lockbox_file.add_record(rec)
+            except Exception as e:
+                if not isinstance(e, LockboxError):
+                    raise
+
+                # if this is some lockbox-related exception,create a new
+                # exception of the same kind we caught, bet prepend the
+                # current line number to it so we know where to look while
+                # troubleshooting
+                six.reraise(
+                    type(e),
+                    'Line {}: {} ("{}")'.format(line_num, str(e), line),
+                    sys.exc_info()[2]
                 )
 
-            lockbox_file.add_record(rec)
-        except Exception as e:
-            if not isinstance(e, LockboxError):
-                raise
-
-            # if this is some lockbox-related exception,create a new
-            # exception of the same kind we caught, bet prepend the
-            # current line number to it so we know where to look while
-            # troubleshooting
-            six.reraise(
-                type(e),
-                'Line {}: {} ("{}")'.format(line_num, str(e), line),
-                sys.exc_info()[2]
-            )
-
-    lockbox_file.validate()
-    return lockbox_file
-
+        lockbox_file.validate()
+        return lockbox_file
 
-def read_lockbox_file(inf):
-    return read_lockbox_lines([line.strip() for line in inf.readlines()])
+    @classmethod
+    def from_file(cls, inf):
+        return LockboxFile.from_lines(inf.readlines())
diff --git a/lockbox/records.py b/lockbox/records.py
index 71fd5c9..d491c87 100644
--- a/lockbox/records.py
+++ b/lockbox/records.py
@@ -1,25 +1,16 @@
-# This file is part of bai-lockbox.
-
-# bai-lockbox is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-
-# bai-lockbox 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 Lesser General Public License for more details.
-
-# You should have received a copy of the GNU Lesser General Public
-# License along with bai-lockbox.  If not, see
-# <http://www.gnu.org/licenses/>.
-
 import datetime
 import re
+import six
 
 from .exceptions import LockboxDefinitionError, LockboxParseError
 
 
+class LockboxFieldType(object):
+    Numeric = 'numeric'
+    Alphanumeric = 'alphanumeric'
+    Blank = 'blank'
+
+
 class LockboxBaseRecord(object):
     '''The format of the field 'fields' should be an array of dicts like
     this:
@@ -30,11 +21,12 @@ class LockboxBaseRecord(object):
       ...
     }
 
-    Valid types: 'alphanumeric', 'numeric', 'blank'
+    Valid types are listed inside the LockboxFieldType class.
 
     Note: The record type which is determined by first character of a
     line is defined by setting MAX_RECORD_LENGTH in a derrived class
     rather than by adding it to the 'fields' field.
+
     '''
 
     MAX_RECORD_LENGTH = 104
@@ -56,7 +48,7 @@ class LockboxBaseRecord(object):
             # we can only parse if there are actually fields defined
             self.fields['record_type'] = {
                 'location': (0,1),
-                'type': 'numeric',
+                'type':  LockboxFieldType.Numeric,
             }
             self._parse()
 
@@ -67,7 +59,7 @@ class LockboxBaseRecord(object):
             # has already been performed by the regexps in _parse(),
             # so at this point we just create any missing fields by
             # doing self.my_field = self._my_field_raw
-            for field_name, field_def in self.fields.items():
+            for field_name, field_def in six.iteritems(self.fields):
                 if hasattr(self, field_name):
                     continue
 
@@ -75,14 +67,14 @@ class LockboxBaseRecord(object):
 
                 raw_field_val = (
                     None
-                    if field_def['type'] == 'blank'
+                    if field_def['type'] ==  LockboxFieldType.Blank
                     else getattr(self, raw_field_name, None)
                 )
 
                 setattr(self, field_name, raw_field_val)
 
     def _parse(self):
-        for field_name, field_def in self.fields.items():
+        for field_name, field_def in six.iteritems(self.fields):
             raw_field_name = '_{}_raw'.format(field_name)
             if hasattr(self, field_name):
                 raise AttributeError(
@@ -91,15 +83,14 @@ class LockboxBaseRecord(object):
                     )
                 )
 
-            start_col = field_def['location'][0]
-            end_col = field_def['location'][1]
+            start_col, end_col = field_def['location']
             raw_field = self.raw_record_text[start_col:end_col]
 
-            if field_def['type'] == 'alphanumeric':
+            if field_def['type'] ==  LockboxFieldType.Alphanumeric:
                 patt = re.compile(r'^[ A-Z0-9]+$')
-            elif field_def['type'] == 'numeric':
+            elif field_def['type'] ==  LockboxFieldType.Numeric:
                 patt = re.compile(r'^[0-9]+$')
-            elif field_def['type'] == 'blank':
+            elif field_def['type'] ==  LockboxFieldType.Blank:
                 patt = re.compile(r'^\s*$')
             else:
                 raise LockboxDefinitionError(
@@ -186,12 +177,12 @@ class LockboxImmediateAddressHeader(LockboxBaseRecord):
     RECORD_TYPE_NUM = 1
 
     fields = {
-        'priority_code': { 'location': (1, 3), 'type': 'numeric' },
-        'destination_id': { 'location': (3, 13), 'type': 'alphanumeric' },
-        'originating_trn': { 'location': (13, 23), 'type': 'numeric' },
-        'processing_date': { 'location': (23, 29), 'type': 'numeric' },
-        'processing_time': { 'location': (29, 33), 'type': 'numeric' },
-        'filler': {'location': (33, 104), 'type': 'blank' },
+        'priority_code': { 'location': (1, 3), 'type':  LockboxFieldType.Numeric },
+        'destination_id': { 'location': (3, 13), 'type':  LockboxFieldType.Alphanumeric },
+        'originating_trn': { 'location': (13, 23), 'type':  LockboxFieldType.Numeric },
+        'processing_date': { 'location': (23, 29), 'type':  LockboxFieldType.Numeric },
+        'processing_time': { 'location': (29, 33), 'type':  LockboxFieldType.Numeric },
+        'filler': {'location': (33, 104), 'type':  LockboxFieldType.Blank },
     }
 
     def validate(self):
@@ -205,14 +196,14 @@ class LockboxServiceRecord(LockboxBaseRecord):
     fields = {
         'ultimate_dest_and_origin': {
             'location': (1, 21),
-            'type': 'alphanumeric',
+            'type':  LockboxFieldType.Alphanumeric,
         },
-        'ref_code':  {'location': (21, 31), 'type': 'numeric'},
-        'service_type': {'location': (31, 34), 'type': 'numeric'},
-        'record_size': {'location': (34, 37), 'type': 'numeric'},
-        'blocking_factor': {'location': (37, 41), 'type': 'numeric'},
-        'format_code': {'location': (41,42), 'type': 'numeric'},
-        'filler': {'location': (42, 104), 'type': 'blank'},
+        'ref_code':  {'location': (21, 31), 'type':  LockboxFieldType.Numeric},
+        'service_type': {'location': (31, 34), 'type':  LockboxFieldType.Numeric},
+        'record_size': {'location': (34, 37), 'type':  LockboxFieldType.Numeric},
+        'blocking_factor': {'location': (37, 41), 'type':  LockboxFieldType.Numeric},
+        'format_code': {'location': (41,42), 'type':  LockboxFieldType.Numeric},
+        'filler': {'location': (42, 104), 'type':  LockboxFieldType.Blank},
     }
 
 
@@ -220,15 +211,15 @@ class LockboxDetailHeader(LockboxBaseRecord):
     RECORD_TYPE_NUM = 5
 
     fields = {
-        'batch_number':  { 'location': (1, 4), 'type': 'numeric' },
-        'ref_code':  { 'location': (4, 7, ), 'type': 'numeric' },
-        'lockbox_number':  { 'location': (7, 14), 'type': 'numeric' },
-        'deposit_date':  { 'location': (14, 20), 'type': 'numeric' },
+        'batch_number':  { 'location': (1, 4), 'type':  LockboxFieldType.Numeric },
+        'ref_code':  { 'location': (4, 7, ), 'type':  LockboxFieldType.Numeric },
+        'lockbox_number':  { 'location': (7, 14), 'type':  LockboxFieldType.Numeric },
+        'deposit_date':  { 'location': (14, 20), 'type':  LockboxFieldType.Numeric },
         'ultimate_dest_and_origin':  {
             'location': (20, 40),
-            'type': 'alphanumeric',
+            'type':  LockboxFieldType.Alphanumeric,
         },
-        'filler': {'location': (40, 104), 'type': 'blank' },
+        'filler': {'location': (40, 104), 'type':  LockboxFieldType.Blank },
     }
 
     def validate(self):
@@ -239,16 +230,16 @@ class LockboxDetailRecord(LockboxBaseRecord):
     RECORD_TYPE_NUM = 6
 
     fields = {
-        'batch_number': { 'location': (1, 4), 'type': 'numeric' },
-        'item_number': { 'location': (4, 7), 'type': 'numeric' },
-        'check_amount': { 'location': (7, 17), 'type': 'numeric' },
-        'transit_routing_number': { 'location': (17, 26), 'type': 'numeric' },
-        'dd_account_number': { 'location': (26, 36), 'type': 'numeric' },
-        'check_number': { 'location': (36, 46), 'type': 'numeric' },
-        'check_date': { 'location': (46, 52), 'type': 'numeric' },
-        'remitter_name': { 'location': (52, 82), 'type': 'alphanumeric' },
-        'payee_name': { 'location': (82, 102), 'type': 'alphanumeric' },
-        'filler': {'location': (102, 104), 'type': 'blank' },
+        'batch_number': { 'location': (1, 4), 'type':  LockboxFieldType.Numeric },
+        'item_number': { 'location': (4, 7), 'type':  LockboxFieldType.Numeric },
+        'check_amount': { 'location': (7, 17), 'type':  LockboxFieldType.Numeric },
+        'transit_routing_number': { 'location': (17, 26), 'type':  LockboxFieldType.Numeric },
+        'dd_account_number': { 'location': (26, 36), 'type':  LockboxFieldType.Numeric },
+        'check_number': { 'location': (36, 46), 'type':  LockboxFieldType.Numeric },
+        'check_date': { 'location': (46, 52), 'type':  LockboxFieldType.Numeric },
+        'remitter_name': { 'location': (52, 82), 'type':  LockboxFieldType.Alphanumeric },
+        'payee_name': { 'location': (82, 102), 'type':  LockboxFieldType.Alphanumeric },
+        'filler': {'location': (102, 104), 'type':  LockboxFieldType.Blank },
     }
 
     def validate(self):
@@ -269,13 +260,13 @@ class LockboxDetailOverflowRecord(LockboxBaseRecord):
     RECORD_TYPE_NUM = 4
 
     fields = {
-        'batch_number': { 'location': (1, 4), 'type': 'numeric' },
-        'item_number': { 'location': (4, 7), 'type': 'numeric' },
-        'overflow_record_type': { 'location': (7, 8), 'type': 'numeric' },
-        'overflow_sequence_number': { 'location': (8, 10), 'type': 'numeric' },
-        'overflow_indicator': { 'location': (10, 11), 'type': 'numeric' },
-        'memo_line': { 'location': (11, 41), 'type': 'alphanumeric' },
-        'filler': {'location': (41, 104), 'type': 'blank' },
+        'batch_number': { 'location': (1, 4), 'type':  LockboxFieldType.Numeric },
+        'item_number': { 'location': (4, 7), 'type':  LockboxFieldType.Numeric },
+        'overflow_record_type': { 'location': (7, 8), 'type':  LockboxFieldType.Numeric },
+        'overflow_sequence_number': { 'location': (8, 10), 'type':  LockboxFieldType.Numeric },
+        'overflow_indicator': { 'location': (10, 11), 'type':  LockboxFieldType.Numeric },
+        'memo_line': { 'location': (11, 41), 'type':  LockboxFieldType.Alphanumeric },
+        'filler': {'location': (41, 104), 'type':  LockboxFieldType.Blank },
     }
 
     def validate(self):
@@ -289,16 +280,16 @@ class LockboxBatchTotalRecord(LockboxBaseRecord):
     RECORD_TYPE_NUM = 7
 
     fields = {
-        'batch_number': { 'location': (1, 4), 'type': 'numeric' },
-        'item_number': { 'location': (4, 7), 'type': 'numeric' },
-        'lockbox_number': { 'location': (7, 14), 'type': 'numeric' },
-        'deposit_date': { 'location': (14, 20), 'type': 'numeric' },
+        'batch_number': { 'location': (1, 4), 'type':  LockboxFieldType.Numeric },
+        'item_number': { 'location': (4, 7), 'type':  LockboxFieldType.Numeric },
+        'lockbox_number': { 'location': (7, 14), 'type':  LockboxFieldType.Numeric },
+        'deposit_date': { 'location': (14, 20), 'type':  LockboxFieldType.Numeric },
         'total_number_remittances': {
             'location': (20, 23),
-            'type': 'numeric'
+            'type':  LockboxFieldType.Numeric
         },
-        'check_dollar_total': { 'location': (23, 33), 'type': 'numeric' },
-        'filler': {'location': (33, 104), 'type': 'blank' },
+        'check_dollar_total': { 'location': (23, 33), 'type':  LockboxFieldType.Numeric },
+        'filler': {'location': (33, 104), 'type':  LockboxFieldType.Blank },
     }
 
     def validate(self):
@@ -313,13 +304,13 @@ class LockboxServiceTotalRecord(LockboxBaseRecord):
     RECORD_TYPE_NUM = 8
 
     fields = {
-        'batch_number': { 'location': (1, 4), 'type': 'numeric' },
-        'item_number': { 'location': (4, 7), 'type': 'numeric' },
-        'lockbox_number': { 'location': (7, 14), 'type': 'numeric' },
-        'deposit_date': { 'location': (14, 20), 'type': 'numeric' },
-        'total_num_checks': { 'location': (20, 24), 'type': 'numeric' },
-        'check_dollar_total': { 'location': (24, 34), 'type': 'numeric' },
-        'filler': {'location': (34, 104), 'type': 'blank' },
+        'batch_number': { 'location': (1, 4), 'type':  LockboxFieldType.Numeric },
+        'item_number': { 'location': (4, 7), 'type':  LockboxFieldType.Numeric },
+        'lockbox_number': { 'location': (7, 14), 'type':  LockboxFieldType.Numeric },
+        'deposit_date': { 'location': (14, 20), 'type':  LockboxFieldType.Numeric },
+        'total_num_checks': { 'location': (20, 24), 'type':  LockboxFieldType.Numeric },
+        'check_dollar_total': { 'location': (24, 34), 'type':  LockboxFieldType.Numeric },
+        'filler': {'location': (34, 104), 'type':  LockboxFieldType.Blank },
     }
 
     def validate(self):
@@ -333,8 +324,8 @@ class LockboxDestinationTrailerRecord(LockboxBaseRecord):
     RECORD_TYPE_NUM = 9
 
     fields = {
-        'total_num_records': { 'location': (1, 7), 'type': 'numeric' },
-        'filler': {'location': (7, 104), 'type': 'blank' },
+        'total_num_records': { 'location': (1, 7), 'type':  LockboxFieldType.Numeric },
+        'filler': {'location': (7, 104), 'type':  LockboxFieldType.Blank },
     }
 
     def validate(self):
diff --git a/setup.py b/setup.py
index aebdbc4..4cbeca1 100644
--- a/setup.py
+++ b/setup.py
@@ -9,15 +9,13 @@ setup(
     version='0.0.1',
     packages=find_packages(exclude=['docs', 'tests']),
     install_requires=[
-        'django-jsonfield>=0.8.11',
         'six',
-        'django-apptemplates',
     ],
     test_suite='nose.collector',
     tests_require=['nose', 'coverage'],
     include_package_data=True,
-    license='LGPLv3',
-    description='An elegant solution for keeping a relational log of chronological events in a Django application.',
+    license='Apache License 2.0',
+    description='A library for parsing files in the BAI lockbox format.',
     url='https://www.github.com/FundersClub/bai-lockbox',
     author='Jon Friedman / FundersClub Inc.',
     author_email='jon@fundersclub.com',
@@ -27,8 +25,8 @@ setup(
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
         'Programming Language :: Python :: 3.4',
-        'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
+        'License :: OSI Approved :: Apache Software License',
         'Intended Audience :: Developers',
-    'Intended Audience :: Financial and Insurance Industry'
+        'Intended Audience :: Financial and Insurance Industry'
     ],
 )
diff --git a/tox.ini b/tox.ini
index 4c556c8..892779a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,8 +2,7 @@
 envlist =
     py27
     py34
-    pypy
     
 [testenv]
 deps = -rrequirements.txt
-commands = python setup.py nosetests
\ No newline at end of file
+commands = python setup.py nosetests

Code review
* [x] https://github.com/FundersClub/bai-lockbox/blob/master/setup.py#L12 - are all of these used?
* [x] https://github.com/FundersClub/bai-lockbox/blob/master/setup.py#L20 - incorrect
* [x] https://github.com/FundersClub/bai-lockbox/blob/master/lockbox/exceptions.py#L32 - what is this?
* [x] https://github.com/FundersClub/bai-lockbox/blob/master/lockbox/parser.py#L70 - Where is `.get()` defined?
* [x] https://github.com/FundersClub/bai-lockbox/blob/master/lockbox/parser.py#L81 - Why not something like `return map(Check, self.details)` (or good ol' list comprehension)
* [x] https://github.com/FundersClub/bai-lockbox/blob/master/lockbox/parser.py#L269 - Look up `enumerate()`
* [x] https://github.com/FundersClub/bai-lockbox/blob/master/lockbox/parser.py#L266 and https://github.com/FundersClub/bai-lockbox/blob/master/lockbox/parser.py#L311 - I would make these `@classmethod`s in `LockboxFile` and call them something like `from_lines` and `from_file`
* [x] https://github.com/FundersClub/bai-lockbox/blob/master/lockbox/parser.py#L312 - I would make the `read_lockbox_lines` perform the `strip()` as it is always desired
* [x] https://github.com/FundersClub/bai-lockbox/blob/master/lockbox/parser.py#L271 - Instead of all those `str()` and repeating `line[0]` why not: 1) convert `int(line[0])` and store it in a local variable and then compare. 2) Create a dictionary that maps the record number to the class. (see *1 below)
* [x] https://github.com/FundersClub/bai-lockbox/blob/master/lockbox/records.py#L70 - Please use `iteritems`, no need to create an in-memory list
* [x] https://github.com/FundersClub/bai-lockbox/blob/master/lockbox/records.py#L85 - `iteritems()`
* [x] https://github.com/FundersClub/bai-lockbox/blob/master/lockbox/records.py#L94 - `start_col, end_col = field_def['location']`
* [x] I think we should make field types a constant and then use that. Reduces chances for unnoticed typos. (see *2 below)


----
*1
```
class FieldType(object):
    Numeric = 'numeric'
    ...
``` 
----
*2  
```
record_types = {
    LockboxBatchTotalRecord.RECORD_TYPE_NUM: LockboxBatchTotalRecord,
    ....
}

record_cls = record_types.get(int(line[0]))
if not record_cls: ...
rec = record_cls(line)
diff --git a/lockbox/tests/test_parser.py b/lockbox/tests/test_parser.py
index 262f87d..1a2649a 100644
--- a/lockbox/tests/test_parser.py
+++ b/lockbox/tests/test_parser.py
@@ -1,25 +1,9 @@
-# This file is part of bai-lockbox.
-
-# bai-lockbox is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-
-# bai-lockbox 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 Lesser General Public License for more details.
-
-# You should have received a copy of the GNU Lesser General Public
-# License along with bai-lockbox.  If not, see
-# <http://www.gnu.org/licenses/>.
-
 import datetime
 import os
 
 from unittest import TestCase
 
-from lockbox.parser import read_lockbox_file, read_lockbox_lines
+from lockbox.parser import LockboxFile
 
 
 class TestLockboxParser(TestCase):
@@ -42,7 +26,7 @@ class TestLockboxParser(TestCase):
         self.empty_lockbox_lines = [l.strip() for l in open(empty_lockbox_path, 'r').readlines()]
 
     def test_parsing_valid_file(self):
-        lockbox_file = read_lockbox_lines(self.valid_lockbox_lines)
+        lockbox_file = LockboxFile.from_lines(self.valid_lockbox_lines)
 
         self.assertEqual(len(lockbox_file.checks), 1)
 
@@ -55,6 +39,6 @@ class TestLockboxParser(TestCase):
         self.assertEqual(check.memo, 'CE554')
 
     def test_parsing_file_with_no_checks(self):
-        lockbox_file = read_lockbox_lines(self.empty_lockbox_lines)
+        lockbox_file = LockboxFile.from_lines(self.empty_lockbox_lines)
 
         self.assertEqual(len(lockbox_file.checks), 0)
diff --git a/lockbox/tests/test_records.py b/lockbox/tests/test_records.py
index de5f1b3..19a52c5 100644
--- a/lockbox/tests/test_records.py
+++ b/lockbox/tests/test_records.py
@@ -1,19 +1,3 @@
-# This file is part of bai-lockbox.
-
-# bai-lockbox is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-
-# bai-lockbox 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 Lesser General Public License for more details.
-
-# You should have received a copy of the GNU Lesser General Public
-# License along with bai-lockbox.  If not, see
-# <http://www.gnu.org/licenses/>.
-
 import datetime
 
 from unittest import TestCase

pip install -e .[dev]
parse_log_pytest
requirements.txt
nose pytest
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
asgiref==3.8.1
-e git+https://github.com/FundersClub/bai-lockbox.git@c7e4fbc207b21953e4741ba55f787eaa108e55fb#egg=bai_lockbox
Django==4.2.20
django-apptemplates==1.5
django-jsonfield==1.4.1
exceptiongroup==1.2.2
iniconfig==2.1.0
nose==1.3.7
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
six==1.17.0
sqlparse==0.5.3
tomli==2.2.1
typing_extensions==4.13.0

name: bai-lockbox
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - asgiref==3.8.1
      - django==4.2.20
      - django-apptemplates==1.5
      - django-jsonfield==1.4.1
      - exceptiongroup==1.2.2
      - iniconfig==2.1.0
      - nose==1.3.7
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - six==1.17.0
      - sqlparse==0.5.3
      - tomli==2.2.1
      - typing-extensions==4.13.0
prefix: /opt/conda/envs/bai-lockbox

lockbox/tests/test_parser.py::TestLockboxParser::test_parsing_file_with_no_checks lockbox/tests/test_parser.py::TestLockboxParser::test_parsing_valid_file
lockbox/tests/test_records.py::TestRecordDefinitions::test_immediate_address_header lockbox/tests/test_records.py::TestRecordDefinitions::test_invalid_alphanumeric_field lockbox/tests/test_records.py::TestRecordDefinitions::test_invalid_numeric_field lockbox/tests/test_records.py::TestRecordDefinitions::test_lockbox_batch_total_record lockbox/tests/test_records.py::TestRecordDefinitions::test_lockbox_destination_trailer_record lockbox/tests/test_records.py::TestRecordDefinitions::test_lockbox_detail_header lockbox/tests/test_records.py::TestRecordDefinitions::test_lockbox_detail_overflow_record lockbox/tests/test_records.py::TestRecordDefinitions::test_lockbox_detail_record lockbox/tests/test_records.py::TestRecordDefinitions::test_lockbox_service_total_record lockbox/tests/test_records.py::TestRecordDefinitions::test_overlong_record lockbox/tests/test_records.py::TestRecordDefinitions::test_valid_lockbox_service_record
setup.py .gitignore lockbox/exceptions.py lockbox/records.py tox.ini LICENSE.txt lockbox/parser.py
setup.py .gitignore lockbox/exceptions.py lockbox/records.py tox.ini LICENSE.txt lockbox/parser.py