summaryrefslogtreecommitdiffstats
path: root/modules/kirbybase.py
blob: c18f1115e0793afda8282b73a15ed2d568d7f059 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
"""Contains classes for a plain-text, client-server dbms.

Classes:
    KirbyBase - database class
    KBError - exceptions

Example:
    from db import *

    db = KirbyBase()
    db.create('plane.tbl', ['name:str', 'country:str', 'speed:int',
     'range:int'])
    db.insert('plane.tbl', ['P-51', 'USA', 403, 1201])
    db.insert('plane.tbl', ['P-38', 'USA', 377, 999])
    db.select('plane.tbl', ['country', 'speed'], ['USA', '>400'])
    db.update('plane.tbl', ['country'], ['USA'], ['United States'],
     ['country'])
    db.delete('plane.tbl', ['speed'], ['<400'])
    db.close()

Author:
    Jamey Cribbs -- jcribbs@twmi.rr.com
    www.netpromi.com

History:
    2003-04-08:  Version 1.0 released.
    2003-04-15:  Version 1.01 released.
        -Added getFieldNames method.
        -Added getFieldTypes method.
        -Added drop method.
        -Added check for existing file in create method.
        -Improved documentation.
    2003-07-16:  Version 1.02 released.
        -KirbyBase now uses distutils for installation.
        -Fixed bug in createTable where field list was getting modified.
        -Fixed bug in insert where values list was getting modified.
        -Added the ability to use special characters in table data such
         as \r, \n, \032, and |.
        -Changed the name of all private methods to start with two
         underscores, thereby following recommended naming conventions.
        -Improved documentation.
        -Added licensing information.
    2003-08-14:  Version 1.3 released.
        -Added len method.
        -Fixed bug in validateMatchCriteria where script was not
         restricting other match criteria if already attempting to match
         by recno.
        -Fixed bug in validateMatchCriteria where script was not checking
         to see if pattern argument was an integer when attempting to
         match by recno.
        -Added ability to pass field values to update and insert using a
         dictionary.
        -Added ability to specify field to sort on and sort direction for
         the results of a select.
        -Changed the way field types are handled internally.  Instead of
         treating them as strings (which is how they are stored) and
         having to constantly 'eval' them to get the type, I decided to
         work with them in their 'native' format.  This should not change
         any of the api or interfaces, EXCEPT for the getFieldTypes
         method, which now returns a list of types, instead of a list of
         strings.  I hope this doesn't screw anyone's programs up.
        -Corrected version number to conform to guidelines in distutils
         documentation.
    2003-08-27:  Version 1.4 released.
        -Added two new database field types:  datetime.date and
         datetime.datetime.  They are stored as strings, but are input and
         output as instances of datetime.date and datetime.datetime
         respectively.  
        -Made a few internal optimizations when running queries that have
         resulted in a 15-20% speed increase when doing large queries or
         updates.
        -Changed the name of all private methods from starting with two
         underscores to starting with one underscore based on a discussion
         in comp.lang.python as to how to properly name private variables.
    2003-09-02:  Version 1.5 released.
        -Changed the way queries are handled internally.  Instead of doing
         an eval to do numeric and datetime comparisons, I changed it to do
         the actual comparison itself.  This resulted in a 40% speed 
         increase on large queries that do comparison expressions.
        -Changed how data is passed between the server and the client in
         client/server mode.  I now use cPickle instead of repr and eval.
         This resulted in an approximately 40% speed increase in 
         client/server operations.
    2004-06-10:  Version 1.5.1 released.
        -Added a new database field type:  boolean.
        -Fixed a bug where KirbyBase was trying to convert an empty table
         field (i.e. '') back into it's native format such as int or float
         and raising an Exception.  Now, if a table field is empty, I just
         append it to the result record as is and dont' try to convert it. 
        -getMatches method will now split each database record only up to
         the number of fields required to satisfy the query.  This should
         save a little query time on large databases with many fields.
        -Added ability to have the getMatches method match string fields
         based on string equality rather than regular expressions.  
    2004-06-22:  Version 1.6 released.
        -On numeric comparisons, you can now specify negative numbers.
        -Fixed a bug where program would crash if you had a space between
         the comparison operator and the number in numeric comparisons in
         select statement.
        -You can select all records by specifying that you want to match
         'recno' against '*'.  
        -Got rid of the last eval in the code.    
        -Modest speed improvement by checking for strings that need to be
         encoded first insteading of just encoding all strings.
        -Added a compatibility layer for declaring field types when 
         creating a table.  If you use the new compatible field types when
         creating a table, you can use the table either from the Python or
         Ruby version of KirbyBase without having to change anything. 
        -Changed the record-counter and deleted-records-counter in the
         header record of the table to be zero padded instead of spaces
         padded. 
    2005-01-30:  Version 1.7 released.
        -***IMPORTANT***
         Changed the default value for the keyword argument 'useRegExp' to
         be false instead of true.  This means that, when doing a update,
         delete, or select, records being selected on string fields will
         be matched using exact matching instead of regular expression
         matching.  If you want to do regular expression matching, pass
         'useRegExp = True' to the method.
        -Added a new public method called validate.  Calling this method
         with a table name will check each record of that table and
         validate that all of the fields have values of the correct type.
         This can be used to validate data you have put into the table by
         means other than through KirbyBase, perhaps by opening the table
         in a text editor and typing in information.
        -Fixed a bug in _closeTable where if an exception occurred it was
         blowing up because the variable 'name' did not exist.
        -Fixed a bug in _writeRecord where if an exception occured it was 
         blowing up because the variable 'name' did not exist.
        -Fixed a bug in _getMatches where I was referencing 
         self.field_names as a method instead of as a dictionary.
        -Added a keyword argument to select() called returnType.  If set to
         'object', the result list returned will contain Record objects
         where each field name is an attribute of the object, so you could
         refer to a record's field as plane.speed instead of plane[4].  If
         set to 'dict', the result list returned will contain dictionaries
         where each key is a field name and each value is a field value.  
         If set to 'list', the default, the result is a list of lists.
        -Added a new method, insertBatch.  It allows you to insert multiple
         records at one time into a table.
        -Added a new private method, _strToBool, that converts string
         values like 'True' to boolean values.
        -Added a new private method, _convertInput, and moved to it the 
         code that ensures that the data on an insert is in proper list 
         format.  I did this so that I did not have duplicate code in both
         the insert and insertBatch methods. 
        -To accomodate the fact that users can now send a large batch of
         records to be inserted, I changed _sendSocket so that it first
         sends the length of the database command to the server, then it
         actually sends the command itself, which can now be any length.
        -Changed the code in _getMatches to precompile the regular
         expression pattern instead of dynamically compiling every time the
         pattern is compared to a table record.  This should speed up 
         queries a little bit.
        -Changed the code in select that converts table fields back to
         their native types to be more efficient.
        -Changed _sendSocket to use StringIO (actually cStringIO) to hold
         the result set of a client/server-based query instead of just
         capturing the result by concatenating records to one big string.
         In informal testing on large result sets, it shaves a few tenths
         of a second off the query time.
    2005-01-31:  Version 1.7.1 released.
        -Fixed a nasty bug in _getMatches.  If useRegExp was True and the
         select was against multiple string fields, the code was only
         using one of the input patterns to search on instead of using all
         of them.
    2005-02-20:  Version 1.8 released.
         ********   IMPORTANT - Method Interface Changes   ****************
        -Added the ability to sort the result set of a select by multiple
         fields and to specify whether each field should be sorted
         ascending or descending.  This necessitated a change to the
         interface of the select method.  I moved the position of sortField
         in the argument list and also changed it to be a list instead of a
         string.  I also changed the name of sortField to sortFields. I 
         also moved sortDesc in the arguement list and also made it a list.
         ******************************************************************
        -Added another allowable value, 'report',  to the keyword 
         parameter, returnType in the select method.  This returns the
         result set in a pretty print format.  Along with this, added
         another keyword parameter called rptSettings to the select method.
         This is only used if rptType is 'report'.  It is a 2 element list.
         The first element specifies the number of records to print on a 
         page.  The second element is boolean specifying whether to print
         a dashed line between records.
        -Added ability to pass field values to update and insert using an
         object with attributes set equal to the field names.
        -Fixed a bug in _getMatches.  If a field of type int or float had
         a blank value in the table (i.e. ''), the code was attempting to
         convert it to it's proper type (i.e. int or float) before 
         doing the match comparison.  This would cause an exception to
         occur.  Now, if the field is an int,float,date or datetime and it
         is blank, I convert it to None.  This allows the numeric 
         comparisons to work correctly for null fields.
        -Fixed a bug in select.  If a field in the result set was equal
         to '', I was letting it stay that way, when I should really be
         converting it to None before returning the result set.
        -Cleaned up the internals a bit.  Mainly, I tried to use functions
         in the operator module like lt and ge instead of hardcoding < and
         >= in an if statement.
    2005-04-26:  Version 1.8.1 released.
        -Added the ability to select, update, delete multiple records by
         specifying a list of record numbers.
        -Cleaned up the internals of _getMatchesByRecno by not splitting
         the record line into fields if user is selecting all records, and
         by using a generator to return matches instead of building a list
         of matches and returning the whole list.
    2005-05-03:  Version 1.8.2 released.
        -Emergency bug-fix release:  Fred Pacquier's eagle eyes spotte a bug
         I introducted in version 1.8.1.  It's in _getMatchByRecno.  If you
         are selecting all records, KirbyBase is not getting rid of the
         newline character attached to the last field of the record.
         Thanks Fred Pacquier!   
    2005-09-19:  Version 1.9 released.
        -Fixed a bug that can occur on very fast machines when sending data
         between the client and the server.  Thanks to David Edwards for
         this fix.
        -Added a method, setDefaultReturnType, that allows you to, set the
         default return type of results from the select method.  The default
         is still 'list', but you can set it to 'list', 'object', or 'dict'.
         Thanks to David Edwards for this suggested enhancement.   
        -Added methods addFields and dropFields.  These allow you to add new
         columns to a table and remove existing columns from the table.
         Thanks to Pierre Quentel for the code for these two enhancements.     
"""
import re
import socket
import os.path
import datetime
import cPickle
import cStringIO
import operator
import tempfile
import shutil


#--------------------------------------------------------------------------
# KirbyBase Class
#--------------------------------------------------------------------------
class KirbyBase:
    """Database Management System.

    Public Methods:
        __init__             - Create an instance of database.
        close                - Close database.
        create               - Create a table.
        insert               - Insert a record into a table.
        insertBatch          - Insert a list of records into a table.
        update               - Update a table.
        delete               - Delete record(s) from a table.
        select               - select record(s) from a table.
        pack                 - remove deleted records from a table.
        validate             - validate data in table records.
        drop                 - Remove a table.
        getFieldNames        - Get a list of a table's field names.
        getFieldTypes        - Get a list of a table's field types.
        addFields            - Insert new column(s) into table.
        dropFields           - Remove column(s) from table.
        len                  - Total number of records in table.
        setDefaultReturnType - Set the default return type for selects.
    """

    #----------------------------------------------------------------------
    # PUBLIC METHODS
    #----------------------------------------------------------------------

    #----------------------------------------------------------------------
    # init
    #----------------------------------------------------------------------
    def __init__(self, type='local', host=None, port=None):
        """Create an instance of the database and return a reference to it.

        Keyword Arguments:
            type - Connection type: local(default), client, or server.
            host - IP address of server to connect to, if connection type
                   is client.
            port - Port number of server to connect to, if connection type
                   is client.
        """
        self.connect_type = type

        # Regular expression used to determine if field needs to be
        # encoded.
        self.encodeRegExp = re.compile(r'\n|\r|\032|\|')

        # Regular expression used to determine if field needs to be
        # un-encoded.
        self.unencodeRegExp = re.compile(
         r'&linefeed;|&carriage_return;|&substitute;|&pipe;')

        # This will be used to validate the select statements.
        self.cmpFuncs = {"<":operator.lt, "<=":operator.le, 
         ">=":operator.ge, ">":operator.gt, "==":operator.eq,
         "!=":operator.ne, "<>":operator.ne}
         
        # This will be used to validate and convert the field types in
        # the header rec of the table into valid python types.
        self.strToTypes = {'int':int, 'Integer':int, 'float':float, 
         'Float':float, 'datetime.date':datetime.date, 
         'Date':datetime.date, 'datetime.datetime':datetime.datetime,
         'DateTime':datetime.datetime, 'bool':bool, 'Boolean':bool,
         'str':str, 'String':str}

        # If connecting as client, open a socket connection with server.
        if self.connect_type == 'client':
            self.host, self.port = host, port
            
        # Default select return type to list.
        self.def_return_type = 'list'    

    #----------------------------------------------------------------------
    # close
    #----------------------------------------------------------------------
    def close(self):
        """Close connection to database server.
        """
        if self.connect_type == 'client':
            self.dbSock.close()

    #----------------------------------------------------------------------
    # create
    #----------------------------------------------------------------------
    def create(self, name, fields):
        """Create a new table and return True on success.

        Arguments:
            name   - physical filename, including path, that will hold
                     table.
            fields - list holding strings made up of multiple fieldname,
                     fieldtype pairs (i.e. ['plane:str','speed:int']).
                     Valid fieldtypes are: str, int, float, datetime.date,
                     datetime.datetime, bool or, for compatibility with 
                     the Ruby version of KirbyBase use String, Integer,
                     Float, Date, DateTime, and Boolean.

        Returns True if no exceptions are raised.
        """
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            return self._sendSocket("db.create('%s',%s)" %(name,fields))

        # Check to see if file already exists.
        if os.path.exists(name):
            raise KBError(name + ' already exists!')

        # Validate field types. Integer, String, Float, Date, DateTime, and
        # Boolean types are compatible between the Ruby and Python versions
        # of KirbyBase.
        for x in [y.split(':')[1] for y in fields]:
            if x not in self.strToTypes:
                raise KBError('Invalid field type: %s' % x)

        # Make copy of fields list so that value passed in is not changed.
        # Add recno counter, delete counter, and recno field definition at
        # beginning.
        header_rec = list(['000000','000000','recno:int'] + fields)

        # Open the table in write mode since we are creating it new, write
        # the header record to it and close it.
        fptr = self._openTable(name, 'w')
        fptr.write('|'.join(header_rec) + '\n')
        self._closeTable(fptr)

        # Return success.
        return True

    #----------------------------------------------------------------------
    # insert
    #----------------------------------------------------------------------
    def insert(self, name, values):
        """Insert a new record into table, return unique record number.

        Arguments:
            name   - physical file name, including path, that holds table.
            values - list, dictionary, or object containing field values
                     of new record.

        Returns unique record number assigned to new record when it is
        created.
        """
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            # I put this code when I added the ability to use a record
            # object for the values argument.  The problem was that, if the
            # user specified a record object, things worked ok in embedded
            # mode.  However, in client/server mode, I could not figure out
            # a good way to send the record object to the server because it
            # is embedded in a string representation of the database
            # command (i.e. "db.insert('plane', recordObject)").  So, what
            # I did to get it working was, I check to see if values is a
            # record object.  If it is, I grab the object's internal 
            # dictionary (__dict__) that holds all of the attributes and I
            # send this as part of the expression string, instead of 
            # sending the object itself.
            if not isinstance(values, (list, dict)):
                return self._sendSocket("db.insert('%s',%s)" %(name,
                 values.__dict__))
            else:           
                return self._sendSocket("db.insert('%s',%s)" %(name,values))
                
        # Open the table.
        fptr = self._openTable(name, 'r+')

        # Update the instance variables holding table header info
        self._updateHeaderVars(fptr)

        # If values is a dictionary or an object, we are going to convert
        # it into a list.  That way, we can use the same validation and 
        # updating routines regardless of whether the user passed in a 
        # dictionary, an object, or a list.  This returns a copy of 
        # values so that we are not messing with the original values.
        record = self._convertInput(values)

        # Check input fields to make sure they are valid.
        self._validateUpdateCriteria(record, self.field_names[1:])

        try:
            # Get a new record number.
            rec_no = self._incrRecnoCounter(fptr)

            # Add record number to front of record.
            record.insert(0, rec_no)

            # Append the new record to the end of the table and close the
            # table.  Run each field through encoder to take care of 
            # special characters.
            self._writeRecord(fptr, 'end', '|'.join(map(self._encodeString,
             [str(item) for item in record])))
        finally:
            self._closeTable(fptr)

        # Return the unique record number created for this new record.
        return rec_no

    #----------------------------------------------------------------------
    # insertBatch
    #----------------------------------------------------------------------
    def insertBatch(self, name, batchRecords):
        """Insert a batch of records into table, return a list of rec #s.

        Arguments:
            name         - physical file name, including path, that holds 
                           table.
            batchRecords - list of records.  Each record can be a list, a 
                           dictionary, or an object containing field values
                           of new record.

        Returns list of unique record numbers assigned.
        """
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            return self._sendSocket("db.insertBatch('%s',%s)" %(name,
             batchRecords))

        # Open the table, update the instance variables holding table
        # header info and close table.
        fptr = self._openTable(name, 'r')
        self._updateHeaderVars(fptr)
        self._closeTable(fptr)

        # Create an empty list to hold the batch after it has been
        # validated and any records within it that are in dictionary format
        # have been converted to list format.
        records = []

        for values in batchRecords:
            # If values is a dictionary or an object, we are going to 
            # convert it into a list.  That way, we can use the same 
            # validation and updating routines regardless of whether the
            # user passed in a dictionary, an object, or a list.  This 
            # returns a copy of values so that we are not messing with the
            # original values.
            record = self._convertInput(values)
            # Check input fields to make sure they are valid.
            self._validateUpdateCriteria(record, self.field_names[1:])
    
            # Add the validated (and possibly converted) record to the
            # records list.
            records.append(record)

        # Create empty list to hold new record numbers.
        rec_nos = []

        # Open the table again, this time in read-write mode.
        fptr = self._openTable(name, 'r+')

        try:
            # Now that the batch has been validated, add it to the database
            # table.
            for record in records:
                # Get a new record number.
                rec_no = self._incrRecnoCounter(fptr)

                # Add record number to front of record.
                record.insert(0, rec_no)

                # Append the new record to the end of the table.  Run each 
                # field through encoder to take care of special characters.
                self._writeRecord(fptr, 'end', '|'.join(
                 map(self._encodeString, [str(item) for item in record])))

                # Add the newly create record number to the list of that we
                # we return back to the user.
                rec_nos.append(rec_no)
        finally:
            self._closeTable(fptr)

        # Return the unique record number created for this new record.
        return rec_nos

    #----------------------------------------------------------------------
    # update
    #----------------------------------------------------------------------
    def update(self, name, fields, searchData, updates, filter=None, 
     useRegExp=False):
        """Update record(s) in table, return number of records updated.

        Arguments:
            name       - physical file name, including path, that holds
                         table.
            fields     - list containing names of fields to search on. If 
                         any of the items in this list is 'recno', then the
                         table will be searched by the recno field only and
                         will update, at most, one record, since recno is 
                         the system generated primary key.
            searchData - list containing actual data to search on.  Each 
                         item in list corresponds to item in the 'fields' 
                         list.
            updates    - list, dictionary, or object containing actual data
                         to put into table field.  If it is a list and 
                         'filter' list is empty or equal to None, then 
                         updates list must have a value for each field in
                         table record.
            filter     - only used if 'updates' is a list.  This is a
                         list containing names of fields to update.  Each
                         item in list corresponds to item in the 'updates'
                         list.  If 'filter' list is empty or equal to None,
                         and 'updates' is a list, then 'updates' list must 
                         have an item for each field in table record, 
                         excepting the recno field.
            useRegExp  - if true, match string fields using regular 
                         expressions, else match string fields using
                         strict equality (i.e. '==').  Defaults to true.

        Returns integer specifying number of records that were updated.

        Example:
            db.update('plane.tbl',['country','speed'],['USA','>400'],
             [1230],['range'])

            This will search for any plane from the USA with a speed
            greater than 400mph and update it's range to 1230 miles.
        """
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            # I put this code when I added the ability to use a record
            # object for the updates argument.  The problem was that, if the
            # user specified a record object, things worked ok in embedded
            # mode.  However, in client/server mode, I could not figure out
            # a good way to send the record object to the server because it
            # is embedded in a string representation of the database
            # command (i.e. "db.update('plane', ['recno'], [45], 
            # recordObject)").  So, what I did to get it working was, I 
            # check to see if updates is a record object.  If it is, I grab 
            # the object's internal dictionary (__dict__) that holds all of 
            # the attributes and I send this as part of the expression 
            # string, instead of sending the object itself.
            if not isinstance(updates, (list, dict)):
                return self._sendSocket("db.update('%s',%s,%s,%s,%s,%s)" 
                 %(name, fields, searchData, updates.__dict__, filter, 
                 useRegExp))
            else:
                return self._sendSocket("db.update('%s',%s,%s,%s,%s,%s)" 
                 %(name, fields, searchData, updates, filter, useRegExp))
            
        # Make copy of searchData list so that value passed in is not 
        # changed if I edit it in validateMatchCriteria.
        patterns = list(searchData)

        # Open the table.
        fptr = self._openTable(name, 'r+')

        # Update the instance variables holding table header info.
        self._updateHeaderVars(fptr)

        # If no update filter fields were specified, that means user wants
        # to update all field in record, so we set the filter list equal
        # to the list of field names of table, excluding the recno field,
        # since user is not allowed to update recno field.
        if filter:
            if isinstance(updates, list):
                pass
            # If updates is a dictionary, user cannot specify a filter,
            # because the keys of the dictionary will function as the
            # filter.
            elif isinstance(updates, dict):
                raise KBError('Cannot specify filter when updates is a ' +
                 'dictionary.')
            else:
                raise KBError('Cannot specify filter when updates is an ' +
                 'object.')

        else:
            # If updates is a list and no update filter
            # fields were specified, that means user wants to update
            # all fields in record, so we set the filter list equal
            # to the list of field names of table, excluding the recno
            # field, since user is not allowed to update recno field.
            if isinstance(updates, list): filter = self.field_names[1:]

        # If updates is a list, do nothing because it is already in the
        # proper format and filter has either been supplied by the user
        # or populated above.
        if isinstance(updates, list): pass
        # If updates is a dictionary, we are going to convert it into an
        # updates list and a filters list.  This will allow us to use the
        # same routines for validation and updating.
        elif isinstance(updates, dict):
            filter = [k for k in updates.keys() if k in 
             self.field_names[1:]]
            updates = [updates[i] for i in filter]
        # If updates is an object, we are going to convert it into an
        # updates list and a filters list.  This will allow us to use the
        # same routines for validation and updating.
        else:
            filter = [x for x in self.field_names[1:] if hasattr(updates,x)]
            updates = [getattr(updates,x) for x in self.field_names[1:] if 
             hasattr(updates,x)]
             
        try:
            # Check input arguments to make sure they are valid.
            self._validateMatchCriteria(fields, patterns)
            self._validateUpdateCriteria(updates, filter)
        except KBError:
            # If something didn't check out, close the table and re-raise
            # the error.
            fptr.close()
            raise

        # Search the table and populate the match list.
        match_list = self._getMatches(fptr, fields, patterns, useRegExp)

        # Create a list with each member being a list made up of a
        # fieldname and the corresponding update value, converted to a
        # safe string.
        filter_updates = zip(filter, 
         [self._encodeString(str(u)) for u in updates])

        updated = 0
        # Step through the match list.
        for line, fpos in match_list:
            # Create a copy of the current record.
            new_record = line.strip().split('|')
            # For each filter field, apply the updated value to the
            # table record.
            for field, update in filter_updates:
                new_record[self.field_names.index(field)] = update

            # Convert the updated record back into a text line so we
            # can write it back out to the file.
            new_line = '|'.join(new_record)

            # Since we are changing the current record, we will first
            # write over it with all blank spaces in the file.
            self._deleteRecord(fptr, fpos, line)

            # If the updated copy of the record is not bigger than the
            # old copy, then we can just write it in the same spot in
            # the file.  If it is bigger, then we will have to append
            # it to the end of the file.
            if len(new_line) > len(line):
                self._writeRecord(fptr, 'end', new_line)
                # If we didn't overwrite the current record, that means
                # we have another blank record (i.e. delete record) out
                # there, so we need to increment the deleted records
                # counter.
                self._incrDeleteCounter(fptr)
            else:
                self._writeRecord(fptr, fpos, new_line)
            updated+=1

        # Close the table.
        self._closeTable(fptr)

        # Return the number of records updated.
        return updated

    #----------------------------------------------------------------------
    # delete
    #----------------------------------------------------------------------
    def delete(self, name, fields, searchData, useRegExp=False):
        """Delete record(s) from table, return number of records deleted.

        Arguments:
            name       - physical file name, including path, that holds
                         table.
            fields     - list containing names of fields to search on. if
                         any of the items in this list is 'recno', then the
                         table will be searched by the recno field only and
                         will delete, at most, one record, since recno is 
                         the system generated primary key.
            searchData - list containing actual data to search on.  Each 
                         item in list corresponds to item in the 'fields'
                         list.
            useRegExp  - if true, match string fields using regular 
                         expressions, else match string fields using
                         strict equality (i.e. '==').  Defaults to true.

        Returns integer specifying number of records that were deleted.

        Example:
            db.delete('plane.tbl',['country','speed'],['USA','>400'])

            This will search for any plane from the USA with a speed
            greater than 400mph and delete it.
        """
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            return self._sendSocket("db.delete('%s',%s,%s,%s)" 
             %(name,fields, searchData, useRegExp))

        # Make copy of searchData list so that value passed in is not 
        # changed if I edit it in validateMatchCriteria.
        patterns = list(searchData)

        # Open the table.
        fptr = self._openTable(name, 'r+')

        # Update the instance variables holding table header info.
        self._updateHeaderVars(fptr)

        try:
            # Check input arguments to make sure they are valid.
            self._validateMatchCriteria(fields, patterns)
        except KBError:
            # If something didn't check out, close the table and re-raise
            # the error.
            fptr.close()
            raise

        # Search the table and populate the match list.
        match_list = self._getMatches(fptr, fields, patterns, useRegExp)
        deleted = 0

        # Delete any matches found.
        for line, fpos in match_list:
            self._deleteRecord(fptr, fpos, line)
            # Increment the delete counter.
            self._incrDeleteCounter(fptr)
            deleted+=1

        # Close the table.
        self._closeTable(fptr)

        # Return the number of records deleted.
        return deleted

    #----------------------------------------------------------------------
    # select
    #----------------------------------------------------------------------
    def select(self, name, fields, searchData, filter=None, 
     useRegExp=False, sortFields=[], sortDesc=[], returnType=None, 
     rptSettings=[0,False]):
        """Select record(s) from table, return list of records selected.

        Arguments:
            name          - physical file name, including path, that holds
                            table.
            fields        - list containing names of fields to search on. 
                            If any of the items in this list is 'recno', 
                            then the table will be searched by the recno 
                            field only and will select, at most, one record,
                            since recno is the system generated primary key.
            searchData    - list containing actual data to search on.  Each
                            item in list corresponds to item in the 
                            'fields' list.
            filter        - list containing names of fields to include for
                            selected records.  If 'filter' list is empty or
                            equal to None, then all fields will be included
                            in result set.
            useRegExp     - if true, match string fields using regular 
                            expressions, else match string fields using
                            strict equality (i.e. '==').  Defaults to False.
            sortFields    - list of fieldnames to sort on.  Each must be a
                            valid field name, and, if filter list is not
                            empty, the same fieldname must be in the filter
                            list.  Result set will be sorted in the same
                            order as fields appear in sortFields in 
                            ascending order unless the same field name also 
                            appears in sortDesc, then they will be sorted in
                            descending order.
            sortDesc      - list of fieldnames that you want to sort result
                            set by in descending order.  Each field name
                            must also be in sortFields.
            returnType    - a string specifying the type of the items in the
                            returned list.  Can be 'list' (items are lists
                            of values), 'dict' (items are dictionaries with
                            keys = field names and values = matching
                            values), 'object' (items = instances of the
                            generic Record class) or 'report' (result set
                            is formatted as a table with a header, suitable
                            for printing).  Defaults to None.  If None, the
                            instance variable def_return_type will be used.
            rptSettings   - a list with two elements.  This is only used if
                            returnType is 'report'.  The first element
                            specifies the number of records to print on each
                            page.  The default is 0, which means do not do
                            any page breaks.  The second element is a 
                            boolean specifying whether to print a row 
                            separator (a line of dashes) between each 
                            record.  The default is False.

        Returns list of records matching selection criteria.

        Example:
            db.select('plane.tbl',['country','speed'],['USA','>400'])

            This will search for any plane from the USA with a speed
            greater than 400mph and return it.
        """
        # Check returnType argument to make sure it is either 'list',
        # 'dict', or 'object'.
        if returnType not in [None, 'list', 'dict', 'object', 'report']:
            raise KBError('Invalid return type: %s' % returnType)
            
        # If user did not specify a return type, use whatever the default
        # return type is set to.
        if returnType == None:
            returnType = self.def_return_type
            
        # Check rptSettings list to make sure it's items are valid.
        if type(rptSettings[0]) != int:
            raise KBError('Invalid report setting: %s' % rptSettings[0])
        if type(rptSettings[1]) != bool:        
            raise KBError('Invalid report setting: %s' % rptSettings[1])
            
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            return self._sendSocket(
             "db.select('%s',%s,%s,%s,%s,%s,%s,'%s',%s)" 
             %(name, fields, searchData, filter, useRegExp, sortFields,
             sortDesc, returnType, rptSettings))

        # Make copy of searchData list so that value passed in is not 
        # changed if I edit it in validateMatchCriteria.
        patterns = list(searchData)

        # Open the table in read-only mode since we won't be updating it.
        fptr = self._openTable(name, 'r')

        # Update the instance variables holding table header info.
        self._updateHeaderVars(fptr)

        try:
            # Check input arguments to make sure they are valid.
            self._validateMatchCriteria(fields, patterns)
            if filter: self._validateFilter(filter)
            else: filter = self.field_names
        except KBError:
            # If something didn't check out, close the table and re-raise
            # the error.
            fptr.close()
            raise

        # Validate sort field argument.  It needs to be one of the field
        # names included in the filter.
        for field in [sf for sf in sortFields if sf not in filter]:
            raise KBError('Invalid sort field specified: %s' % field)
        # Make sure any fields specified in sort descending list are also
        # in sort fields list.    
        for field in [sf for sf in sortDesc if sf not in sortFields]:
            raise KBError('Cannot specify a field to sort in descending ' +
             'order if you have not specified that field as a sort field')

        # Search table and populate match list.
        match_list = self._getMatches(fptr, fields, patterns, useRegExp)

        # Initialize result set.
        result_set = []
        # Get a list of filter field indexes (i.e., where in the
        # table record is the field that the filter item is
        # referring to.
        filterIndeces = [self.field_names.index(x) for x in filter]

        # For each record in match list, add it to the result set.
        for record, fpos in match_list:
            # Initialize a record to hold the filtered fields of
            # the record.
            result_rec = []
            # Split the record line into it's fields.
            fields = record.split('|')

            # Step through each field index in the filter list. Grab the
            # result field at that position, convert it to
            # proper type, and put it in result set.
            for i in filterIndeces:
                # If the field is empty, just append it to the result rec.
                if fields[i] == '':
                    result_rec.append(None)
                # Otherwise, convert field to its proper type before 
                # appending it to the result record.
                else:
                    result_rec.append(
                     self.convert_types_functions[i](fields[i]))

            # Add the result record to the result set.
            result_set.append(result_rec)

        # Close the table.
        self._closeTable(fptr)

        # If a sort field was specified...
        # I stole the following code from Steve Lucy.  I got it from the
        # ASPN Python Cookbook webpages.  Thanks Steve!
        if len(sortFields) > 0:
            reversedSortFields = list(sortFields)
            reversedSortFields.reverse()
            for sortField in reversedSortFields:
                i = filter.index(sortField)
                result_set.sort( lambda x,y:
                    cmp(*[(x[i], y[i]), (y[i], x[i])]
                     [sortField in sortDesc]))

        # If returnType is 'object', then convert each result record
        # to a Record object before returning the result list.
        if returnType == 'object':
            return [Record(filter, rec) for rec in result_set]
        # If returnType is 'dict', then convert each result record to
        # a dictionary with the keys being the field names before returning
        # the result list.
        elif returnType == 'dict':
            return [dict(zip(filter, rec)) for rec in result_set]
        # If returnType is 'report', then return a pretty print version of
        # the result set.
        elif returnType == 'report':
            # How many records before a formfeed.
            numRecsPerPage = rptSettings[0]
            # Put a line of dashes between each record?
            rowSeparator = rptSettings[1]
            delim = ' | '

            # columns of physical rows
            columns = apply(zip, [filter] + result_set)

            # get the maximum of each column by the string length of its 
            # items
            maxWidths = [max([len(str(item)) for item in column]) 
             for column in columns]
            # Create a string of dashes the width of the print out.
            rowDashes = '-' * (sum(maxWidths) + len(delim)*
             (len(maxWidths)-1))

            # select the appropriate justify method
            justifyDict = {str:str.ljust,int:str.rjust,float:str.rjust,
             bool:str.ljust,datetime.date:str.ljust,
             datetime.datetime:str.ljust}

            # Create a string that holds the header that will print.
            headerLine = delim.join([justifyDict[fieldType](item,width) 
             for item,width,fieldType in zip(filter,maxWidths,
            self.field_types)])

            # Create a StringIO to hold the print out.
            output=cStringIO.StringIO()

            # Variable to hold how many records have been printed on the
            # current page.
            recsOnPageCount = 0

            # For each row of the result set, print that row.
            for row in result_set:
                # If top of page, print the header and a dashed line.
                if recsOnPageCount == 0:
                    print >> output, headerLine
                    print >> output, rowDashes

                # Print a record.
                print >> output, delim.join([justifyDict[fieldType](
                 str(item),width) for item,width,fieldType in 
                 zip(row,maxWidths,self.field_types)])

                # If rowSeparator is True, print a dashed line.
                if rowSeparator: print >> output, rowDashes

                # Add one to the number of records printed so far on
                # the current page.
                recsOnPageCount += 1

                # If the user wants page breaks and you have printed 
                # enough records on this page, print a form feed and
                # reset records printed variable.
                if numRecsPerPage > 0 and (recsOnPageCount ==
                 numRecsPerPage):
                    print >> output, '\f',
                    recsOnPageCount = 0
            # Return the contents of the StringIO.
            return output.getvalue()
        # Otherwise, just return the list of lists.
        else:
            return result_set


    #----------------------------------------------------------------------
    # pack
    #----------------------------------------------------------------------
    def pack(self, name):
        """Remove blank records from table and return total removed.

        Keyword Arguments:
            name - physical file name, including path, that holds table.

        Returns number of blank lines removed from table.
        """
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            return self._sendSocket("db.pack('%s')" %(name))

        # Open the table in read-only mode since we won't be updating it.
        fptr = self._openTable(name, 'r')

        # Read in all records.
        lines = fptr.readlines()

        # Close the table so we can re-build it.
        self._closeTable(fptr)

        # Reset number of deleted records to zero.
        header_rec = lines[0].split('|')
        header_rec[1] = "000000"

        # Set first line of re-built file to the header record.
        lines[0] = '|'.join(header_rec)

        # Open the table in write mode since we will be re-building it.
        fptr = self._openTable(name, 'w')

        # This is the counter we will use to report back how many blank
        # records were removed.
        lines_deleted = 0

        # Step through all records in table, only writing out non-blank
        # records.
        for line in lines:
            # By doing a rstrip instead of a strip, we can remove any
            # extra spaces at the end of line that were a result of
            # updating a record with a shorter one.
            line = line.rstrip()
            if line == "":
               lines_deleted += 1
               continue
            try:
                fptr.write(line + '\n')
            except:
                raise KBError('Could not write record in: ' + name)

        # Close the table.
        self._closeTable(fptr)

        # Return number of records removed from table.
        return lines_deleted

    #----------------------------------------------------------------------
    # validate
    #----------------------------------------------------------------------
    def validate(self, name):
        """Validate all records have field values of proper data type.

        Keyword Arguments:
            name - physical file name, including path, that holds table.

        Returns list of records that have invalid data.
        """
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            return self._sendSocket("db.validate('%s')" %(name))

        # Open the table in read-only mode since we won't be updating it.
        fptr = self._openTable(name, 'r')

        # Update the instance variables holding table header info
        self._updateHeaderVars(fptr)

        # Create list to hold any invalid records that are found.
        invalid_list = []

        # Loop through all records in the table.
        for line in fptr:
            # Strip off newline character and any trailing spaces.
            line = line[:-1].strip()

            # If blank line, skip this record.
            if line == "": continue

            # Split the line up into fields.
            record = line.split("|")

            # Check the value of recno to see if the value is
            # greater than the last recno assigned.  If it is,
            # add this to the invalid record list.
            if self.last_recno < int(record[0]):
                invalid_list.append([record[0], 'recno', record[0]])

            # For each field in the record check to see if you
            # can convert it to the field type specified in the
            # header record by using the conversion function
            # specified in self.convert_types_functions. 
            # If you can't convert it, add the
            # record number, the field name, and the offending
            # field value to the list of invalid records.
            for i, item in enumerate(record):
                if item == '': continue
                try:
                    if self.convert_types_functions[i](item): pass
                except:
                    invalid_list.append([record[0], self.field_names[i], 
                     item])

        # Close the table.
        self._closeTable(fptr)

        # Return list of invalid records.
        return invalid_list

    #----------------------------------------------------------------------
    # drop
    #----------------------------------------------------------------------
    def drop(self, name):
        """Delete physical file containing table and return True.

        Arguments:
            name - physical filename, including path, that holds table.

        Returns True if no exceptions are raised.
        """
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            return self._sendSocket("db.drop('%s')" %(name))

        # Delete physical file.
        os.remove(name)

        # Return success.
        return True

    #----------------------------------------------------------------------
    # getFieldNames
    #----------------------------------------------------------------------
    def getFieldNames(self, name):
        """Return list of field names for specified table name

        Arguments:
            name - physical file name, including path, that holds table.

        Returns list of field names for table.
        """
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            return self._sendSocket("db.getFieldNames('%s')" %(name))

        # Open the table in read-only mode since we won't be updating it.
        fptr = self._openTable(name, 'r')

        # Update the instance variables holding table header info
        self._updateHeaderVars(fptr)

        # Close the table.
        self._closeTable(fptr)

        return self.field_names

    #----------------------------------------------------------------------
    # getFieldTypes
    #----------------------------------------------------------------------
    def getFieldTypes(self, name):
        """Return list of field types for specified table name

        Arguments:
            name - physical file name, including path, that holds table.

        Returns list of field types for table.
        """
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            return self._sendSocket("db.getFieldTypes('%s')" %(name))

        # Open the table in read-only mode since we won't be updating it.
        fptr = self._openTable(name, 'r')

        # Update the instance variables holding table header info
        self._updateHeaderVars(fptr)

        # Close the table.
        self._closeTable(fptr)

        return self.field_types

    #----------------------------------------------------------------------
    # addFields
    #----------------------------------------------------------------------
    def addFields(self, name, fields, after = ''):
        """Modify the table to insert new fields after the specified field,
        or at the beginning if after is None
        
        Arguments:
            name          - physical file name, including path, that holds
                            table.
            fields        - list containing names of fields to insert. 
            after         - name of the field after which the new fields
                            will be inserted, or empty string to insert at
                            the beginning of the record
        
        A temporary file is created, then copied into the old file
        """
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            return self._sendSocket("db.addFields('%s',%s,'%s')" %(name,
             fields,after))

        # Validate field types
        for x in [y.split(':')[1] for y in fields]:
            if x not in self.strToTypes:
                raise KBError('Invalid field type: %s' % x)
        # Validate the field given in "after"
        field_names = self.getFieldNames(name)
        if after and after not in field_names:
            raise KBError('Invalid field name: %s' % after)
        # index to insert the new fields
        if not after:
            insert_after = 0
        else:
            insert_after = field_names.index(after)
        # build the modified fields list
        old_fields = zip(field_names,self.getFieldTypes(name))
        new_fields = []
        for (n,t) in old_fields[1:]:
            if t.__name__ in ['date','datetime']:
                new_fields.append('%s:datetime.%s' %(n,t.__name__))
            else:
                new_fields.append('%s:%s' %(n,t.__name__))
        new_fields = new_fields[:insert_after]+fields+ \
            new_fields[insert_after:]

        # Use tempfile to get a unique name for a temporary file 
        # to insert the records of the modified table
        temp_file=tempfile.NamedTemporaryFile()
        temp_name=temp_file.name
        temp_file.close()

        # Recreate the header line to go into the temp file.
        header_rec = list(['%06d' % self.last_recno, 
         '%06d' % self.del_counter, 'recno:int'] + new_fields)

        # copy old table into temporary one
        in_file = open(name,'r')
        in_file.readline() # skip first line
        out_file = open(temp_name,'a')
        # Write header rec to new file.
        out_file.write('|'.join(header_rec) + '\n')

        blank = ['']*len(fields)
        # build the new file faster than with selects from old file 
        # and inserts in new one
        for line in in_file:
            recs = line[:-1].split('|')
            recs = recs[:insert_after+1]+blank+recs[insert_after+1:]
            out_file.write('|'.join(recs)+'\n')
        out_file.close()
        # copy temporary file into old file
        shutil.copyfile(temp_name,name)
        # delete temporary file
        os.remove(temp_name)
        return True 

    #----------------------------------------------------------------------
    # dropFields
    #----------------------------------------------------------------------
    def dropFields(self, name, fields, after = ''):
        """Modify the table to drop the specified fields
        
        Arguments:
            name          - physical file name, including path, that holds
                            table.
            fields        - list containing names of fields to drop. 
        
        A temporary file is created, then copied into the old file
        """
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            return self._sendSocket("db.dropFields('%s',%s)" %(name,fields))

        # Validate the fields
        if 'recno' in fields:
           raise KBError("Can't drop the field 'recno'")
        field_names = self.getFieldNames(name)
        for field in fields:
            if field not in field_names:
                raise KBError('Invalid field name : %s' %field)
        # build the modified fields list
        old_fields = zip(field_names,self.getFieldTypes(name))
        new_fields = []
        dropped_indeces = []
        for i,(n,t) in enumerate(old_fields[1:]):
            if not n in fields:
                if t.__name__ in ['date','datetime']:
                    new_fields.append('%s:datetime.%s' %(n,t.__name__))
                else:
                    new_fields.append('%s:%s' %(n,t.__name__))
            else:
                dropped_indeces.append(i+1)

        # Use tempfile to get a unique name for a temporary file 
        # to insert the records of the modified table
        temp_file=tempfile.NamedTemporaryFile()
        temp_name=temp_file.name
        temp_file.close()

        # Recreate the header rec to go into the temp file.
        header_rec = list(['%06d' % self.last_recno, 
         '%06d' % self.del_counter, 'recno:int'] + new_fields)

        # copy old table into temporary one
        in_file = open(name,'r')
        in_file.readline() # skip first line
        out_file = open(temp_name,'a')
        # Write header rec to new file.
        out_file.write('|'.join(header_rec) + '\n')

        # build the new file faster than with selects from old file 
        # and inserts in new one
        for line in in_file:
            recs = line[:-1].split('|')
            new_recs = [ item for (i,item) in enumerate(recs) \
                if not i in dropped_indeces]
            out_file.write('|'.join(new_recs)+'\n')
        out_file.close()
        # copy temporary file into old file
        shutil.copyfile(temp_name,name)
        # delete temporary file
        os.remove(temp_name)
        return True

    #----------------------------------------------------------------------
    # len
    #----------------------------------------------------------------------
    def len(self, name):
        '''Return total number of non-deleted records in specified table

        Arguments:
            name - physical file name, including path, that holds table.

        Returns total number of records in table.
        '''
        # If running as a client, then send the command to the server for
        # it to execute.
        if self.connect_type == 'client':
            return self._sendSocket("db.len('%s')" %(name))

        # Initialize counter.
        rec_count = 0

        # Open the table in read-only mode since we won't be updating it.
        fptr = self._openTable(name, 'r')

        # Skip header record.
        line = fptr.readline()

        # Loop through entire table.
        line = fptr.readline()
        while line:
            # Strip off newline character.
            line = line[0:-1]

            # If not blank line, add 1 to record count.
            if line.strip() != "": rec_count += 1

            # Read next record.
            line = fptr.readline()

        # Close the table.
        self._closeTable(fptr)

        return rec_count

    #----------------------------------------------------------------------
    # setDefaultReturnType
    #----------------------------------------------------------------------
    def setDefaultReturnType(self, ret_type):
        """Set the default return type for selects.
        """
        if ret_type not in ['list', 'dict', 'object', 'report']:
            raise KBError('Invalid return type: %s' % ret_type)
        self.def_return_type = ret_type

    #----------------------------------------------------------------------
    # PRIVATE METHODS
    #----------------------------------------------------------------------


    #----------------------------------------------------------------------
    # _strToBool
    #----------------------------------------------------------------------
    def _strToBool(self, boolString):
        if boolString == 'True': return True
        elif boolString == 'False': return False
        else: raise KBError('Invalid value for boolean: %s' % boolString)


    #----------------------------------------------------------------------
    # _strToDate
    #----------------------------------------------------------------------
    def _strToDate(self, dateString):
        # Split the date string up into pieces and create a
        # date object.
        return datetime.date(*map(int, dateString.split('-'))) 
        
    #----------------------------------------------------------------------
    # _strToDateTime
    #----------------------------------------------------------------------
    def _strToDateTime(self, dateTimeString):
        # Split datetime string into datetime portion microseconds portion.
        tempDateTime = dateTimeString.split('.')
        # Were there microseconds in the datetime string.
        if len(tempDateTime) > 1: microsec = int(tempDateTime[1])
        else: microsec = 0
        
        # Now, split the datetime portion into a date
        # and a time string.  Take all of the pieces and
        # create a datetime object.
        tempDate, tempTime = tempDateTime[0].split(' ')
        y, m, d = tempDate.split('-')
        h, min, sec = tempTime.split(':')
        return datetime.datetime(int(y),int(m),int(d),int(h),int(min),
         int(sec),microsec)
         
    #----------------------------------------------------------------------
    # _encodeString
    #----------------------------------------------------------------------
    def _encodeString(self, s):
        '''Encode a string.

        Translates problem characters like \n, \r, and \032 to benign
        character strings.

        Keyword Arguments:
            s - string to encode.

        Returns encoded string.
        '''
        if self.encodeRegExp.search(s):
            s = s.replace('\n', '&linefeed;')
            s = s.replace('\r', '&carriage_return;')
            s = s.replace('\032', '&substitute;')
            s = s.replace('|', '&pipe;')
        return s

    #----------------------------------------------------------------------
    # _unencodeString
    #----------------------------------------------------------------------
    def _unencodeString(self, s):
        '''Unencode a string.

        Translates encoded character strings back to special characters
        like \n, \r, \032.

        Keyword Arguments:
            s - string to unencode.

        Returns unencoded string.
        '''
        if self.unencodeRegExp.search(s):
            s = s.replace('&linefeed;', '\n')
            s = s.replace('&carriage_return;', '\r')
            s = s.replace('&substitute;', '\032')
            s = s.replace('&pipe;', '|')
        return s

    #----------------------------------------------------------------------
    # _updateHeaderVars
    #----------------------------------------------------------------------
    def _updateHeaderVars(self, fptr):
        # Go to the header record and read it in.
        fptr.seek(0)
        line = fptr.readline()

        # Chop off the newline character.
        line = line[0:-1]

        # Split the record into fields.
        header_rec = line.split('|')

        # Update Last Record Number and Deleted Records counters.
        self.last_recno = int(header_rec[0])
        self.del_counter = int(header_rec[1])

        # Skip the recno counter, and the delete counter.
        header_fields = header_rec[2:]

        # Create an instance variable holding the field names.
        self.field_names = [item.split(':')[0] for item in header_fields]
        # Create an instance variable holding the field types.
        self.field_types = [self.strToTypes[x.split(':')[1]] for x in
         header_fields]
         
        # the functions to use to convert values as strings into their type
        convTypes={int:int,float:float,bool:self._strToBool,
            str:self._unencodeString,
            datetime.date:self._strToDate,
            datetime.datetime:self._strToDateTime}
        self.convert_types_functions = [convTypes[f] for f in 
         self.field_types]


    #----------------------------------------------------------------------
    # _validateMatchCriteria
    #----------------------------------------------------------------------
    def _validateMatchCriteria(self, fields, patterns):
        """Run various checks against list of fields and patterns to be
        used as search criteria.  This method is called from all public
        methods that search the database.
        """
        if len(fields) == 0:
            raise KBError('Length of fields list must be greater ' +
             'than zero.')
        if len(fields) != len(patterns):
            raise KBError('Length of fields list and patterns list ' +
             'not the same.')

        # If any of the list of fields to search on do not match a field
        # in the table, raise an error.
        for field, pattern in zip(fields, patterns):
            if field not in self.field_names:
                raise KBError('Invalid field name in fields list: %s' 
                    %field)

            # If the field is recno, make sure they are trying not to
            # search on more than one field.  Also, make sure they are
            # either trying to match all records or that it is an integer.
            if field == 'recno':
                if len(fields) > 1:
                    raise KBError('If selecting by recno, no other ' +
                     'selection criteria is allowed')
                if pattern != '*':
                    # check if all specified recnos are integers
                    if not isinstance(pattern,(tuple,list)):
                        pattern = [pattern]
                    for x in pattern:
                        if not isinstance(x,int):
                            raise KBError('Recno argument %s has type %s' 
                                ', expected an integer' %(x,type(x)))                        
                continue
                            
            # If the field type is not a str or a bool, make sure the
            # pattern you are searching on has a proper comparion
            # operator (<,<=,>,>=,==,!=,or <>) in it.
            if (self.field_types[self.field_names.index(field)] in  
             [int, float, datetime.date, datetime.datetime]):
                r = re.search('[\s]*[\+-]?\d', pattern)
                if not self.cmpFuncs.has_key(pattern[:r.start()]):
                    raise KBError('Invalid comparison syntax: %s'
                     % pattern[:r.start()])


    #----------------------------------------------------------------------
    #_convertInput
    #----------------------------------------------------------------------
    def _convertInput(self, values):
        """If values is a dictionary or an object, we are going to convert 
        it into a list.  That way, we can use the same validation and 
        updating routines regardless of whether the user passed in a 
        dictionary, an object, or a list.
        """
        # If values is a list, make a copy of it.
        if isinstance(values, list): record = list(values)
        # If values is a dictionary, convert it's values into a list
        # corresponding to the table's field names.  If there is not
        # a key in the dictionary corresponding to a field name, place a
        # '' in the list for that field name's value.
        elif isinstance(values, dict):
            record = [values.get(k,'') for k in self.field_names[1:]]        
        # If values is a record object, then do the same thing for it as
        # you would do for a dictionary above.
        else:
            record = [getattr(values,a,'') for a in self.field_names[1:]]
        # Return the new list with all items == None replaced by ''.
        new_rec = []
        for r in record:
            if r == None:
                new_rec.append('')
            else:
                new_rec.append(r)
        return new_rec        


    #----------------------------------------------------------------------
    # _validateUpdateCriteria
    #----------------------------------------------------------------------
    def _validateUpdateCriteria(self, updates, filter):
        """Run various checks against list of updates and fields to be
        used as update criteria.  This method is called from all public
        methods that update the database.
        """
        if len(updates) == 0:
            raise KBError('Length of updates list must be greater ' +
             'than zero.')

        if len(updates) != len(filter):
            raise KBError('Length of updates list and filter list ' +
             'not the same.')
        # Since recno is the record's primary key and is system
        # generated, like an autoincrement field, do not allow user
        # to update it.
        if 'recno' in filter:
            raise KBError("Cannot update value of 'recno' field.")

        # Validate filter list.
        self._validateFilter(filter)

        # Make sure each update is of the proper type.
        for update, field_name in zip(updates, filter):
            if update in ['', None]: pass
            elif type(update) != self.field_types[
             self.field_names.index(field_name)]:
                raise KBError("Invalid update value for %s" % field_name)

    #----------------------------------------------------------------------
    # _validateFilter
    #----------------------------------------------------------------------
    def _validateFilter(self, filter):
        # Each field in the filter list must be a valid field in the table.
        for field in filter:
            if field not in self.field_names:
                raise KBError('Invalid field name: %s' % field)

    #----------------------------------------------------------------------
    # _getMatches
    #----------------------------------------------------------------------
    def _getMatches(self, fptr, fields, patterns, useRegExp):
        # Initialize a list to hold all records that match the search
        # criteria.
        match_list = []

        # If one of the fields to search on is 'recno', which is the
        # table's primary key, then search just on that field and return
        # at most one record.
        if 'recno' in fields:
            return self._getMatchByRecno(fptr,patterns)
        # Otherwise, search table, using all search fields and patterns
        # specified in arguments lists.
        else:
            new_patterns = [] 
            fieldNrs = [self.field_names.index(x) for x in fields]
            for fieldPos, pattern in zip(fieldNrs, patterns):
                if self.field_types[fieldPos] == str:
                    # If useRegExp is True, compile the pattern to a
                    # regular expression object and add it to the
                    # new_patterns list.  Otherwise,  just add it to
                    # the new_patterns list.  This will be used below 
                    # when matching table records against the patterns.
                    if useRegExp:
                        new_patterns.append(re.compile(pattern))
                        # the pattern can be a tuple with re flags like re.I
                    else:
                        new_patterns.append(pattern)
                elif self.field_types[fieldPos] == bool:
                    # If type is boolean, I am going to coerce it to be
                    # either True or False by applying bool to it.  This
                    # is because it could be '' or [].  Next, I am going
                    # to convert it to the string representation: either
                    # 'True' or 'False'.  The reason I do this is because
                    # that is how it is stored in each record of the table
                    # and it is a lot faster to change this one value from
                    # boolean to string than to change possibly thousands
                    # of table values from string to boolean.  And, if they
                    # both are either 'True' or 'False' I can still
                    # compare them using the equality test and get the same
                    # result as if they were both booleans.
                    new_patterns.append(str(bool(pattern)))
                else:
                    # If type is int, float, date, or datetime, this next
                    # bit of code will split the the comparison string
                    # into the string representing the comparison
                    # operator (i.e. ">=" and the actual value we are going
                    # to compare the table records against from the input
                    # pattern, (i.e. "5").  So, for example, ">5" would be
                    # split into ">" and "5".
                    r = re.search('[\s]*[\+-]?\d', pattern)
                    if self.field_types[fieldPos] == int:
                        patternValue = int(pattern[r.start():])
                    elif self.field_types[fieldPos] == float:
                        patternValue = float(pattern[r.start():])
                    else:
                        patternValue = pattern[r.start():]
                    new_patterns.append(
                     [self.cmpFuncs[pattern[:r.start()]], patternValue]
                    ) 

            fieldPos_new_patterns = zip(fieldNrs, new_patterns)
            maxfield = max(fieldNrs)+1

            # Record current position in table. Then read first detail
            # record.
            fpos = fptr.tell()
            line = fptr.readline()

            # Loop through entire table.
            while line:
                # Strip off newline character and any trailing spaces.
                line = line[:-1].strip()
                # Keep track of matching
                is_no_match = False
                # If blank line, skip this record.
                if line == "": is_no_match = True
                # Split the line up into fields.
                record = line.split("|", maxfield)

                if not is_no_match:
                    # Foreach correspond field and pattern, check to see
                    # if the table record's field matches successfully.
                    for fieldPos, pattern in fieldPos_new_patterns:
                        # If the field type is string, it
                        # must be an exact match or a regular expression,
                        # so we will compare the table record's field to it
                        # using either '==' or the regular expression 
                        # engine.  Since it is a string field, we will need
                        # to run it through the unencodeString function to
                        # change any special characters back to their 
                        # original values.
                        if self.field_types[fieldPos] == str:
                            try:
                                if useRegExp:
                                    if not pattern.search(
                                        self._unencodeString(record[fieldPos])
                                        ):
                                        is_no_match = True
                                        break
                                else:
                                    if record[fieldPos] != pattern:
                                        is_no_match = True
                                        break
                            except Exception:
                                raise KBError(
                                    'Invalid match expression for %s'
                                    % self.field_names[fieldPos])
                        # If the field type is boolean, then I will simply
                        # do an equality comparison.  See comments above
                        # about why I am actually doing a string compare
                        # here rather than a boolean compare.
                        elif self.field_types[fieldPos] == bool:
                            if record[fieldPos] != pattern:
                                is_no_match = True
                                break
                        # If it is not a string or a boolean, then it must 
                        # be a number or a date.
                        else:
                            # Convert the table's field value, which is a
                            # string, back into it's native type so that
                            # we can do the comparison.
                            if record[fieldPos] == '':
                                tableValue = None
                            elif self.field_types[fieldPos] == int:
                                tableValue = int(record[fieldPos])
                            elif self.field_types[fieldPos] == float:
                                tableValue = float(record[fieldPos])
                            # I don't convert datetime values from strings
                            # back into their native types because it is
                            # faster to just leave them as strings and 
                            # convert the comparison value that the user
                            # supplied into a string.  Comparing the two
                            # strings works out the same as comparing two
                            # datetime values anyway.    
                            elif self.field_types[fieldPos] in (
                                datetime.date, datetime.datetime):
                                tableValue = record[fieldPos]
                            else:
                                # If it falls through to here, then,
                                # somehow, a bad field type got put into
                                # the table and we show an error.
                                raise KBError('Invalid field type for %s'
                                              % self.field_names[fieldPos])
                            # Now we do the actual comparison.  I used to
                            # just do an eval against the pattern string
                            # here, but I found that eval's are VERY slow.
                            # So, now I determine what type of comparison
                            # they are trying to do and I do it directly.
                            # This sped up queries by 40%.     
                            if not pattern[0](tableValue, pattern[1]):
                                is_no_match = True
                                break
                # If a 'No Match' exception was raised, then go to the
                # next record, otherwise, add it to the list of matches.
                if not is_no_match:
                    match_list.append([line, fpos])
                # Save the file position BEFORE we read the next record,
                # because after a read it is pointing at the END of the
                # current record, which, of course, is also the BEGINNING
                # of the next record.  That's why we have to save the
                # position BEFORE we read the next record.
                fpos = fptr.tell()
                line = fptr.readline()

        # After searching, return the list of matched records.
        return match_list

    #----------------------------------------------------------------------
    # _getMatchByRecno
    #----------------------------------------------------------------------
    def _getMatchByRecno(self, fptr, recnos):
        """Search by recnos. recnos is a list, containing '*', an integer, or
        a list or tuple of integers"""

        # Initialize table location marker and read in first record
        # of table.
        fpos = fptr.tell()
        line = fptr.readline()
        
        if recnos == ['*']:
            # take all the non blank lines
            while line:
                # Strip of newline character.
                line = line[0:-1]

                if line.strip():
                    yield [line,fpos]
                fpos = fptr.tell()
                line = fptr.readline()
        else:
            # select the records with record number in recnos
            if isinstance(recnos[0],(tuple,list)):
                # must make it a list, to be able to remove items
                recnos = list(recnos[0])
            while line:
                # Strip of newline character.
                line = line[0:-1]

                # If line is not blank, split it up into fields.
                if line.strip():
                    record = line.split("|")
                    # If record number for current record equals record number
                    # we are searching for, add it to match list
                    if int(record[0]) in recnos:
                        yield [line, fpos]
                        recnos.remove(int(record[0]))
                        # if no more recno to search, stop looping
                        if not recnos: break

                # update the table location marker
                # and read the next record.
                fpos = fptr.tell()
                line = fptr.readline()

        # Stop iteration
        return

    #----------------------------------------------------------------------
    # _incrRecnoCounter
    #----------------------------------------------------------------------
    def _incrRecnoCounter(self, fptr):
        # Save where we are in the table.
        last_pos = fptr.tell()

        # Go to header record and grab header fields.
        fptr.seek(0)
        line = fptr.readline()
        header_rec = line[0:-1].split('|')

        # Increment the recno counter.
        self.last_recno += 1
        header_rec[0] = "%06d" %(self.last_recno)

        # Write the header record back to the file.  Run each field through
        # encoder to handle special characters.
        self._writeRecord(fptr, 0, '|'.join(header_rec))

        # Go back to where you were in the table.
        fptr.seek(last_pos)
        # Return the newly incremented recno counter.
        return self.last_recno

    #----------------------------------------------------------------------
    # _incrDeleteCounter
    #----------------------------------------------------------------------
    def _incrDeleteCounter(self, fptr):
        # Save where we are in the table.
        last_pos = fptr.tell()

        # Go to header record and grab header fields.
        fptr.seek(0)
        line = fptr.readline()
        header_rec = line[0:-1].split('|')

        # Increment the delete counter.
        self.del_counter += 1
        header_rec[1] = "%06d" %(self.del_counter)

        # Write the header record back to the file.
        self._writeRecord(fptr, 0, '|'.join(header_rec))

        # Go back to where you were in the table.
        fptr.seek(last_pos)

    #----------------------------------------------------------------------
    # _deleteRecord
    #----------------------------------------------------------------------
    def _deleteRecord(self, fptr, pos, record):
        # Move to record position in table.
        fptr.seek(pos)

        # Overwrite record with all spaces.
        self._writeRecord(fptr, pos, " " * len(record))

    #----------------------------------------------------------------------
    # _writeRecord
    #----------------------------------------------------------------------
    def _writeRecord(self, fptr, pos, record):
        try:
            # If record is to be appended, go to end of table and write
            # record, adding newline character.
            if pos == 'end':
                fptr.seek(0, 2)
                fptr.write(record + '\n')
            else:
                # Otherwise, move to record position in table and write
                # record.
                fptr.seek(pos)
                fptr.write(record)
        except:
            raise KBError('Could not write record to: ' + fptr.name)

    #----------------------------------------------------------------------
    # _openTable
    #----------------------------------------------------------------------
    def _openTable(self, name, access):
        try:
            # Open physical file holding table.
            fptr = open(name, access)
        except:
            raise KBError('Could not open table: ' + name)
        # Return handle to physical file.
        return fptr

    #----------------------------------------------------------------------
    # _closeTable
    #----------------------------------------------------------------------
    def _closeTable(self, fptr):
        try:
            # Close the file containing the table.
            fptr.close()
        except:
            raise KBError('Could not close table: ' + fptr.name)

    #----------------------------------------------------------------------
    # _sendSocket
    #----------------------------------------------------------------------
    def _sendSocket(self, command):
        dbSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        dbSock.connect((self.host, self.port))
        self.dbSock = dbSock

        # Send the length of the command followed by the command itself.
        dbSock.send('%16s%s' % (len(command), command))
        
        # Receive the return value of the method.
        data = ''
        while len(data) < 16:
            data = data + dbSock.recv(1024)
        recv_length = int(data[:16])
        data = data[16:]

        while len(data) < recv_length:
            data = data + dbSock.recv(1024)
            
        # Convert pickled binary data back into it's original format
        # (usually a list).
        data = cPickle.loads(data[:recv_length])

        # If the server passed back an error object, re-raise that error
        # here on the client side, otherwise, just return the data to the
        # caller.
        if isinstance(data, Exception):
            raise data
        else:
            return data


#--------------------------------------------------------------------------
# Generic class for records
#--------------------------------------------------------------------------
class Record(object):
    """Generic class for record objects.

    Public Methods:
        __init__ - Create an instance of Record.
    """
    #----------------------------------------------------------------------
    # init
    #----------------------------------------------------------------------
    def __init__(self,names,values):
        self.__dict__ = dict(zip(names, values))


#--------------------------------------------------------------------------
# KBError Class
#--------------------------------------------------------------------------
class KBError(Exception):
    """Exception class for Database Management System.

    Public Methods:
        __init__ - Create an instance of exception.
    """
    #----------------------------------------------------------------------
    # init
    #----------------------------------------------------------------------
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return `self.value`

    # I overrode repr so I could pass error objects from the server to the
    # client across the network.
    def __repr__(self):
        format = """KBError("%s")"""
        return format % (self.value)