BloomFilter-py 1.2.0__tar.gz → 1.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bloomfilter-py
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Yet another bloomfilter implementation in Python
5
5
  Author-email: OldPanda <me@old-panda.com>
6
6
  License: MIT License
@@ -62,6 +62,11 @@ Dynamic: license-file
62
62
  ## Overview
63
63
  Yet another Bloomfilter implementation in Python, compatible with Java's Guava library.
64
64
 
65
+ Compatibility targets Guava's compact `BloomFilter.writeTo` / `readFrom` format.
66
+ Java `ObjectOutputStream` / `ObjectInputStream` serialization is a different
67
+ format and is not supported. The compact format does not include the funnel;
68
+ callers must provide the matching encoder when loading a filter.
69
+
65
70
  I was looking for a Python library which is capable of reading what Bloomfilter of Java's Guava library serializes and is also able to output byte array which is recognizable by Java. But unfortunately failed. Hence I developed this library by borrowing how Guava implements Bloomfilter serialization/deserialization a lot to deal with Bloomfilters on both Python and Java sides.
66
71
 
67
72
  As for Bloomfilter usage in Java world, please refer to [this post](https://www.baeldung.com/guava-bloom-filter).
@@ -92,6 +97,49 @@ False
92
97
  >>>
93
98
  ```
94
99
 
100
+ ### Guava Funnel Compatibility
101
+
102
+ Guava's compact BloomFilter format stores the hash strategy and bit array, but
103
+ does not store its `Funnel`. To query a filter produced by Guava, select the
104
+ same funnel that was used in Java:
105
+
106
+ ```Python
107
+ from bloomfilter import BloomFilter, INTEGER_FUNNEL
108
+
109
+ with open("guava-filter.out", "rb") as f:
110
+ bloom_filter = BloomFilter.loads(f.read(), funnel=INTEGER_FUNNEL)
111
+ ```
112
+
113
+ The built-in mappings are:
114
+
115
+ | Java funnel | bloomfilter-py funnel |
116
+ | --- | --- |
117
+ | `Funnels.integerFunnel()` | `INTEGER_FUNNEL` |
118
+ | `Funnels.longFunnel()` | `LONG_FUNNEL` |
119
+ | `Funnels.stringFunnel(StandardCharsets.UTF_8)` | `UTF8_STRING_FUNNEL` |
120
+ | `Funnels.stringFunnel(StandardCharsets.UTF_16)` | `StringFunnel("UTF-16")` |
121
+ | `Funnels.stringFunnel(StandardCharsets.UTF_16BE)` | `StringFunnel("UTF-16BE")` |
122
+ | `Funnels.stringFunnel(StandardCharsets.UTF_16LE)` | `StringFunnel("UTF-16LE")` |
123
+ | `Funnels.byteArrayFunnel()` | `BYTE_ARRAY_FUNNEL` |
124
+
125
+ `StringFunnel` supports Java's six standard charsets: US-ASCII, ISO-8859-1,
126
+ UTF-8, UTF-16, UTF-16BE, and UTF-16LE. Custom Guava funnels are not encoded in
127
+ the serialized filter; use a matching custom Python `Funnel` implementation
128
+ when reading them.
129
+
130
+ Use the same funnel when creating a filter that Java will read:
131
+
132
+ ```Python
133
+ from bloomfilter import BloomFilter, LONG_FUNNEL
134
+
135
+ bloom_filter = BloomFilter(500, 0.01, funnel=LONG_FUNNEL)
136
+ bloom_filter.put(1) # Encoded as a Java long even though 1 fits in an int.
137
+ ```
138
+
139
+ Calls that omit `funnel` retain the historical bloomfilter-py behavior, which
140
+ chooses integer width from each value and therefore is not suitable for all
141
+ Guava interoperability scenarios.
142
+
95
143
  ### Serialize Bloomfilter
96
144
  You can easily serialize `BloomFilter` instance to a byte array
97
145
  ```Python
@@ -147,6 +195,66 @@ True
147
195
  True
148
196
  ```
149
197
 
198
+ Deserialization limits the backing bit array to 2³⁰ bits by default. For a
199
+ trusted larger Guava filter, pass an explicit limit to any loading method:
200
+
201
+ ```Python
202
+ bf = BloomFilter.loads(dumps, funnel=INTEGER_FUNNEL, max_num_bits=2**31)
203
+ ```
204
+
205
+ ### Stream I/O
206
+
207
+ Read and write compact Guava data directly with binary streams:
208
+
209
+ ```Python
210
+ with open("guava-filter.out", "rb") as stream:
211
+ bf = BloomFilter.read_from(stream, funnel=INTEGER_FUNNEL)
212
+
213
+ with open("python-filter.out", "wb") as stream:
214
+ bf.write_to(stream)
215
+
216
+ byte_count = bf.serialized_size()
217
+ ```
218
+
219
+ `read_from` reads exactly one filter, leaving any following stream data unread.
220
+ It accepts the same `max_num_bits` limit as `loads`, and validates the header
221
+ before reading the payload. `loads` requires exactly one filter with no trailing
222
+ bytes. Neither stream method closes the caller's stream. Both handle partial
223
+ reads or writes on blocking binary streams.
224
+
225
+ ### Copies, Merging, and Statistics
226
+
227
+ ```Python
228
+ snapshot = bf.copy()
229
+ other = BloomFilter(500, 0.01, funnel=INTEGER_FUNNEL)
230
+ other.put(123)
231
+
232
+ if bf.is_compatible(other):
233
+ bf.put_all(other)
234
+
235
+ false_positive_probability = bf.expected_fpp()
236
+ estimated_distinct_elements = bf.approximate_element_count()
237
+ ```
238
+
239
+ `copy` duplicates the backing bit array and preserves the loading limit. It
240
+ shares the funnel and stateless strategy, as Guava does; custom funnels should
241
+ be immutable. `put_all` performs an in-place union without modifying the source.
242
+ Compatibility requires distinct instances with the same bit-array size, number
243
+ of hashes, strategy, and equal funnels. Custom funnels must define equality
244
+ when separate instances represent the same encoding.
245
+
246
+ Statistics use the current bit occupancy, so they also work on loaded and merged
247
+ filters. A fully saturated filter has an expected FPP of `1.0`; its element count
248
+ cannot be estimated and raises `OverflowError`, corresponding to Guava's
249
+ `ArithmeticException`. Filter equality compares configuration, funnel, and bit
250
+ contents. Mutable filters are unhashable in Python.
251
+
252
+ Unlike Guava's atomic implementation, this library does not guarantee safe
253
+ concurrent mutations. Use an external lock around mutations, queries, copies,
254
+ statistics, and serialization when sharing a filter between threads. Java's
255
+ deprecated predicate aliases and stream collectors are language-specific APIs;
256
+ use `might_contain` (or `in`) and a loop calling `put` in Python.
257
+
150
258
  ## Development and Testing
151
259
 
152
260
  From the repository root, create an isolated virtual environment and install the
@@ -165,6 +273,19 @@ Run the complete test suite:
165
273
  python -m pytest -v
166
274
  ```
167
275
 
276
+ The bidirectional Java integration test requires a JDK and a Guava JAR. Point
277
+ `GUAVA_JAR` at the JAR to compile the included Java helper and verify both
278
+ Guava-to-Python and Python-to-Guava serialization:
279
+
280
+ ```bash
281
+ GUAVA_JAR=/path/to/guava-33.7.2-jre.jar \
282
+ python -m pytest -v tests/test_guava_interop.py
283
+ ```
284
+
285
+ The test is skipped when those prerequisites are unavailable. CI downloads the
286
+ pinned Guava release, verifies its SHA-256 checksum, and runs the test as a
287
+ required separate job.
288
+
168
289
  To run the same formatting, type, and security checks used during development,
169
290
  install the additional tools and execute:
170
291
 
@@ -0,0 +1,245 @@
1
+ # bloomfilter-py
2
+ ![](https://img.shields.io/pypi/v/bloomfilter-py.svg)
3
+ ![](https://img.shields.io/pypi/pyversions/bloomfilter-py.svg)
4
+ [![codecov](https://codecov.io/gh/OldPanda/bloomfilter-py/branch/master/graph/badge.svg?token=RBX1JK7P7O)](https://codecov.io/gh/OldPanda/bloomfilter-py)
5
+ [![Downloads](https://pepy.tech/badge/bloomfilter-py)](https://pepy.tech/project/bloomfilter-py)
6
+
7
+ ## Overview
8
+ Yet another Bloomfilter implementation in Python, compatible with Java's Guava library.
9
+
10
+ Compatibility targets Guava's compact `BloomFilter.writeTo` / `readFrom` format.
11
+ Java `ObjectOutputStream` / `ObjectInputStream` serialization is a different
12
+ format and is not supported. The compact format does not include the funnel;
13
+ callers must provide the matching encoder when loading a filter.
14
+
15
+ I was looking for a Python library which is capable of reading what Bloomfilter of Java's Guava library serializes and is also able to output byte array which is recognizable by Java. But unfortunately failed. Hence I developed this library by borrowing how Guava implements Bloomfilter serialization/deserialization a lot to deal with Bloomfilters on both Python and Java sides.
16
+
17
+ As for Bloomfilter usage in Java world, please refer to [this post](https://www.baeldung.com/guava-bloom-filter).
18
+
19
+ Here's a brief [introduction](https://en.wikipedia.org/wiki/Bloom_filter) to Bloomfilter.
20
+
21
+ ## Requirements
22
+ * Python 3.8+
23
+
24
+ ## Install
25
+ ```
26
+ pip install bloomfilter-py
27
+ ```
28
+
29
+ ## Usage Examples
30
+
31
+ ### Basic Usage
32
+ ```Python
33
+ >>> from bloomfilter import BloomFilter
34
+ >>> bloom_filter = BloomFilter(expected_insertions=500, err_rate=0.01)
35
+ >>> for i in range(100):
36
+ ... bloom_filter.put(i)
37
+ ...
38
+ >>> 1 in bloom_filter
39
+ True
40
+ >>> 100 in bloom_filter
41
+ False
42
+ >>>
43
+ ```
44
+
45
+ ### Guava Funnel Compatibility
46
+
47
+ Guava's compact BloomFilter format stores the hash strategy and bit array, but
48
+ does not store its `Funnel`. To query a filter produced by Guava, select the
49
+ same funnel that was used in Java:
50
+
51
+ ```Python
52
+ from bloomfilter import BloomFilter, INTEGER_FUNNEL
53
+
54
+ with open("guava-filter.out", "rb") as f:
55
+ bloom_filter = BloomFilter.loads(f.read(), funnel=INTEGER_FUNNEL)
56
+ ```
57
+
58
+ The built-in mappings are:
59
+
60
+ | Java funnel | bloomfilter-py funnel |
61
+ | --- | --- |
62
+ | `Funnels.integerFunnel()` | `INTEGER_FUNNEL` |
63
+ | `Funnels.longFunnel()` | `LONG_FUNNEL` |
64
+ | `Funnels.stringFunnel(StandardCharsets.UTF_8)` | `UTF8_STRING_FUNNEL` |
65
+ | `Funnels.stringFunnel(StandardCharsets.UTF_16)` | `StringFunnel("UTF-16")` |
66
+ | `Funnels.stringFunnel(StandardCharsets.UTF_16BE)` | `StringFunnel("UTF-16BE")` |
67
+ | `Funnels.stringFunnel(StandardCharsets.UTF_16LE)` | `StringFunnel("UTF-16LE")` |
68
+ | `Funnels.byteArrayFunnel()` | `BYTE_ARRAY_FUNNEL` |
69
+
70
+ `StringFunnel` supports Java's six standard charsets: US-ASCII, ISO-8859-1,
71
+ UTF-8, UTF-16, UTF-16BE, and UTF-16LE. Custom Guava funnels are not encoded in
72
+ the serialized filter; use a matching custom Python `Funnel` implementation
73
+ when reading them.
74
+
75
+ Use the same funnel when creating a filter that Java will read:
76
+
77
+ ```Python
78
+ from bloomfilter import BloomFilter, LONG_FUNNEL
79
+
80
+ bloom_filter = BloomFilter(500, 0.01, funnel=LONG_FUNNEL)
81
+ bloom_filter.put(1) # Encoded as a Java long even though 1 fits in an int.
82
+ ```
83
+
84
+ Calls that omit `funnel` retain the historical bloomfilter-py behavior, which
85
+ chooses integer width from each value and therefore is not suitable for all
86
+ Guava interoperability scenarios.
87
+
88
+ ### Serialize Bloomfilter
89
+ You can easily serialize `BloomFilter` instance to a byte array
90
+ ```Python
91
+ >>> dumps = bloom_filter.dumps()
92
+ >>> with open("dumps.out", "wb") as f:
93
+ ... f.write(dumps)
94
+ ...
95
+ >>>
96
+ ```
97
+
98
+ or to a hex string
99
+ ```Python
100
+ >>> hex_str = bloom_filter.dumps_to_hex()
101
+ ```
102
+
103
+ or to a base64 encoded bytes
104
+ ```Python
105
+ base64_bytes = bloom_filter.dumps_to_base64()
106
+ ```
107
+
108
+ ### Deserialize Bloomfilter
109
+ And you can easily initialize a `BloomFilter` instance from a byte array
110
+ ```Python
111
+ >>> with open("dumps.out", "rb") as f:
112
+ ... bf = BloomFilter.loads(f.read())
113
+ ...
114
+ >>> 1 in bf
115
+ True
116
+ >>> 100 in bf
117
+ False
118
+ >>>
119
+ ```
120
+
121
+ or from a hex string
122
+ ```Python
123
+ >>> bf = BloomFilter.loads_from_hex(hex_str)
124
+ >>> 1 in bf
125
+ True
126
+ >>> 100 in bf
127
+ False
128
+ ```
129
+
130
+ or from a base64 encoded bytes
131
+ ```Python
132
+ >>> bf = BloomFilter.loads_from_base64(base64_bytes)
133
+ >>> 100 in bf
134
+ False
135
+ >>> 200 in bf
136
+ False
137
+ >>> 1 in bf
138
+ True
139
+ >>> 99 in bf
140
+ True
141
+ ```
142
+
143
+ Deserialization limits the backing bit array to 2³⁰ bits by default. For a
144
+ trusted larger Guava filter, pass an explicit limit to any loading method:
145
+
146
+ ```Python
147
+ bf = BloomFilter.loads(dumps, funnel=INTEGER_FUNNEL, max_num_bits=2**31)
148
+ ```
149
+
150
+ ### Stream I/O
151
+
152
+ Read and write compact Guava data directly with binary streams:
153
+
154
+ ```Python
155
+ with open("guava-filter.out", "rb") as stream:
156
+ bf = BloomFilter.read_from(stream, funnel=INTEGER_FUNNEL)
157
+
158
+ with open("python-filter.out", "wb") as stream:
159
+ bf.write_to(stream)
160
+
161
+ byte_count = bf.serialized_size()
162
+ ```
163
+
164
+ `read_from` reads exactly one filter, leaving any following stream data unread.
165
+ It accepts the same `max_num_bits` limit as `loads`, and validates the header
166
+ before reading the payload. `loads` requires exactly one filter with no trailing
167
+ bytes. Neither stream method closes the caller's stream. Both handle partial
168
+ reads or writes on blocking binary streams.
169
+
170
+ ### Copies, Merging, and Statistics
171
+
172
+ ```Python
173
+ snapshot = bf.copy()
174
+ other = BloomFilter(500, 0.01, funnel=INTEGER_FUNNEL)
175
+ other.put(123)
176
+
177
+ if bf.is_compatible(other):
178
+ bf.put_all(other)
179
+
180
+ false_positive_probability = bf.expected_fpp()
181
+ estimated_distinct_elements = bf.approximate_element_count()
182
+ ```
183
+
184
+ `copy` duplicates the backing bit array and preserves the loading limit. It
185
+ shares the funnel and stateless strategy, as Guava does; custom funnels should
186
+ be immutable. `put_all` performs an in-place union without modifying the source.
187
+ Compatibility requires distinct instances with the same bit-array size, number
188
+ of hashes, strategy, and equal funnels. Custom funnels must define equality
189
+ when separate instances represent the same encoding.
190
+
191
+ Statistics use the current bit occupancy, so they also work on loaded and merged
192
+ filters. A fully saturated filter has an expected FPP of `1.0`; its element count
193
+ cannot be estimated and raises `OverflowError`, corresponding to Guava's
194
+ `ArithmeticException`. Filter equality compares configuration, funnel, and bit
195
+ contents. Mutable filters are unhashable in Python.
196
+
197
+ Unlike Guava's atomic implementation, this library does not guarantee safe
198
+ concurrent mutations. Use an external lock around mutations, queries, copies,
199
+ statistics, and serialization when sharing a filter between threads. Java's
200
+ deprecated predicate aliases and stream collectors are language-specific APIs;
201
+ use `might_contain` (or `in`) and a loop calling `put` in Python.
202
+
203
+ ## Development and Testing
204
+
205
+ From the repository root, create an isolated virtual environment and install the
206
+ project in editable mode with pytest:
207
+
208
+ ```bash
209
+ python -m venv .venv
210
+ source .venv/bin/activate
211
+ python -m pip install --upgrade pip
212
+ python -m pip install -e . pytest
213
+ ```
214
+
215
+ Run the complete test suite:
216
+
217
+ ```bash
218
+ python -m pytest -v
219
+ ```
220
+
221
+ The bidirectional Java integration test requires a JDK and a Guava JAR. Point
222
+ `GUAVA_JAR` at the JAR to compile the included Java helper and verify both
223
+ Guava-to-Python and Python-to-Guava serialization:
224
+
225
+ ```bash
226
+ GUAVA_JAR=/path/to/guava-33.7.2-jre.jar \
227
+ python -m pytest -v tests/test_guava_interop.py
228
+ ```
229
+
230
+ The test is skipped when those prerequisites are unavailable. CI downloads the
231
+ pinned Guava release, verifies its SHA-256 checksum, and runs the test as a
232
+ required separate job.
233
+
234
+ To run the same formatting, type, and security checks used during development,
235
+ install the additional tools and execute:
236
+
237
+ ```bash
238
+ python -m pip install black mypy bandit
239
+ python -m black --check bloomfilter tests
240
+ python -m mypy bloomfilter --strict
241
+ python -m bandit -r bloomfilter
242
+ ```
243
+
244
+ On Windows PowerShell, activate the virtual environment with
245
+ `.venv\Scripts\Activate.ps1` instead of `source .venv/bin/activate`.
@@ -0,0 +1,21 @@
1
+ from bloomfilter.bloomfilter import BloomFilter
2
+ from bloomfilter.funnel import (
3
+ BYTE_ARRAY_FUNNEL,
4
+ INTEGER_FUNNEL,
5
+ LEGACY_FUNNEL,
6
+ LONG_FUNNEL,
7
+ UTF8_STRING_FUNNEL,
8
+ Funnel,
9
+ StringFunnel,
10
+ )
11
+
12
+ __all__ = [
13
+ "BloomFilter",
14
+ "Funnel",
15
+ "StringFunnel",
16
+ "INTEGER_FUNNEL",
17
+ "LONG_FUNNEL",
18
+ "BYTE_ARRAY_FUNNEL",
19
+ "UTF8_STRING_FUNNEL",
20
+ "LEGACY_FUNNEL",
21
+ ]