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 ADDED
@@ -0,0 +1,4 @@
1
+ from .light import Dbf, open_db
2
+
3
+
4
+ VERSION = '1.0.1'
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()
@@ -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
@@ -0,0 +1,4 @@
1
+
2
+
3
+ class DbfException(Exception):
4
+ """Base exception."""
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
+ [![release](https://img.shields.io/pypi/v/dbf_light.svg)](https://pypi.python.org/pypi/dbf_light)
31
+ [![lic](https://img.shields.io/pypi/l/dbf_light.svg)](https://pypi.python.org/pypi/dbf_light)
32
+ [![coverage](https://img.shields.io/coveralls/idlesign/dbf_light/master.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dbf_light = dbf_light.cli:main
@@ -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
+