BloomFilter-py 1.3.0__tar.gz → 2.0.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.
Files changed (30) hide show
  1. bloomfilter_py-2.0.0/CHANGELOG.md +22 -0
  2. bloomfilter_py-2.0.0/MANIFEST.in +4 -0
  3. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/PKG-INFO +116 -6
  4. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/README.md +112 -1
  5. bloomfilter_py-2.0.0/benchmarks/benchmark_backends.py +134 -0
  6. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/bloomfilter/__init__.py +2 -0
  7. bloomfilter_py-2.0.0/bloomfilter/_bitarray.py +300 -0
  8. bloomfilter_py-2.0.0/bloomfilter/_murmur3.py +63 -0
  9. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/bloomfilter/bloomfilter.py +151 -31
  10. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/bloomfilter/bloomfilter_strategy.py +59 -19
  11. bloomfilter_py-2.0.0/bloomfilter/py.typed +0 -0
  12. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/bloomfilter_py.egg-info/PKG-INFO +116 -6
  13. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/bloomfilter_py.egg-info/SOURCES.txt +13 -1
  14. bloomfilter_py-2.0.0/bloomfilter_py.egg-info/requires.txt +4 -0
  15. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/pyproject.toml +19 -8
  16. bloomfilter_py-2.0.0/scripts/check_api_compatibility.py +259 -0
  17. bloomfilter_py-2.0.0/scripts/verify_distribution.py +93 -0
  18. bloomfilter_py-2.0.0/tests/__init__.py +16 -0
  19. bloomfilter_py-2.0.0/tests/data/murmur3-vectors.json +3642 -0
  20. bloomfilter_py-2.0.0/tests/java/GuavaBloomFilterInterop.java +301 -0
  21. bloomfilter_py-2.0.0/tests/test_backends.py +454 -0
  22. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/tests/test_bloomfilter.py +119 -1
  23. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/tests/test_guava_interop.py +21 -9
  24. bloomfilter_py-1.3.0/bloomfilter_py.egg-info/requires.txt +0 -10
  25. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/LICENSE +0 -0
  26. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/bloomfilter/funnel.py +0 -0
  27. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/bloomfilter_py.egg-info/dependency_links.txt +0 -0
  28. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/bloomfilter_py.egg-info/top_level.txt +0 -0
  29. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/setup.cfg +0 -0
  30. {bloomfilter_py-1.3.0 → bloomfilter_py-2.0.0}/tests/test_filter_operations.py +0 -0
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## 2.0.0
4
+
5
+ - Retain master security checks that bound hexadecimal and Base64 decoding
6
+ before allocating the complete serialized payload.
7
+ - Remove all mandatory runtime dependencies; ship packed bit storage and a
8
+ seed-zero MurmurHash3 x64_128 implementation supporting Python 3.8+.
9
+ - Add explicit `backend="python"` (default) and `backend="native"` modes.
10
+ Native acceleration is available through the `native` installation extra.
11
+ - Preserve both hashing strategies, existing funnels and the Guava compact
12
+ serialization format. Support cross-backend comparison and merging.
13
+ - Replace per-word serialization objects with bounded chunk transformations;
14
+ chunk large counts and merges as well.
15
+ - Export a stable `BitArray` facade. `setup` copies external third-party
16
+ bitarray inputs; third-party-only data methods and aliasing need migration.
17
+ - Reject non-finite error rates, nonintegral sizing and misbehaving streams.
18
+ - Add fixed and random hash differential tests, packed-bit/backend tests,
19
+ memory checks, packaging validation, benchmarks and a dual-backend CI matrix.
20
+
21
+ Public bit storage and external-array aliasing changes require a major version.
22
+ The version is prepared locally; publishing requires a separate release action.
@@ -0,0 +1,4 @@
1
+ include LICENSE README.md CHANGELOG.md
2
+ recursive-include tests *.py *.json *.java
3
+ recursive-include benchmarks *.py
4
+ recursive-include scripts *.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bloomfilter-py
3
- Version: 1.3.0
3
+ Version: 2.0.0
4
4
  Summary: Yet another bloomfilter implementation in Python
5
5
  Author-email: OldPanda <me@old-panda.com>
6
6
  License: MIT License
@@ -47,10 +47,9 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
47
47
  Requires-Python: >=3.8
48
48
  Description-Content-Type: text/markdown
49
49
  License-File: LICENSE
50
- Requires-Dist: bitarray==3.11.0
51
- Requires-Dist: mmh3==5.0.1; python_version < "3.9"
52
- Requires-Dist: mmh3==5.2.0; python_version >= "3.9" and python_version < "3.10"
53
- Requires-Dist: mmh3==5.3.1; python_version >= "3.10"
50
+ Provides-Extra: native
51
+ Requires-Dist: bitarray<4,>=3.0; extra == "native"
52
+ Requires-Dist: mmh3<6,>=5.0; extra == "native"
54
53
  Dynamic: license-file
55
54
 
56
55
  # bloomfilter-py
@@ -81,6 +80,68 @@ Here's a brief [introduction](https://en.wikipedia.org/wiki/Bloom_filter) to Blo
81
80
  pip install bloomfilter-py
82
81
  ```
83
82
 
83
+ ## Backends and migration from 1.3
84
+
85
+ Version 2.0 runs on the Python standard library alone. `bitarray` and `mmh3`
86
+ are no longer required to install or import this package. The default is always
87
+ `backend="python"`, even if those packages are already installed. For explicit
88
+ native acceleration:
89
+
90
+ ```bash
91
+ pip install 'bloomfilter-py[native]'
92
+ ```
93
+
94
+ ```python
95
+ from bloomfilter import BloomFilter
96
+
97
+ fast = BloomFilter(500, 0.01, backend="native")
98
+ fast.put("example")
99
+ loaded = BloomFilter.loads(fast.dumps(), backend="native")
100
+ assert "example" in loaded
101
+ assert loaded.backend == "native"
102
+ ```
103
+
104
+ The constructor and all loading methods accept the keyword-only `backend`
105
+ argument (`"python"` or `"native"`). An invalid name raises `ValueError`;
106
+ requesting native mode without **both** optional packages raises `ImportError`
107
+ with installation instructions. There is no automatic fallback. `copy()`
108
+ preserves the backend. Filters using different backends can be compared and
109
+ merged when their strategies, funnels, sizes, and hash counts are compatible.
110
+ Backend choice is not serialized: all old dumps remain readable, and new dumps
111
+ use exactly the same Guava compact format.
112
+
113
+ Pure Python hashing is slower than the C extension, especially on long keys.
114
+ Native mode is available for throughput-sensitive workloads. Serialization,
115
+ counting, and merging process at most 64 KiB of payload per chunk; copies and
116
+ returned dumps necessarily allocate their complete result. `dumps()` and
117
+ `loads()` also retain full input/output bytes, so use stream methods for large
118
+ filters. Backing storage remains packed (roughly one byte per eight bits).
119
+
120
+ ### Bit storage and custom strategies
121
+
122
+ `filter.data` is now a `bloomfilter.BitArray` facade with consistent semantics
123
+ on either backend. It supports `len`, iteration, negative/integer indexing,
124
+ slice copies, scalar or equal-length iterable slice assignment, `setall`,
125
+ `count(0/1)`, `copy`, equality, `|=`, `+=`, `frombytes`, and `tobytes`.
126
+ Slices preserve the backend; slice assignment cannot resize. Appending via
127
+ `frombytes` or `+=` works even at non-byte-aligned lengths. `tobytes()` uses
128
+ most-significant-bit-first bytes with zero padding at the end. The buffer
129
+ protocol and the complete third-party `bitarray` API are **not** provided.
130
+
131
+ Existing `setup(hash_count, data, strategy, funnel)` calls accept this facade.
132
+ For migration, a third-party `bitarray.bitarray` is also accepted when installed;
133
+ it is copied into Python-backed storage and normalized to logical bit order,
134
+ including little-endian inputs. It is no longer aliased to `filter.data`.
135
+ Callers relying on that alias or on third-party-only methods must update.
136
+ Passing a `BitArray` to `setup` retains it, as before, so external mutations
137
+ remain visible. `backend` reflects the current data object.
138
+
139
+ Custom strategies still receive the same positional `put` / `might_contain`
140
+ arguments, now with the facade as their data argument. Overrides of built-in
141
+ strategies' `hash_key` are honored on both backends. Strategies must not depend
142
+ on private storage fields or a specific third-party data type. Direct calls to built-in strategies with legacy third-party bit arrays remain
143
+ supported. Direct calls to `Strategy.hash_key` always use the standard-library MurmurHash3 implementation.
144
+
84
145
  ## Usage Examples
85
146
 
86
147
  ### Basic Usage
@@ -291,10 +352,59 @@ install the additional tools and execute:
291
352
 
292
353
  ```bash
293
354
  python -m pip install black mypy bandit
294
- python -m black --check bloomfilter tests
355
+ python -m black --check bloomfilter tests benchmarks scripts
295
356
  python -m mypy bloomfilter --strict
296
357
  python -m bandit -r bloomfilter
297
358
  ```
298
359
 
299
360
  On Windows PowerShell, activate the virtual environment with
300
361
  `.venv\Scripts\Activate.ps1` instead of `source .venv/bin/activate`.
362
+
363
+ ### Reproducible backend benchmarks
364
+
365
+ ```bash
366
+ python benchmarks/benchmark_backends.py --backend python --output python.json
367
+ python benchmarks/benchmark_backends.py --backend native --output native.json
368
+ # Against a separate checkout of 1.3.0, with its dependencies installed:
369
+ python benchmarks/benchmark_backends.py --backend baseline \
370
+ --package-path /path/to/1.3.0 --output baseline.json
371
+ ```
372
+
373
+ The script reports median timings, raw repeats, workload sizes, and interpreter
374
+ metadata. It covers fresh and repeated insertions, hits and misses, short and
375
+ long byte keys, serialization, statistics, and merging. Results describe the
376
+ specified workload rather than a universal throughput guarantee.
377
+
378
+ For the zero-dependency source suite run `python -S -m unittest discover`.
379
+ Install `.[native]` to enable optional differential tests against the C packages.
380
+ The checked-in hash vectors were generated with mmh3 5.3.1 and cover every
381
+ length from 0 through 512 plus larger block boundaries.
382
+ Run Java integration tests for both backends with `BLOOMFILTER_TEST_BACKEND`
383
+ set to `python` and `native` respectively. CI covers Python 3.8 through 3.14.
384
+
385
+ Build and verify release artifacts with `python -m build` followed by
386
+ `python scripts/verify_distribution.py dist`. The latter installs the wheel
387
+ offline into a clean temporary environment and verifies it outside the source
388
+ path, including the absence of mandatory runtime dependencies.
389
+
390
+ ### Auditing compatibility against an earlier checkout
391
+
392
+ Use an environment containing the baseline's dependencies to compare actual
393
+ behavior in separate processes:
394
+
395
+ ```bash
396
+ python scripts/check_api_compatibility.py --baseline /path/to/baseline \
397
+ --candidate . --output compatibility.json
398
+ ```
399
+
400
+ The audit checks twelve strategy/funnel combinations, serialized bytes, query
401
+ results, copies/merges/statistics, streams, custom hashes and existing call
402
+ signatures. It separately reports changes to bit-storage type, third-party
403
+ methods, buffer support and `setup` input aliasing. A successful audit command
404
+ means the comparison completed; inspect `fully_backward_compatible` for the
405
+ compatibility verdict. Version 2.0 preserves the tested core operations but
406
+ changes these public storage behaviors, so it is a major release.
407
+
408
+ Hex and Base64 loaders include the master allocation-bound security update:
409
+ encoded sizes and headers are checked before full Base64 decoding, and hex
410
+ inputs are decoded in bounded chunks even with substantial whitespace.
@@ -26,6 +26,68 @@ Here's a brief [introduction](https://en.wikipedia.org/wiki/Bloom_filter) to Blo
26
26
  pip install bloomfilter-py
27
27
  ```
28
28
 
29
+ ## Backends and migration from 1.3
30
+
31
+ Version 2.0 runs on the Python standard library alone. `bitarray` and `mmh3`
32
+ are no longer required to install or import this package. The default is always
33
+ `backend="python"`, even if those packages are already installed. For explicit
34
+ native acceleration:
35
+
36
+ ```bash
37
+ pip install 'bloomfilter-py[native]'
38
+ ```
39
+
40
+ ```python
41
+ from bloomfilter import BloomFilter
42
+
43
+ fast = BloomFilter(500, 0.01, backend="native")
44
+ fast.put("example")
45
+ loaded = BloomFilter.loads(fast.dumps(), backend="native")
46
+ assert "example" in loaded
47
+ assert loaded.backend == "native"
48
+ ```
49
+
50
+ The constructor and all loading methods accept the keyword-only `backend`
51
+ argument (`"python"` or `"native"`). An invalid name raises `ValueError`;
52
+ requesting native mode without **both** optional packages raises `ImportError`
53
+ with installation instructions. There is no automatic fallback. `copy()`
54
+ preserves the backend. Filters using different backends can be compared and
55
+ merged when their strategies, funnels, sizes, and hash counts are compatible.
56
+ Backend choice is not serialized: all old dumps remain readable, and new dumps
57
+ use exactly the same Guava compact format.
58
+
59
+ Pure Python hashing is slower than the C extension, especially on long keys.
60
+ Native mode is available for throughput-sensitive workloads. Serialization,
61
+ counting, and merging process at most 64 KiB of payload per chunk; copies and
62
+ returned dumps necessarily allocate their complete result. `dumps()` and
63
+ `loads()` also retain full input/output bytes, so use stream methods for large
64
+ filters. Backing storage remains packed (roughly one byte per eight bits).
65
+
66
+ ### Bit storage and custom strategies
67
+
68
+ `filter.data` is now a `bloomfilter.BitArray` facade with consistent semantics
69
+ on either backend. It supports `len`, iteration, negative/integer indexing,
70
+ slice copies, scalar or equal-length iterable slice assignment, `setall`,
71
+ `count(0/1)`, `copy`, equality, `|=`, `+=`, `frombytes`, and `tobytes`.
72
+ Slices preserve the backend; slice assignment cannot resize. Appending via
73
+ `frombytes` or `+=` works even at non-byte-aligned lengths. `tobytes()` uses
74
+ most-significant-bit-first bytes with zero padding at the end. The buffer
75
+ protocol and the complete third-party `bitarray` API are **not** provided.
76
+
77
+ Existing `setup(hash_count, data, strategy, funnel)` calls accept this facade.
78
+ For migration, a third-party `bitarray.bitarray` is also accepted when installed;
79
+ it is copied into Python-backed storage and normalized to logical bit order,
80
+ including little-endian inputs. It is no longer aliased to `filter.data`.
81
+ Callers relying on that alias or on third-party-only methods must update.
82
+ Passing a `BitArray` to `setup` retains it, as before, so external mutations
83
+ remain visible. `backend` reflects the current data object.
84
+
85
+ Custom strategies still receive the same positional `put` / `might_contain`
86
+ arguments, now with the facade as their data argument. Overrides of built-in
87
+ strategies' `hash_key` are honored on both backends. Strategies must not depend
88
+ on private storage fields or a specific third-party data type. Direct calls to built-in strategies with legacy third-party bit arrays remain
89
+ supported. Direct calls to `Strategy.hash_key` always use the standard-library MurmurHash3 implementation.
90
+
29
91
  ## Usage Examples
30
92
 
31
93
  ### Basic Usage
@@ -236,10 +298,59 @@ install the additional tools and execute:
236
298
 
237
299
  ```bash
238
300
  python -m pip install black mypy bandit
239
- python -m black --check bloomfilter tests
301
+ python -m black --check bloomfilter tests benchmarks scripts
240
302
  python -m mypy bloomfilter --strict
241
303
  python -m bandit -r bloomfilter
242
304
  ```
243
305
 
244
306
  On Windows PowerShell, activate the virtual environment with
245
307
  `.venv\Scripts\Activate.ps1` instead of `source .venv/bin/activate`.
308
+
309
+ ### Reproducible backend benchmarks
310
+
311
+ ```bash
312
+ python benchmarks/benchmark_backends.py --backend python --output python.json
313
+ python benchmarks/benchmark_backends.py --backend native --output native.json
314
+ # Against a separate checkout of 1.3.0, with its dependencies installed:
315
+ python benchmarks/benchmark_backends.py --backend baseline \
316
+ --package-path /path/to/1.3.0 --output baseline.json
317
+ ```
318
+
319
+ The script reports median timings, raw repeats, workload sizes, and interpreter
320
+ metadata. It covers fresh and repeated insertions, hits and misses, short and
321
+ long byte keys, serialization, statistics, and merging. Results describe the
322
+ specified workload rather than a universal throughput guarantee.
323
+
324
+ For the zero-dependency source suite run `python -S -m unittest discover`.
325
+ Install `.[native]` to enable optional differential tests against the C packages.
326
+ The checked-in hash vectors were generated with mmh3 5.3.1 and cover every
327
+ length from 0 through 512 plus larger block boundaries.
328
+ Run Java integration tests for both backends with `BLOOMFILTER_TEST_BACKEND`
329
+ set to `python` and `native` respectively. CI covers Python 3.8 through 3.14.
330
+
331
+ Build and verify release artifacts with `python -m build` followed by
332
+ `python scripts/verify_distribution.py dist`. The latter installs the wheel
333
+ offline into a clean temporary environment and verifies it outside the source
334
+ path, including the absence of mandatory runtime dependencies.
335
+
336
+ ### Auditing compatibility against an earlier checkout
337
+
338
+ Use an environment containing the baseline's dependencies to compare actual
339
+ behavior in separate processes:
340
+
341
+ ```bash
342
+ python scripts/check_api_compatibility.py --baseline /path/to/baseline \
343
+ --candidate . --output compatibility.json
344
+ ```
345
+
346
+ The audit checks twelve strategy/funnel combinations, serialized bytes, query
347
+ results, copies/merges/statistics, streams, custom hashes and existing call
348
+ signatures. It separately reports changes to bit-storage type, third-party
349
+ methods, buffer support and `setup` input aliasing. A successful audit command
350
+ means the comparison completed; inspect `fully_backward_compatible` for the
351
+ compatibility verdict. Version 2.0 preserves the tested core operations but
352
+ changes these public storage behaviors, so it is a major release.
353
+
354
+ Hex and Base64 loaders include the master allocation-bound security update:
355
+ encoded sizes and headers are checked before full Base64 decoding, and hex
356
+ inputs are decoded in bounded chunks even with substantial whitespace.
@@ -0,0 +1,134 @@
1
+ """Reproducible stdlib-only driver; baseline/native modes need their packages."""
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ from pathlib import Path
7
+ import platform
8
+ import statistics
9
+ import sys
10
+ import timeit
11
+
12
+
13
+ def main():
14
+ parser = argparse.ArgumentParser(description=__doc__)
15
+ parser.add_argument(
16
+ "--backend", choices=("python", "native", "baseline"), default="python"
17
+ )
18
+ parser.add_argument(
19
+ "--package-path", type=Path, default=Path(__file__).resolve().parents[1]
20
+ )
21
+ parser.add_argument("--output", type=Path, required=True)
22
+ parser.add_argument("--repeat", type=int, default=5)
23
+ parser.add_argument("--number", type=int, default=3)
24
+ args = parser.parse_args()
25
+ if args.repeat < 1 or args.number < 1:
26
+ parser.error("repeat and number must be positive")
27
+ sys.path.insert(0, str(args.package_path.resolve()))
28
+ from bloomfilter import BloomFilter, BYTE_ARRAY_FUNNEL
29
+
30
+ def make(expected=5000):
31
+ if args.backend == "baseline":
32
+ return BloomFilter(expected, 0.01, funnel=BYTE_ARRAY_FUNNEL)
33
+ return BloomFilter(
34
+ expected, 0.01, funnel=BYTE_ARRAY_FUNNEL, backend=args.backend
35
+ )
36
+
37
+ measurements = {}
38
+ fingerprints = {}
39
+ for key_size in (16, 1024):
40
+ keys = [
41
+ i.to_bytes(4, "little") + bytes([i % 256]) * (key_size - 4)
42
+ for i in range(500)
43
+ ]
44
+ misses = [
45
+ (i + 500).to_bytes(4, "little") + bytes([(i + 500) % 256]) * (key_size - 4)
46
+ for i in range(500)
47
+ ]
48
+ bf = make()
49
+ for key in keys:
50
+ bf.put(key)
51
+ fingerprints[str(key_size)] = hashlib.sha256(bf.dumps()).hexdigest()
52
+
53
+ def insert_new():
54
+ fresh = make()
55
+ for key in keys:
56
+ fresh.put(key)
57
+
58
+ tasks = {
59
+ "insert_new": insert_new,
60
+ "insert_repeat": lambda: [bf.put(key) for key in keys],
61
+ "query_hit": lambda: [bf.might_contain(key) for key in keys],
62
+ "query_miss": lambda: [bf.might_contain(key) for key in misses],
63
+ }
64
+ for name, task in tasks.items():
65
+ raw = [
66
+ value / args.number
67
+ for value in timeit.repeat(task, number=args.number, repeat=args.repeat)
68
+ ]
69
+ measurements[f"{name}_{key_size}B_500keys"] = {
70
+ "median_seconds": statistics.median(raw),
71
+ "raw_seconds": raw,
72
+ }
73
+
74
+ # Approximately 1 MiB, enough to exercise bounded chunks repeatedly.
75
+ large = make(875000)
76
+ for i in range(500):
77
+ large.put(i.to_bytes(4, "little"))
78
+ serialized = large.dumps()
79
+ second = large.copy()
80
+ second.put(b"second-filter")
81
+ if args.backend == "baseline":
82
+ load = lambda: BloomFilter.loads(serialized, BYTE_ARRAY_FUNNEL)
83
+ else:
84
+ load = lambda: BloomFilter.loads(
85
+ serialized, BYTE_ARRAY_FUNNEL, backend=args.backend
86
+ )
87
+ for name, task in {
88
+ "dump_1MiB": large.dumps,
89
+ "load_1MiB": load,
90
+ "statistics_1MiB": large.expected_fpp,
91
+ "merge_repeat_1MiB": lambda: large.put_all(second),
92
+ }.items():
93
+ raw = [
94
+ value / args.number
95
+ for value in timeit.repeat(task, number=args.number, repeat=args.repeat)
96
+ ]
97
+ measurements[name] = {
98
+ "median_seconds": statistics.median(raw),
99
+ "raw_seconds": raw,
100
+ }
101
+ report = {
102
+ "backend": args.backend,
103
+ "python": sys.version,
104
+ "platform": platform.platform(),
105
+ "package_path": str(args.package_path.resolve()),
106
+ "repeat": args.repeat,
107
+ "number": args.number,
108
+ "short_filter_capacity": 5000,
109
+ "fpp": 0.01,
110
+ "keys_per_batch": 500,
111
+ "large_filter_bytes": len(serialized),
112
+ "fingerprints": fingerprints,
113
+ "notes": [
114
+ "Fresh insertion includes constructing an empty filter.",
115
+ "Repeated insertion and merge operate on already populated filters.",
116
+ "Query misses are absent keys and may include false positives.",
117
+ "Operations return full serialized bytes for dump/load.",
118
+ ],
119
+ "measurements": measurements,
120
+ }
121
+ args.output.write_text(json.dumps(report, indent=2) + "\n")
122
+ print(
123
+ json.dumps(
124
+ {
125
+ name: round(value["median_seconds"] * 1000, 3)
126
+ for name, value in measurements.items()
127
+ },
128
+ indent=2,
129
+ )
130
+ )
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()
@@ -1,3 +1,4 @@
1
+ from bloomfilter._bitarray import BitArray
1
2
  from bloomfilter.bloomfilter import BloomFilter
2
3
  from bloomfilter.funnel import (
3
4
  BYTE_ARRAY_FUNNEL,
@@ -11,6 +12,7 @@ from bloomfilter.funnel import (
11
12
 
12
13
  __all__ = [
13
14
  "BloomFilter",
15
+ "BitArray",
14
16
  "Funnel",
15
17
  "StringFunnel",
16
18
  "INTEGER_FUNNEL",