pyhyperminhash 0.2.0__cp39-abi3-win_arm64.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.
- pyhyperminhash/__init__.py +5 -0
- pyhyperminhash/__init__.pyi +59 -0
- pyhyperminhash/py.typed +0 -0
- pyhyperminhash/pyhyperminhash.pyd +0 -0
- pyhyperminhash-0.2.0.dist-info/METADATA +188 -0
- pyhyperminhash-0.2.0.dist-info/RECORD +9 -0
- pyhyperminhash-0.2.0.dist-info/WHEEL +4 -0
- pyhyperminhash-0.2.0.dist-info/licenses/LICENSE +21 -0
- pyhyperminhash-0.2.0.dist-info/sboms/pyhyperminhash.cyclonedx.json +2287 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from typing_extensions import final
|
|
3
|
+
|
|
4
|
+
class _SupportsRead(typing.Protocol):
|
|
5
|
+
def read(self, n: int, /) -> bytes: ...
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"__version_info__",
|
|
9
|
+
"Entry",
|
|
10
|
+
"Sketch",
|
|
11
|
+
"__version__",
|
|
12
|
+
"__profile__",
|
|
13
|
+
"__hyperminhash_version__",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
@final
|
|
17
|
+
class Entry:
|
|
18
|
+
def add(self, obj: typing.Hashable, /) -> None: ...
|
|
19
|
+
def add_bytes(self, obj: bytes, /) -> None: ...
|
|
20
|
+
def __iadd__(self, value: typing.Hashable, /) -> "Entry": ...
|
|
21
|
+
def add_reader(self, src: _SupportsRead, /) -> int: ...
|
|
22
|
+
def fork(self) -> "Entry": ...
|
|
23
|
+
def _digest(self) -> int: ...
|
|
24
|
+
|
|
25
|
+
@final
|
|
26
|
+
class Sketch:
|
|
27
|
+
@staticmethod
|
|
28
|
+
def load(buf: bytes, /) -> "Sketch": ...
|
|
29
|
+
@staticmethod
|
|
30
|
+
def from_iter(src: typing.Iterator[typing.Hashable], /) -> "Sketch": ...
|
|
31
|
+
def save(self) -> bytes: ...
|
|
32
|
+
def add(self, obj: typing.Hashable, /) -> None: ...
|
|
33
|
+
def add_reader(self, src: _SupportsRead, /) -> int: ...
|
|
34
|
+
def add_entry(self, entry: Entry, /) -> None: ...
|
|
35
|
+
def cardinality(self) -> float: ...
|
|
36
|
+
def union(self, other: "Sketch", /) -> None: ...
|
|
37
|
+
def intersection(self, other: "Sketch", /, *, fast: bool = False) -> float: ...
|
|
38
|
+
def similarity(self, other: "Sketch", /, *, fast: bool = False) -> float: ...
|
|
39
|
+
def intersection_many(
|
|
40
|
+
self, others: typing.Iterable["Sketch"], /
|
|
41
|
+
) -> list[float]: ...
|
|
42
|
+
def similarity_many(self, others: typing.Iterable["Sketch"], /) -> list[float]: ...
|
|
43
|
+
def __bool__(self) -> bool: ...
|
|
44
|
+
def __int__(self) -> int: ...
|
|
45
|
+
def __len__(self) -> int: ...
|
|
46
|
+
def __float__(self) -> float: ...
|
|
47
|
+
def __iadd__(self, value: typing.Hashable, /) -> "Sketch": ...
|
|
48
|
+
def __ior__(self, value: "Sketch", /) -> "Sketch": ...
|
|
49
|
+
def __or__(self, value: "Sketch", /) -> "Sketch": ...
|
|
50
|
+
def __lt__(self, value: "Sketch", /) -> bool: ...
|
|
51
|
+
def __le__(self, value: "Sketch", /) -> bool: ...
|
|
52
|
+
def __gt__(self, value: "Sketch", /) -> bool: ...
|
|
53
|
+
def __ge__(self, value: "Sketch", /) -> bool: ...
|
|
54
|
+
|
|
55
|
+
def __version_info__() -> str: ...
|
|
56
|
+
|
|
57
|
+
__version__: str
|
|
58
|
+
__hyperminhash_version__: str
|
|
59
|
+
__profile__: str
|
pyhyperminhash/py.typed
ADDED
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyhyperminhash
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Classifier: Programming Language :: Rust
|
|
5
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Summary: Very fast, constant memory-footprint cardinality approximation, including intersection
|
|
10
|
+
Author-email: Lukas Lueg <lukas.lueg@gmail.com>
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
13
|
+
Project-URL: Homepage, https://github.com/lukaslueg/pyhyperminhash
|
|
14
|
+
|
|
15
|
+
# Hyperminhash for Python
|
|
16
|
+
|
|
17
|
+
[](https://docs.rs/hyperminhash)
|
|
18
|
+
[](https://pypi.org/project/pyhyperminhash/)
|
|
19
|
+
|
|
20
|
+
Very fast, constant memory-footprint cardinality approximation, including intersection
|
|
21
|
+
and union operation.
|
|
22
|
+
|
|
23
|
+
The class `Sketch` can be used to count unique elements that were encountered during
|
|
24
|
+
the instance's lifetime. Unlike e.g. when using a `set`, the memory consumed by the
|
|
25
|
+
`Sketch`-instance does _not_ grow as elements are added; each `Sketch`-instance
|
|
26
|
+
consumes approximately 32kb of memory, independent of the number of elements.
|
|
27
|
+
|
|
28
|
+
### Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install pyhyperminhash
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Basic usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# Construct an empty Sketch
|
|
38
|
+
sk = pyhyperminhash.Sketch()
|
|
39
|
+
assert not sk # The Sketch is empty
|
|
40
|
+
sk.add('Foo') # Add elements
|
|
41
|
+
sk.add('Bar')
|
|
42
|
+
sk.add(42)
|
|
43
|
+
assert sk # The Sketch is not empty
|
|
44
|
+
assert float(sk) == sk.cardinality() # Approximately 3.0
|
|
45
|
+
assert int(sk) == len(sk) # Approximately 3
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# Sketches can be combined...
|
|
50
|
+
|
|
51
|
+
sk2 = pyhyperminhash.Sketch()
|
|
52
|
+
sk2.add('Foobar')
|
|
53
|
+
sk2 |= sk # sk2 now holds all elements that were in `sk` or `sk2`
|
|
54
|
+
|
|
55
|
+
sk3 = pyhyperminhash.Sketch()
|
|
56
|
+
sk3.add(42)
|
|
57
|
+
sk3.add('Foo')
|
|
58
|
+
assert sk3.intersection(sk2) > 0.0 # Approximately 1.0, due to `Foo`
|
|
59
|
+
|
|
60
|
+
# `fast=True` uses the faster upstream estimate with a slightly looser
|
|
61
|
+
# correction model; for most inputs the difference is tiny.
|
|
62
|
+
assert sk3.intersection(sk2, fast=True) > 0.0
|
|
63
|
+
assert sk3.similarity(sk2, fast=True) > 0.0
|
|
64
|
+
|
|
65
|
+
# Compare one Sketch against many others
|
|
66
|
+
others = [sk, sk2, sk3]
|
|
67
|
+
assert sk3.intersection_many(others) == [sk3.intersection(other) for other in others]
|
|
68
|
+
assert sk3.similarity_many(others) == [sk3.similarity(other) for other in others]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# Sketches can be stored for later use
|
|
73
|
+
|
|
74
|
+
buf = sk.save() # Serialize the Sketch into a bytes-buffer
|
|
75
|
+
new_sk = pyhyperminhash.Sketch.load(buf) # Deserialize a Sketch
|
|
76
|
+
assert len(new_sk) == len(sk)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# Sketches can be constructed from iterators directly, which avoids
|
|
81
|
+
# materializing all objects simultaneously.
|
|
82
|
+
|
|
83
|
+
# This is also faster than adding elements one by one.
|
|
84
|
+
sk = pyhyperminhash.Sketch.from_iter(iter(range(100)))
|
|
85
|
+
|
|
86
|
+
# Read all lines; uses binary mode to avoid Unicode encoding+decoding
|
|
87
|
+
with open('complaints.txt', 'rb') as f:
|
|
88
|
+
sk = pyhyperminhash.Sketch.from_iter(f)
|
|
89
|
+
|
|
90
|
+
# Add an entire file-like object
|
|
91
|
+
with open('complaints.txt', 'rb') as f:
|
|
92
|
+
sk.add_reader(f)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# Entry-objects can be used to construct what represents a single
|
|
97
|
+
# object from multiple parts. They consume very little memory.
|
|
98
|
+
|
|
99
|
+
# Add sub-objects that don't fit into memory
|
|
100
|
+
e = pyhyperminhash.Entry()
|
|
101
|
+
with open('complaints.txt', 'rb') as f:
|
|
102
|
+
e.add(f.read(4096)) # Add two parts to this Entry
|
|
103
|
+
e.add(f.read(4096))
|
|
104
|
+
sk.add_entry(e) # This counts as one object
|
|
105
|
+
|
|
106
|
+
sk2 = pyhyperminhash.Sketch()
|
|
107
|
+
sk2.add_entry(e) # Entry-objects can be added to multiple Sketches
|
|
108
|
+
|
|
109
|
+
# Entry-objects can be "forked" at the state they are currently at
|
|
110
|
+
e2 = e.fork()
|
|
111
|
+
e2.add("Foo")
|
|
112
|
+
sk.add_entry(e2) # This is a new object in `Sketch`...
|
|
113
|
+
|
|
114
|
+
e3 = e.fork()
|
|
115
|
+
e3.add("Bar")
|
|
116
|
+
sk.add_entry(e3) # So is this, without re-computing `e`
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
It is very much preferred to provide `bytes` to `Sketch.add()` and `Entry.add()`,
|
|
120
|
+
in order to avoid double-hashing the content. Also, notice that the Python interpreter
|
|
121
|
+
probably randomizes hash-values at interpreter-startup (in order to prevent certain kinds
|
|
122
|
+
of DOS-attacks). The randomness introduced here means that the count-approximation provided
|
|
123
|
+
by this package may not be stable from run to run.
|
|
124
|
+
|
|
125
|
+
See the documentation for the underlying [implementation](https://docs.rs/hyperminhash) for additional information.
|
|
126
|
+
|
|
127
|
+
### Usage as a sqlite3 aggregate function
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
class PyHyperminhashCounter:
|
|
131
|
+
def __init__(self):
|
|
132
|
+
self.sk = pyhyperminhash.Sketch()
|
|
133
|
+
|
|
134
|
+
def step(self, *args) -> None:
|
|
135
|
+
self.sk += args
|
|
136
|
+
|
|
137
|
+
def finalize(self) -> int:
|
|
138
|
+
return int(self.sk)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
con = sqlite3.connect(":memory:")
|
|
142
|
+
# Create the function for the current connection
|
|
143
|
+
con.create_aggregate("pyhmh", -1, PyHyperminhashCounter)
|
|
144
|
+
|
|
145
|
+
# Some dummy data
|
|
146
|
+
cur = con.execute("CREATE TABLE test(i, j)")
|
|
147
|
+
cur.executemany("INSERT INTO test(i, j) VALUES(?, ?)",
|
|
148
|
+
((f'foo{i % 4291}bar', i % 819)
|
|
149
|
+
for i in range(1000000)))
|
|
150
|
+
|
|
151
|
+
# Returns exactly 502047
|
|
152
|
+
cur.execute("SELECT COUNT(*) FROM (SELECT DISTINCT i, j FROM test)")
|
|
153
|
+
print(cur.fetchone()[0])
|
|
154
|
+
|
|
155
|
+
# Returns approximately 500000, but faster
|
|
156
|
+
cur.execute("SELECT pyhmh(i, j) FROM test")
|
|
157
|
+
print(cur.fetchone()[0])
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Performance examples
|
|
161
|
+
|
|
162
|
+
Reading a file of 50 million lines, ~1,000,000 unique elements, 20 characters per line.
|
|
163
|
+
|
|
164
|
+
Method | Wall-clock time | Memory consumed | Count |
|
|
165
|
+
---------------------------|-----------------|-----------------|------------|
|
|
166
|
+
Count lines | 2.3 seconds | _nil_ | 50,000,000 |
|
|
167
|
+
`set()` | 14.99 seconds | ~144 megabytes | 1,048,576 |
|
|
168
|
+
`Sketch.from_iter()` | 1.8 seconds | ~32 kilobytes | 1,041,936 |
|
|
169
|
+
|
|
170
|
+
For repeatable local benchmarks of the Python-facing API, use the `pyperf` suite:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
python3 -m venv .venv
|
|
174
|
+
source .venv/bin/activate
|
|
175
|
+
maturin develop -r
|
|
176
|
+
pip install pyperf
|
|
177
|
+
python3 examples/bench.py
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### FAQ
|
|
181
|
+
|
|
182
|
+
* Can I extract an element that was previously added from a `Sketch`?
|
|
183
|
+
* No.
|
|
184
|
+
* Can I check if an element has previously been added to a `Sketch`?
|
|
185
|
+
* No.
|
|
186
|
+
* Wow, why use this, then?
|
|
187
|
+
* It's very fast at counting unique things. And it does not use much memory.
|
|
188
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pyhyperminhash/__init__.py,sha256=bhBaICIKP_QxoP3VoISd10yFR3QQO5oFP5ChuNs3fgg,139
|
|
2
|
+
pyhyperminhash/__init__.pyi,sha256=7sIsBMiXTgseywi-qCoydl5YwvZGX-PbK735OP9LYQQ,2129
|
|
3
|
+
pyhyperminhash/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
pyhyperminhash/pyhyperminhash.pyd,sha256=00adBqDCjxv5SdQTbM0fg2rt-oL1_5SMsusNNTFZFn4,370688
|
|
5
|
+
pyhyperminhash-0.2.0.dist-info/METADATA,sha256=keCqhejg2ciHcUi6kC9egyq8B1QCWJHmuzqMxQJGbmU,6338
|
|
6
|
+
pyhyperminhash-0.2.0.dist-info/WHEEL,sha256=2muIeEoy5DUTfuM3RFMnd0bGlZCGVPOH8LjuzkFAm8s,95
|
|
7
|
+
pyhyperminhash-0.2.0.dist-info/licenses/LICENSE,sha256=5F9XyCGLCpCvSZi0l7noUGu1ISKges2jCstiLZhstdU,1106
|
|
8
|
+
pyhyperminhash-0.2.0.dist-info/sboms/pyhyperminhash.cyclonedx.json,sha256=qLvrRuQPbdG8qM3dqTAuxZVsA_rTAYIk55F_x_aerPc,71343
|
|
9
|
+
pyhyperminhash-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Lukas Lueg (lukas.lueg@gmail.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|