dbf_light 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbf_light/__init__.py +4 -0
- dbf_light/_base.py +37 -0
- dbf_light/cast.py +68 -0
- dbf_light/cli.py +65 -0
- dbf_light/definitions.py +171 -0
- dbf_light/exceptions.py +4 -0
- dbf_light/light.py +195 -0
- dbf_light/utils.py +25 -0
- dbf_light-1.0.1.dist-info/METADATA +72 -0
- dbf_light-1.0.1.dist-info/RECORD +13 -0
- dbf_light-1.0.1.dist-info/WHEEL +4 -0
- dbf_light-1.0.1.dist-info/entry_points.txt +2 -0
- dbf_light-1.0.1.dist-info/licenses/LICENSE +28 -0
dbf_light/__init__.py
ADDED
dbf_light/_base.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
import struct
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Definition:
|
|
6
|
+
|
|
7
|
+
_definition = ()
|
|
8
|
+
_struct_rule = ''
|
|
9
|
+
_struct_size = None
|
|
10
|
+
_names = []
|
|
11
|
+
|
|
12
|
+
def __init__(self, data):
|
|
13
|
+
self.data = data
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def init_cache(cls):
|
|
17
|
+
names = []
|
|
18
|
+
rules = []
|
|
19
|
+
|
|
20
|
+
for name, rule in cls._definition:
|
|
21
|
+
names.append(name)
|
|
22
|
+
rules.append(rule)
|
|
23
|
+
|
|
24
|
+
cls._names = names
|
|
25
|
+
rule = '<' + ''.join(rules)
|
|
26
|
+
cls._struct_rule = rule
|
|
27
|
+
cls._struct_size = struct.calcsize(rule)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def _unpack(cls, raw):
|
|
31
|
+
unpacked = struct.unpack(cls._struct_rule, raw)
|
|
32
|
+
return dict(zip(cls._names, unpacked, strict=False))
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_file(cls, fileobj):
|
|
36
|
+
data = fileobj.read(cls._struct_size)
|
|
37
|
+
return cls(cls._unpack(data))
|
dbf_light/cast.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_string(field, val):
|
|
7
|
+
return val.decode(field.encoding).strip()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_date(field, val):
|
|
11
|
+
val = parse_string(field, val)
|
|
12
|
+
|
|
13
|
+
if not val:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
return datetime.strptime(val, '%Y%m%d').date()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_numeric(field, val):
|
|
20
|
+
|
|
21
|
+
val = parse_string(field, val)
|
|
22
|
+
|
|
23
|
+
if not val:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
if field.data['decimal_count']:
|
|
27
|
+
val = Decimal(val)
|
|
28
|
+
else:
|
|
29
|
+
val = int(val)
|
|
30
|
+
|
|
31
|
+
return val
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_float(field, val):
|
|
35
|
+
val = parse_string(field, val)
|
|
36
|
+
|
|
37
|
+
if not val:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
return float(val)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def parse_bool(field, val):
|
|
44
|
+
val = parse_string(field, val).lower()
|
|
45
|
+
|
|
46
|
+
if not val or val == '?':
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
return val in {'t', 'y'}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_memo(field, val):
|
|
53
|
+
val = parse_string(field, val).lower()
|
|
54
|
+
|
|
55
|
+
if not val:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
return int(val)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
CAST_MAP = {
|
|
62
|
+
b'C': parse_string,
|
|
63
|
+
b'D': parse_date,
|
|
64
|
+
b'N': parse_numeric,
|
|
65
|
+
b'F': parse_float,
|
|
66
|
+
b'L': parse_bool,
|
|
67
|
+
b'M': parse_memo,
|
|
68
|
+
}
|
dbf_light/cli.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from dbf_light import VERSION, open_db
|
|
4
|
+
|
|
5
|
+
arg_db = click.argument('db', type=click.Path(dir_okay=False))
|
|
6
|
+
opt_encoding = click.option('--encoding', help='Encoding used by DB')
|
|
7
|
+
opt_zipped = click.option(
|
|
8
|
+
'--zip', help='Zip filename containing DBF', type=click.Path(exists=True, dir_okay=False))
|
|
9
|
+
opt_nocase = click.option(
|
|
10
|
+
'-i', '--case-insensitive', help='Do not honor DB filename case', is_flag=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
@click.version_option(version=VERSION)
|
|
15
|
+
def entry_point():
|
|
16
|
+
"""dbf_light command line utilities."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@entry_point.command()
|
|
20
|
+
@arg_db
|
|
21
|
+
@opt_encoding
|
|
22
|
+
@click.option('--no-limit', help='Do not limit number of rows to output.', is_flag=True)
|
|
23
|
+
@opt_zipped
|
|
24
|
+
@opt_nocase
|
|
25
|
+
def show(db, encoding, no_limit, zip, case_insensitive):
|
|
26
|
+
"""Show .dbf file contents (rows)."""
|
|
27
|
+
|
|
28
|
+
limit = 15
|
|
29
|
+
|
|
30
|
+
if no_limit:
|
|
31
|
+
limit = float('inf')
|
|
32
|
+
|
|
33
|
+
with open_db(db, zip, encoding=encoding, case_sensitive=not case_insensitive) as dbf:
|
|
34
|
+
for idx, row in enumerate(dbf, 1):
|
|
35
|
+
click.secho('')
|
|
36
|
+
|
|
37
|
+
for key, val in row._asdict().items():
|
|
38
|
+
click.secho(f' {key}: {val}')
|
|
39
|
+
|
|
40
|
+
if idx == limit:
|
|
41
|
+
click.secho(
|
|
42
|
+
f'Note: Output is limited to {limit} rows. Use --no-limit option to bypass.', fg='red')
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@entry_point.command()
|
|
47
|
+
@arg_db
|
|
48
|
+
@opt_zipped
|
|
49
|
+
@opt_nocase
|
|
50
|
+
def describe(db, zip, case_insensitive):
|
|
51
|
+
"""Show .dbf file statistics."""
|
|
52
|
+
|
|
53
|
+
with open_db(db, zip, case_sensitive=not case_insensitive) as dbf:
|
|
54
|
+
click.secho(f'Rows count: {dbf.prolog.records_count}')
|
|
55
|
+
click.secho('Fields:')
|
|
56
|
+
for field in dbf.fields:
|
|
57
|
+
click.secho(f' {field.type}: {field}')
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main():
|
|
61
|
+
entry_point(obj={})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == '__main__':
|
|
65
|
+
main()
|
dbf_light/definitions.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
|
|
2
|
+
import struct
|
|
3
|
+
|
|
4
|
+
from ._base import Definition
|
|
5
|
+
from .cast import CAST_MAP
|
|
6
|
+
from .utils import bytes_to_int
|
|
7
|
+
|
|
8
|
+
CODE_PAGES = {
|
|
9
|
+
1: 'cp437',
|
|
10
|
+
2: 'cp850',
|
|
11
|
+
3: 'cp1252',
|
|
12
|
+
87: 'ascii',
|
|
13
|
+
101: 'cp866',
|
|
14
|
+
201: 'cp1251',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Field(Definition):
|
|
19
|
+
|
|
20
|
+
_definition = (
|
|
21
|
+
('name', '11s'),
|
|
22
|
+
('type', 'c'),
|
|
23
|
+
('reserved1', '4s'),
|
|
24
|
+
('len', 'c'),
|
|
25
|
+
('decimal_count', 'B'),
|
|
26
|
+
('reserved2', '13s'),
|
|
27
|
+
('mdx', '?'), # dBASE IV only
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_file(cls, fileobj, name_lower=True, encoding=None):
|
|
32
|
+
field = super().from_file(fileobj)
|
|
33
|
+
|
|
34
|
+
if name_lower:
|
|
35
|
+
field.set_name(field.data['name'].lower())
|
|
36
|
+
|
|
37
|
+
field.encoding = encoding
|
|
38
|
+
|
|
39
|
+
return field
|
|
40
|
+
|
|
41
|
+
def __init__(self, data):
|
|
42
|
+
super().__init__(data)
|
|
43
|
+
data = self.data
|
|
44
|
+
|
|
45
|
+
data['name'] = data['name'].decode().rstrip('\0')
|
|
46
|
+
|
|
47
|
+
self.name = data['name']
|
|
48
|
+
self.len = bytes_to_int(data['len'])
|
|
49
|
+
self.type = data['type']
|
|
50
|
+
|
|
51
|
+
def __str__(self):
|
|
52
|
+
return self.name
|
|
53
|
+
|
|
54
|
+
def set_name(self, name):
|
|
55
|
+
self.name = self.data['name'] = name
|
|
56
|
+
|
|
57
|
+
def cast(self, value):
|
|
58
|
+
return CAST_MAP.get(self.data['type'], lambda field, val: val)(self, value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Prolog(Definition):
|
|
62
|
+
|
|
63
|
+
cls_field = Field
|
|
64
|
+
|
|
65
|
+
_definition = (
|
|
66
|
+
('y', 'c'),
|
|
67
|
+
('m', 'c'),
|
|
68
|
+
('d', 'c'),
|
|
69
|
+
('records', 'I'),
|
|
70
|
+
('len_head', 'H'),
|
|
71
|
+
('len_rec', 'H'),
|
|
72
|
+
('reserved1', '2s'),
|
|
73
|
+
('incomplete_tr', '?'), # dBASE IV
|
|
74
|
+
('encrypted', '?'), # dBASE IV
|
|
75
|
+
('reserved2', '12s'), # Reserved for multi-user processing.
|
|
76
|
+
('mdx_exists', '?'),
|
|
77
|
+
('code_page', 'B'), # dBASE IV, Visual FoxPro, XBase
|
|
78
|
+
('reserved3', '2s'),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def __init__(self, data):
|
|
82
|
+
super().__init__(data)
|
|
83
|
+
|
|
84
|
+
data = self.data
|
|
85
|
+
|
|
86
|
+
self.records_count = data['records']
|
|
87
|
+
|
|
88
|
+
# +2 -> 1 byte for signature + 1 step
|
|
89
|
+
count = (data['len_head'] - (self._struct_size+2)) / self.cls_field._struct_size
|
|
90
|
+
|
|
91
|
+
assert count.is_integer(), 'Unexpected records count. It seems that file format is misinterpreted.'
|
|
92
|
+
|
|
93
|
+
self.fields_count = int(count)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def encoding(self):
|
|
97
|
+
return CODE_PAGES.get(self.data['code_page'])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class FieldFoxpro(Field):
|
|
101
|
+
|
|
102
|
+
_definition = (
|
|
103
|
+
('name', '32s'),
|
|
104
|
+
('type', 'c'),
|
|
105
|
+
('len', 'c'),
|
|
106
|
+
('decimal_count', 'B'),
|
|
107
|
+
('reserved1', '2s'),
|
|
108
|
+
('mdx', '?'),
|
|
109
|
+
('reserved2', '2s'),
|
|
110
|
+
('autoinc', 'I'),
|
|
111
|
+
('reserved2', '4s'),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class PrologFoxpro(Prolog):
|
|
116
|
+
|
|
117
|
+
cls_field = FieldFoxpro
|
|
118
|
+
|
|
119
|
+
_definition = (
|
|
120
|
+
*Prolog._definition,
|
|
121
|
+
('language_driver', '32s'),
|
|
122
|
+
('reserved4', '4s'),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
Prolog.init_cache()
|
|
127
|
+
Field.init_cache()
|
|
128
|
+
|
|
129
|
+
PrologFoxpro.init_cache()
|
|
130
|
+
FieldFoxpro.init_cache()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_format_description(fileobj):
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
https://www.loc.gov/preservation/digital/formats/fdd/fdd000325.shtml
|
|
137
|
+
https://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_STRUCT
|
|
138
|
+
|
|
139
|
+
http://www.autopark.ru/ASBProgrammerGuide/DBFSTRUC.HTM
|
|
140
|
+
|
|
141
|
+
Сигнатура СУБД Описание
|
|
142
|
+
3 0x03 00000011 dBASE III, dBASE IV, dBASE 5, dBASE 7, FoxPro, FoxBASE+ Таблица без memo-полей
|
|
143
|
+
4 0x04 00000100 dBASE 7 Таблица без memo-полей
|
|
144
|
+
67 0x43 01000011 dBASE IV, dBASE 5 SQL-таблица dBASE IV без memo-полей
|
|
145
|
+
99 0x63 01100011 dBASE IV, dBASE 5 Системная SQL-таблица dBASE IV без memo-полей
|
|
146
|
+
131 0x83 10000011 dBASE III, FoxBASE+, FoxPro Таблица с memo-полями .DBT
|
|
147
|
+
139 0x8B 10001011 dBASE IV, dBASE 5 Таблица с memo-полями .DBT формата dBASE IV
|
|
148
|
+
140 0x8C 10001100 dBASE 7 Таблица с memo-полями .DBT формата dBASE IV
|
|
149
|
+
203 0xE5 11100101 SMT Таблица с memo-полями .SMT
|
|
150
|
+
235 0xEB 11101011 dBASE IV, dBASE 5 Системная SQL-таблица dBASE IV с memo-полями .DBT
|
|
151
|
+
|
|
152
|
+
Custom signatures:
|
|
153
|
+
|
|
154
|
+
2 0x02 00000010 FoxBASE Таблица без memo-полей
|
|
155
|
+
48 0x30 00110000 Visual FoxPro Таблица (признак наличия memo-поля .FPT не предусмотрен )
|
|
156
|
+
49 0x31 00110001 Visual FoxPro Таблица с автоинкрементными полями
|
|
157
|
+
203 0xCB 11001011 dBASE IV, dBASE 5 SQL-таблица dBASE IV с memo-полями .DBT
|
|
158
|
+
245 0xF5 11110101 FoxPro Таблица с memo-полями .FPT
|
|
159
|
+
251 0xFB 11111011 FoxBASE Таблица с memo-полями .???
|
|
160
|
+
|
|
161
|
+
:param fileobj:
|
|
162
|
+
:rtype: tuple(Prolog, bytes)
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
signature = struct.unpack('<B', fileobj.read(1))[0]
|
|
166
|
+
|
|
167
|
+
if signature in {48, 49}: # pragma: nocover
|
|
168
|
+
# todo return PrologFoxpro when it's debugged (records count error exists).
|
|
169
|
+
raise NotImplementedError('FoxPro dbf support is not implemented.')
|
|
170
|
+
|
|
171
|
+
return Prolog, signature
|
dbf_light/exceptions.py
ADDED
dbf_light/light.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
|
|
2
|
+
import struct
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from functools import partial
|
|
7
|
+
from zipfile import ZipFile
|
|
8
|
+
|
|
9
|
+
from .utils import string_types, pick_name
|
|
10
|
+
from .definitions import get_format_description
|
|
11
|
+
from .exceptions import DbfException
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Dbf:
|
|
15
|
+
"""Represents data from .dbf file."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, fileobj, encoding=None, fieldnames_lower=True):
|
|
18
|
+
"""
|
|
19
|
+
:param fileobj: Python file-like object containing .dbf file data.
|
|
20
|
+
|
|
21
|
+
:param str|unicode encoding: Encoding used by DB.
|
|
22
|
+
This will be used if there's no encoding information in the DB itself.
|
|
23
|
+
|
|
24
|
+
:param bool fieldnames_lower: Lowercase field names.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
self._fileobj = fileobj
|
|
28
|
+
self._lower = fieldnames_lower
|
|
29
|
+
|
|
30
|
+
cls_prolog, self.signature = get_format_description(fileobj)
|
|
31
|
+
|
|
32
|
+
self.cls_prolog = cls_prolog
|
|
33
|
+
self.cls_field = cls_prolog.cls_field
|
|
34
|
+
|
|
35
|
+
prolog = self.cls_prolog.from_file(fileobj)
|
|
36
|
+
self.prolog = prolog
|
|
37
|
+
|
|
38
|
+
if encoding is None:
|
|
39
|
+
encoding = prolog.encoding
|
|
40
|
+
|
|
41
|
+
self._encoding = encoding or 'cp866'
|
|
42
|
+
|
|
43
|
+
self.fields, self.cls_row = self._read_fields()
|
|
44
|
+
|
|
45
|
+
def __iter__(self):
|
|
46
|
+
return iter(self.iter_rows())
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
@contextmanager
|
|
50
|
+
def open(cls, dbfile, encoding=None, fieldnames_lower=True, case_sensitive=True):
|
|
51
|
+
"""Context manager. Allows opening a .dbf file.
|
|
52
|
+
|
|
53
|
+
.. code-block::
|
|
54
|
+
|
|
55
|
+
with Dbf.open('some.dbf') as dbf:
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
:param str|unicode|file dbfile: .dbf filepath or a file-like object.
|
|
59
|
+
|
|
60
|
+
:param str|unicode encoding: Encoding used by DB.
|
|
61
|
+
This will be used if there's no encoding information in the DB itself.
|
|
62
|
+
|
|
63
|
+
:param bool fieldnames_lower: Lowercase field names.
|
|
64
|
+
|
|
65
|
+
:param bool case_sensitive: Whether DB filename is case sensitive.
|
|
66
|
+
|
|
67
|
+
:rtype: Dbf
|
|
68
|
+
"""
|
|
69
|
+
if not case_sensitive:
|
|
70
|
+
if isinstance(dbfile, string_types):
|
|
71
|
+
dbfile = pick_name(dbfile, Path(dbfile).parent.iterdir())
|
|
72
|
+
|
|
73
|
+
with Path(dbfile).open('rb') as f:
|
|
74
|
+
yield cls(f, encoding=encoding, fieldnames_lower=fieldnames_lower)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
@contextmanager
|
|
78
|
+
def open_zip(cls, dbname, zipped, encoding=None, fieldnames_lower=True, case_sensitive=True):
|
|
79
|
+
"""Context manager. Allows opening a .dbf file from zip archive.
|
|
80
|
+
|
|
81
|
+
.. code-block::
|
|
82
|
+
|
|
83
|
+
with Dbf.open_zip('some.dbf', 'myarch.zip') as dbf:
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
:param str|unicode dbname: .dbf file name
|
|
87
|
+
|
|
88
|
+
:param str|unicode|file zipped: .zip file path or a file-like object.
|
|
89
|
+
|
|
90
|
+
:param str|unicode encoding: Encoding used by DB.
|
|
91
|
+
This will be used if there's no encoding information in the DB itself.
|
|
92
|
+
|
|
93
|
+
:param bool fieldnames_lower: Lowercase field names.
|
|
94
|
+
|
|
95
|
+
:param bool case_sensitive: Whether DB filename is case sensitive.
|
|
96
|
+
|
|
97
|
+
:rtype: Dbf
|
|
98
|
+
"""
|
|
99
|
+
with ZipFile(zipped, 'r') as zip_:
|
|
100
|
+
|
|
101
|
+
if not case_sensitive:
|
|
102
|
+
dbname = pick_name(dbname, zip_.namelist())
|
|
103
|
+
|
|
104
|
+
with zip_.open(dbname) as f:
|
|
105
|
+
yield cls(f, encoding=encoding, fieldnames_lower=fieldnames_lower)
|
|
106
|
+
|
|
107
|
+
def iter_rows(self):
|
|
108
|
+
"""Generator reading .dbf row one by one.
|
|
109
|
+
|
|
110
|
+
Yields named tuple Row object.
|
|
111
|
+
|
|
112
|
+
:rtype: Row
|
|
113
|
+
"""
|
|
114
|
+
fileobj = self._fileobj
|
|
115
|
+
cls_row = self.cls_row
|
|
116
|
+
fields = self.fields
|
|
117
|
+
|
|
118
|
+
for _ in range(self.prolog.records_count):
|
|
119
|
+
data = fileobj.read(1)
|
|
120
|
+
|
|
121
|
+
marker = struct.unpack('<1s', data)[0]
|
|
122
|
+
is_deleted = marker == b'*'
|
|
123
|
+
|
|
124
|
+
if is_deleted:
|
|
125
|
+
# Skip the record bytes to keep the stream in sync.
|
|
126
|
+
fileobj.read(sum(field.len for field in fields))
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
row_values = []
|
|
130
|
+
for field in fields:
|
|
131
|
+
val = field.cast(fileobj.read(field.len))
|
|
132
|
+
row_values.append(val)
|
|
133
|
+
|
|
134
|
+
yield cls_row(*row_values)
|
|
135
|
+
|
|
136
|
+
def _read_fields(self):
|
|
137
|
+
fh = self._fileobj
|
|
138
|
+
field_from_file = partial(self.cls_field.from_file, name_lower=self._lower, encoding=self._encoding)
|
|
139
|
+
|
|
140
|
+
fields = []
|
|
141
|
+
field_names = []
|
|
142
|
+
for _ in range(self.prolog.fields_count):
|
|
143
|
+
field = field_from_file(fh) # type: Field
|
|
144
|
+
name = field.name
|
|
145
|
+
|
|
146
|
+
if name in field_names:
|
|
147
|
+
# Handle duplicates.
|
|
148
|
+
name = name + '_'
|
|
149
|
+
field.set_name(name)
|
|
150
|
+
|
|
151
|
+
fields.append(field)
|
|
152
|
+
field_names.append(name)
|
|
153
|
+
|
|
154
|
+
terminator = struct.unpack('<c', fh.read(1))[0]
|
|
155
|
+
|
|
156
|
+
if terminator != b'\r':
|
|
157
|
+
raise DbfException(
|
|
158
|
+
'Header termination byte not found. '
|
|
159
|
+
f'Seems to be an unsupported format. Signature: {self.signature}')
|
|
160
|
+
|
|
161
|
+
cls_row = namedtuple('Row', field_names)
|
|
162
|
+
|
|
163
|
+
return fields, cls_row
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@contextmanager
|
|
167
|
+
def open_db(db, zipped=None, encoding=None, fieldnames_lower=True, case_sensitive=True):
|
|
168
|
+
"""Context manager. Allows reading DBF file (maybe even from zip).
|
|
169
|
+
|
|
170
|
+
:param str|unicode|file db: .dbf file name or a file-like object.
|
|
171
|
+
|
|
172
|
+
:param str|unicode zipped: .zip file path or a file-like object.
|
|
173
|
+
|
|
174
|
+
:param str|unicode encoding: Encoding used by DB.
|
|
175
|
+
This will be used if there's no encoding information in the DB itself.
|
|
176
|
+
|
|
177
|
+
:param bool fieldnames_lower: Lowercase field names.
|
|
178
|
+
|
|
179
|
+
:param bool case_sensitive: Whether DB filename is case sensitive.
|
|
180
|
+
|
|
181
|
+
:rtype: Dbf
|
|
182
|
+
"""
|
|
183
|
+
kwargs = {
|
|
184
|
+
'encoding': encoding,
|
|
185
|
+
'fieldnames_lower': fieldnames_lower,
|
|
186
|
+
'case_sensitive': case_sensitive,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if zipped:
|
|
190
|
+
with Dbf.open_zip(db, zipped, **kwargs) as dbf:
|
|
191
|
+
yield dbf
|
|
192
|
+
|
|
193
|
+
else:
|
|
194
|
+
with Dbf.open(db, **kwargs) as dbf:
|
|
195
|
+
yield dbf
|
dbf_light/utils.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import codecs
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
string_types = str,
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def bytes_to_int(val):
|
|
8
|
+
return int(codecs.encode(val, 'hex'), 16)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def pick_name(filename, candidates):
|
|
12
|
+
filedir = Path(filename).parent
|
|
13
|
+
name_lower = Path(filename).name.lower()
|
|
14
|
+
name_candidate = None
|
|
15
|
+
|
|
16
|
+
for name in candidates:
|
|
17
|
+
|
|
18
|
+
if name_lower == Path(name).name.lower():
|
|
19
|
+
name_candidate = str(filedir / name)
|
|
20
|
+
break
|
|
21
|
+
|
|
22
|
+
if name_candidate:
|
|
23
|
+
filename = name_candidate
|
|
24
|
+
|
|
25
|
+
return filename
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dbf_light
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Light and easy DBF reader
|
|
5
|
+
Project-URL: Homepage, https://github.com/idlesign/dbf_light
|
|
6
|
+
Author-email: Igor Starikov <idlesign@yandex.ru>
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: dbf,reader,xbase
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Provides-Extra: cli
|
|
23
|
+
Requires-Dist: click; extra == 'cli'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# dbf_light
|
|
27
|
+
|
|
28
|
+
<https://github.com/idlesign/dbf_light>
|
|
29
|
+
|
|
30
|
+
[](https://pypi.python.org/pypi/dbf_light)
|
|
31
|
+
[](https://pypi.python.org/pypi/dbf_light)
|
|
32
|
+
[](https://coveralls.io/r/idlesign/dbf_light)
|
|
33
|
+
|
|
34
|
+
## Description
|
|
35
|
+
|
|
36
|
+
*Light and easy DBF reader*
|
|
37
|
+
|
|
38
|
+
No fancy stuff, just DBF reading for most common format versions.
|
|
39
|
+
|
|
40
|
+
* Uses `namedtuple` for row representation and iterative row reading to minimize memory usage;
|
|
41
|
+
* Works fine with cyrillic (supports KLADR and CBRF databases);
|
|
42
|
+
* Reads .dbf from zip files.
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from dbf_light import Dbf
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
with Dbf.open('some.dbf') as dbf:
|
|
51
|
+
|
|
52
|
+
for field in dbf.fields:
|
|
53
|
+
print('Field: %s' % field)
|
|
54
|
+
|
|
55
|
+
print('Rows (%s):' % dbf.prolog.records_count)
|
|
56
|
+
|
|
57
|
+
for row in dbf:
|
|
58
|
+
print(row)
|
|
59
|
+
|
|
60
|
+
# Read `some.dbf` from zip (ignoring filename case):
|
|
61
|
+
with Dbf.open_zip('some.dbf', 'here/myarch.zip', case_sensitive=False) as dbf:
|
|
62
|
+
...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## CLI
|
|
66
|
+
|
|
67
|
+
Requires `click` package (can be installed with: `pip install dbf_light[cli]`).
|
|
68
|
+
|
|
69
|
+
```shell
|
|
70
|
+
$ dbf_light describe myfile.dbf
|
|
71
|
+
$ dbf_light show myfile.dbf
|
|
72
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
dbf_light/__init__.py,sha256=qsrGnXM_fD2CYQpT4uPYRIkUMovCkGMRSmgNKSNBOiI,52
|
|
2
|
+
dbf_light/_base.py,sha256=5yMCFJ37RTHt4HbhAUL63CuwXytlf-8ELaZP43iBHRY,804
|
|
3
|
+
dbf_light/cast.py,sha256=LrN4VePnLSivROLK5bK09DI_ghh4150lurNgNbHf4pc,1076
|
|
4
|
+
dbf_light/cli.py,sha256=-HFmItVLptvNCGUuZW53mfNuv6mcSzz_yCdhW7kBGVU,1775
|
|
5
|
+
dbf_light/definitions.py,sha256=qX_A7lJDXyhtWhTr20HlT0eWgRyVpqDarO4ZJ2XNNoQ,4908
|
|
6
|
+
dbf_light/exceptions.py,sha256=JaBVZRNCrqoelKhoF-kYIp-1bJB8oJS8_MQkyddCTZ8,59
|
|
7
|
+
dbf_light/light.py,sha256=TP4xN50kCsX77v7Ldi7_Xo19Ye13Y0oUmK3ZAs71vvA,5865
|
|
8
|
+
dbf_light/utils.py,sha256=gzleSaCDQr08To0FHO598TVqpY_bYJ4RVRNgnbzxwYU,503
|
|
9
|
+
dbf_light-1.0.1.dist-info/METADATA,sha256=2f_-N8BPLgthNX30RGYDIs3z3Pws4yd2wL_iVDpDUAQ,2160
|
|
10
|
+
dbf_light-1.0.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
11
|
+
dbf_light-1.0.1.dist-info/entry_points.txt,sha256=lRvm2Wf09GsjloiOeIxJGi3ELMHH9Vd8jQOF2X-eJVk,49
|
|
12
|
+
dbf_light-1.0.1.dist-info/licenses/LICENSE,sha256=CcFIaRyMMZFt-LjYesCkntdv51hrxgOWbuzgD3jhLl4,1502
|
|
13
|
+
dbf_light-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Copyright (c) 2018-2026, Igor `idle sign` Starikov
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
* Neither the name of the dbf_light nor the names of its
|
|
15
|
+
contributors may be used to endorse or promote products derived from
|
|
16
|
+
this software without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
28
|
+
|