Skip to content

Commit 904a3a3

Browse files
authored
Merge pull request #7 from bbayles/new-release
Preparing for a new release (with Python 3 support)
2 parents 53bedee + 64ddeee commit 904a3a3

20 files changed

Lines changed: 408 additions & 177 deletions

.gitignore

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
wheels/
23+
*.egg-info/
24+
.installed.cfg
25+
*.egg
26+
MANIFEST
27+
28+
# PyInstaller
29+
# Usually these files are written by a python script from a template
30+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
31+
*.manifest
32+
*.spec
33+
34+
# Installer logs
35+
pip-log.txt
36+
pip-delete-this-directory.txt
37+
38+
# Unit test / coverage reports
39+
htmlcov/
40+
.tox/
41+
.nox/
42+
.coverage
43+
.coverage.*
44+
.cache
45+
nosetests.xml
46+
coverage.xml
47+
*.cover
48+
.hypothesis/
49+
.pytest_cache/
50+
51+
# Translations
52+
*.mo
53+
*.pot
54+
55+
# Django stuff:
56+
*.log
57+
local_settings.py
58+
db.sqlite3
59+
60+
# Flask stuff:
61+
instance/
62+
.webassets-cache
63+
64+
# Scrapy stuff:
65+
.scrapy
66+
67+
# Sphinx documentation
68+
docs/_build/
69+
70+
# PyBuilder
71+
target/
72+
73+
# Jupyter Notebook
74+
.ipynb_checkpoints
75+
76+
# IPython
77+
profile_default/
78+
ipython_config.py
79+
80+
# pyenv
81+
.python-version
82+
83+
# celery beat schedule file
84+
celerybeat-schedule
85+
86+
# SageMath parsed files
87+
*.sage.py
88+
89+
# Environments
90+
.env
91+
.venv
92+
env/
93+
venv/
94+
ENV/
95+
env.bak/
96+
venv.bak/
97+
98+
# Spyder project settings
99+
.spyderproject
100+
.spyproject
101+
102+
# Rope project settings
103+
.ropeproject
104+
105+
# mkdocs documentation
106+
/site
107+
108+
# mypy
109+
.mypy_cache/
110+
.dmypy.json
111+
dmypy.json

README.md

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,55 @@
1-
Manipulate DJB's Constant Database files. These are 2 level disk-based hash tables that efficiently handle thousands of keys, while remaining space-efficient.
1+
## Introduction
22

3-
http://cr.yp.to/cdb.html
3+
`python-pure-cdb` (`pure-cdb` on [PyPI](https://pypi.org/project/pure-cdb/)) is a Python library for reading and writing djb's "constant database" files. These disk-based associative arrays that allow efficient storage and retrieval of thousands of keys. For more information on `cdb`, see [djb's page](https://cr.yp.to/cdb.html) and [Wikipedia](https://en.wikipedia.org/wiki/Cdb_(software)).
44

5-
Note the Reader class reads the entire CDB into memory.
5+
### Installation and usage
66

7-
------
7+
Install `pure-cdb` with `pip`:
8+
9+
```
10+
pip install pure-cdb
11+
```
12+
13+
Then import `cdblib` to access the `Reader` and `Writer` classes.
14+
15+
---
16+
17+
`Reader` provides an interface to a `cdb` file's contents - use the `.iterkeys()` method to iterate over keys, and the `.get()` method to retrieve a key's value.
18+
19+
```python
20+
import cdblib
21+
22+
with open('info.cdb', 'rb') as f:
23+
data = f.read()
24+
25+
reader = cdblib.Reader(data)
26+
27+
reader.get(b'hello')
28+
```
29+
30+
All keys are `bytes` objects (`str` in Python 2). By default all values are also retrieved as `bytes` objects, but the `.getstring()` and `.getint()` methods can be used to retrieve decoded strings and integers, respectively.
31+
32+
---
33+
34+
`Writer` allows for creating new `cdb` files. Use the `.put()` method to insert a key / value pair, and the `.finalize()` method to create the CDB structure.
35+
36+
```python
37+
with open('/home/bo/Desktop/new.cdb', 'wb') as f:
38+
writer = cdblib.Writer(f)
39+
writer.put(b'key', b'value')
40+
writer.finalize()
41+
```
42+
43+
As with the `Reader` class, keys and values are `bytes` objects. The `.putstrings()` and `.putint()` methods can be used to insert encoded text data and integers, respectively.
44+
45+
---
46+
47+
### Remarks on usage
848

949
Constant databases have the desirable property of requiring low overhead to open. This makes them ideal for use in environments where it's not always possible to have data hanging around in RAM awaiting use, for example in a CGI script or Google App Engine.
1050

1151
On App Engine, reading a 700kb database into memory from the filesystem costs around 90ms, after which the cost of individual lookups is negligible (one simple benchmark achieved 94k lookups/sec running on App Engine, and that was using `djb_hash()`). This makes CDBs ideal for storing many small keys that are infrequently accessed, for example in per-language corpora containing 10k+ internationalized strings (my original use case).
1252

1353
The format might also be useful as a composite file storage alternative to the `zipfile` module. Since the Reader interface only requires a file-like object, it is possible to wrap a string stored in a Datastore entity in a `cStringIO` object, allowing convenient access to it as a CDB.
1454

15-
When storing many thousands of keys in a Datastore entity, CDBs might enable you to bypass the "indexed fields" limit of the `Expando` class without resorting to `pickle`, as well as reduce deserialization overhead when only a few keys of such an entity are normally accessed and rarely updated.
55+
When storing many thousands of keys in a Datastore entity, CDBs might enable you to bypass the "indexed fields" limit of the `Expando` class without resorting to `pickle`, as well as reduce deserialization overhead when only a few keys of such an entity are normally accessed and rarely updated.

appengine/cdblib.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

appengine/cdplib.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../cdblib/cdblib.py

appengine/testdata

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
../testdata
1+
../tests/testdata

cdblib/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from __future__ import unicode_literals
2+
3+
from .djb_hash import djb_hash
4+
from .cdblib import Reader, Reader64, Writer, Writer64
5+
6+
7+
__all__ = ['djb_hash', 'Reader', 'Reader64', 'Writer', 'Writer64']

_cdblib.c renamed to cdblib/_djb_hash.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ static /*const*/ PyMethodDef methods[] = {
2828

2929

3030
PyMODINIT_FUNC
31-
init_cdblib(void)
31+
init_djb_hash(void)
3232
{
33-
Py_InitModule("_cdblib", methods);
33+
Py_InitModule("_djb_hash", methods);
3434
}

cdblib.py renamed to cdblib/cdblib.py

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,16 @@
55
http://cr.yp.to/cdb.html
66
77
'''
8+
from __future__ import unicode_literals
89

9-
from _struct import Struct
10+
from struct import Struct
1011
from itertools import chain
1112

13+
import six
14+
from six.moves import range
1215

13-
def py_djb_hash(s):
14-
'''Return the value of DJB's hash function for the given 8-bit string.'''
15-
h = 5381
16-
for c in s:
17-
h = (((h << 5) + h) ^ ord(c)) & 0xffffffff
18-
return h
16+
from .djb_hash import djb_hash
1917

20-
try:
21-
from _cdblib import djb_hash
22-
except ImportError:
23-
djb_hash = py_djb_hash
2418

2519
read_2_le4 = Struct('<LL').unpack
2620
read_2_le8 = Struct('<QQ').unpack
@@ -44,7 +38,7 @@ def __init__(self, data, hashfn=djb_hash):
4438
self.data = data
4539
self.hashfn = hashfn
4640
self.index = [self.read_pair(data[i:i+self.pair_size])
47-
for i in xrange(0, 256*self.pair_size, self.pair_size)]
41+
for i in range(0, 256*self.pair_size, self.pair_size)]
4842
self.table_start = min(p[0] for p in self.index)
4943
# Assume load load factor is 0.5 like official CDB.
5044
self.length = sum(p[1] >> 1 for p in self.index)
@@ -111,8 +105,8 @@ def gets(self, key):
111105
end = start + (nslots * self.pair_size)
112106
slot_off = start + (((h >> 8) % nslots) * self.pair_size)
113107

114-
for pos in chain(xrange(slot_off, end, self.pair_size),
115-
xrange(start, slot_off, self.pair_size)):
108+
for pos in chain(range(slot_off, end, self.pair_size),
109+
range(start, slot_off, self.pair_size)):
116110
rec_h, rec_pos = self.read_pair(
117111
self.data[pos:pos+self.pair_size]
118112
)
@@ -132,7 +126,7 @@ def gets(self, key):
132126
def get(self, key, default=None):
133127
'''Get the first value for key, returning default if missing.'''
134128
# Avoid exception catch when handling default case; much faster.
135-
return chain(self.gets(key), (default,)).next()
129+
return next(chain(self.gets(key), (default,)))
136130

137131
def getint(self, key, default=None, base=0):
138132
'''Get the first value for key converted it to an int, returning
@@ -182,12 +176,12 @@ def __init__(self, fp, hashfn=djb_hash):
182176
self.fp = fp
183177
self.hashfn = hashfn
184178

185-
fp.write('\x00' * (256 * self.pair_size))
186-
self._unordered = [[] for i in xrange(256)]
179+
fp.write(b'\x00' * (256 * self.pair_size))
180+
self._unordered = [[] for i in range(256)]
187181

188-
def put(self, key, value=''):
182+
def put(self, key, value=b''):
189183
'''Write a string key/value pair to the output file.'''
190-
assert type(key) is str and type(value) is str
184+
assert type(key) is six.binary_type and type(value) is six.binary_type
191185

192186
pos = self.fp.tell()
193187
self.fp.write(self.write_pair(len(key), len(value)))
@@ -206,22 +200,22 @@ def puts(self, key, values):
206200
def putint(self, key, value):
207201
'''Write an integer as a base-10 string associated with the given key
208202
to the output file.'''
209-
self.put(key, str(value))
203+
self.put(key, str(value).encode('ascii'))
210204

211205
def putints(self, key, values):
212206
'''Write zero or more integers for the same key to the output file.
213207
Equivalent to calling putint() in a loop.'''
214-
self.puts(key, (str(value) for value in values))
208+
self.puts(key, (str(value).encode('ascii') for value in values))
215209

216210
def putstring(self, key, value, encoding='utf-8'):
217211
'''Write a unicode string associated with the given key to the output
218212
file after encoding it as UTF-8 or the given encoding.'''
219-
self.put(key, unicode.encode(value, encoding))
213+
self.put(key, six.text_type.encode(value, encoding))
220214

221215
def putstrings(self, key, values, encoding='utf-8'):
222216
'''Write zero or more unicode strings to the output file. Equivalent to
223217
calling putstring() in a loop.'''
224-
self.puts(key, (unicode.encode(value, encoding) for value in values))
218+
self.puts(key, (six.text_type.encode(value, encoding) for value in values))
225219

226220
def finalize(self):
227221
'''Write the final hash tables to the output file, and write out its
@@ -232,7 +226,7 @@ def finalize(self):
232226
ordered = [(0, 0)] * length
233227
for pair in tbl:
234228
where = (pair[0] >> 8) % length
235-
for i in chain(xrange(where, length), xrange(0, where)):
229+
for i in chain(range(where, length), range(0, where)):
236230
if not ordered[i][0]:
237231
ordered[i] = pair
238232
break

0 commit comments

Comments
 (0)