sillstone 0.2.0b1__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.
sillstone/__init__.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"""Sillstone Python SDK: Zero-dependency multimedia, hashing, and array computing suite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import struct
|
|
9
|
+
|
|
10
|
+
from ._generated import (
|
|
11
|
+
LIB,
|
|
12
|
+
sillstone_version_major_raw,
|
|
13
|
+
sillstone_version_minor_raw,
|
|
14
|
+
sillstone_version_patch_raw,
|
|
15
|
+
sillstone_buffer_checksum_raw,
|
|
16
|
+
sillstone_digest_sha256_raw,
|
|
17
|
+
sillstone_digest_sha3_256_raw,
|
|
18
|
+
sillstone_digest_sha3_512_raw,
|
|
19
|
+
sillstone_digest_blake2b_512_raw,
|
|
20
|
+
sillstone_digest_blake3_raw,
|
|
21
|
+
sillstone_math_dst1_raw,
|
|
22
|
+
sillstone_math_dst2_raw,
|
|
23
|
+
sillstone_math_dst3_raw,
|
|
24
|
+
sillstone_unicode_grapheme_count_raw,
|
|
25
|
+
sillstone_unicode_char_category_raw,
|
|
26
|
+
sillstone_image_phash_raw,
|
|
27
|
+
sillstone_image_ahash_raw,
|
|
28
|
+
sillstone_image_dhash_raw,
|
|
29
|
+
sillstone_image_whash_raw,
|
|
30
|
+
sillstone_image_colorhash_raw,
|
|
31
|
+
sillstone_text_tlsh_raw,
|
|
32
|
+
sillstone_text_tlsh_distance_raw,
|
|
33
|
+
sillstone_curator_detect_raw,
|
|
34
|
+
sillstone_curator_content_hash_raw,
|
|
35
|
+
sillstone_curator_signature_raw,
|
|
36
|
+
sillstone_curator_describe_raw,
|
|
37
|
+
sillstone_curator_match_raw,
|
|
38
|
+
sillstone_curator_free_raw,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__version__ = "0.2.0b1"
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"version",
|
|
45
|
+
"buffer_checksum",
|
|
46
|
+
"sha256",
|
|
47
|
+
"sha3_256",
|
|
48
|
+
"sha3_512",
|
|
49
|
+
"blake2b_512",
|
|
50
|
+
"blake3",
|
|
51
|
+
"dst1",
|
|
52
|
+
"dst2",
|
|
53
|
+
"dst3",
|
|
54
|
+
"grapheme_count",
|
|
55
|
+
"char_category",
|
|
56
|
+
"phash",
|
|
57
|
+
"ahash",
|
|
58
|
+
"dhash",
|
|
59
|
+
"whash",
|
|
60
|
+
"colorhash",
|
|
61
|
+
"tlsh",
|
|
62
|
+
"tlsh_distance",
|
|
63
|
+
"detect",
|
|
64
|
+
"content_hash",
|
|
65
|
+
"signature",
|
|
66
|
+
"describe",
|
|
67
|
+
"match_signatures",
|
|
68
|
+
"parse_signature",
|
|
69
|
+
"parse_describe",
|
|
70
|
+
"fnv1a64",
|
|
71
|
+
"Detection",
|
|
72
|
+
"Signature",
|
|
73
|
+
"Description",
|
|
74
|
+
"MatchResult",
|
|
75
|
+
"FfiError",
|
|
76
|
+
"FORMATS",
|
|
77
|
+
"MODALITIES",
|
|
78
|
+
"MATCH_TAGS",
|
|
79
|
+
"STATUS_OK",
|
|
80
|
+
"STATUS_INVALID",
|
|
81
|
+
"STATUS_REJECTED",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
#: Status codes
|
|
85
|
+
STATUS_OK = 0
|
|
86
|
+
STATUS_INVALID = -1
|
|
87
|
+
STATUS_REJECTED = -2
|
|
88
|
+
|
|
89
|
+
#: Format names by wire code (declaration order)
|
|
90
|
+
FORMATS = ("png", "jpeg", "bmp", "zip", "mp4", "mp3", "wav", "flac", "pdf", "gif", "unknown")
|
|
91
|
+
|
|
92
|
+
#: Modality names by wire code (declaration order)
|
|
93
|
+
MODALITIES = ("image", "audio", "text", "binary", "video")
|
|
94
|
+
|
|
95
|
+
#: Match-outcome tags by wire code (declaration order)
|
|
96
|
+
MATCH_TAGS = ("image", "audio", "text", "binary", "video")
|
|
97
|
+
|
|
98
|
+
#: Signature-stream tag bytes (declaration order)
|
|
99
|
+
SIGNATURE_TAGS = ("image", "audio", "text", "binary", "video")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class FfiError(Exception):
|
|
103
|
+
"""A non-zero status code came back from the cdylib."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, op: str, status: int) -> None:
|
|
106
|
+
kind = {
|
|
107
|
+
STATUS_INVALID: "invalid argument",
|
|
108
|
+
STATUS_REJECTED: "input rejected",
|
|
109
|
+
}.get(status, "unknown failure")
|
|
110
|
+
super().__init__(f"{op} failed: {kind} (status {status})")
|
|
111
|
+
self.status = status
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class Detection:
|
|
116
|
+
"""Container sniffing result."""
|
|
117
|
+
format: str
|
|
118
|
+
modality: str
|
|
119
|
+
pending: bool
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class Signature:
|
|
124
|
+
"""A tier-2 signature, re-expressed from the signature stream."""
|
|
125
|
+
modality: str
|
|
126
|
+
phash: int | None = None
|
|
127
|
+
peak_count: int | None = None
|
|
128
|
+
frames: int | None = None
|
|
129
|
+
peaks: tuple[tuple[int, int], ...] | None = None
|
|
130
|
+
peaks_bytes: bytes | None = None
|
|
131
|
+
word_count: int | None = None
|
|
132
|
+
words: tuple[int, ...] | None = None
|
|
133
|
+
chunk_count: int | None = None
|
|
134
|
+
digest_bytes: bytes | None = None
|
|
135
|
+
frame_count: int | None = None
|
|
136
|
+
width: int | None = None
|
|
137
|
+
height: int | None = None
|
|
138
|
+
duration_bits: int | None = None
|
|
139
|
+
frame_hashes: tuple[int, ...] | None = None
|
|
140
|
+
minhash: tuple[int, ...] | None = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True)
|
|
144
|
+
class Description:
|
|
145
|
+
"""Both tiers plus facts, re-expressed from the describe stream."""
|
|
146
|
+
format: str
|
|
147
|
+
modality: str
|
|
148
|
+
tier1: str
|
|
149
|
+
signature: Signature
|
|
150
|
+
facts: dict
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class MatchResult:
|
|
155
|
+
"""A match outcome, re-expressed from the slots."""
|
|
156
|
+
tag: str
|
|
157
|
+
slots: tuple[int, ...]
|
|
158
|
+
matched: bool
|
|
159
|
+
|
|
160
|
+
def slot_float(self, index: int) -> float:
|
|
161
|
+
"""Reinterprets slot `index` as an IEEE-754 double."""
|
|
162
|
+
return struct.unpack("<d", struct.pack("<Q", self.slots[index]))[0]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def version() -> str:
|
|
166
|
+
"""Returns library version string."""
|
|
167
|
+
return f"{sillstone_version_major_raw()}.{sillstone_version_minor_raw()}.{sillstone_version_patch_raw()}"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def buffer_checksum(data: bytes) -> int:
|
|
171
|
+
"""Computes standard 64-bit FNV-1a checksum over input bytes."""
|
|
172
|
+
return int(sillstone_buffer_checksum_raw(data, len(data)))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def sha256(data: bytes) -> bytes:
|
|
176
|
+
"""Computes 32-byte SHA-256 digest."""
|
|
177
|
+
out = ctypes.create_string_buffer(32)
|
|
178
|
+
rc = sillstone_digest_sha256_raw(data, len(data), out)
|
|
179
|
+
if rc != 0:
|
|
180
|
+
raise FfiError("sha256", rc)
|
|
181
|
+
return out.raw
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def sha3_256(data: bytes) -> bytes:
|
|
185
|
+
"""Computes 32-byte SHA-3-256 digest."""
|
|
186
|
+
out = ctypes.create_string_buffer(32)
|
|
187
|
+
rc = sillstone_digest_sha3_256_raw(data, len(data), out)
|
|
188
|
+
if rc != 0:
|
|
189
|
+
raise FfiError("sha3_256", rc)
|
|
190
|
+
return out.raw
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def sha3_512(data: bytes) -> bytes:
|
|
194
|
+
"""Computes 64-byte SHA-3-512 digest."""
|
|
195
|
+
out = ctypes.create_string_buffer(64)
|
|
196
|
+
rc = sillstone_digest_sha3_512_raw(data, len(data), out)
|
|
197
|
+
if rc != 0:
|
|
198
|
+
raise FfiError("sha3_512", rc)
|
|
199
|
+
return out.raw
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def blake2b_512(data: bytes) -> bytes:
|
|
203
|
+
"""Computes 64-byte unkeyed BLAKE2b digest."""
|
|
204
|
+
out = ctypes.create_string_buffer(64)
|
|
205
|
+
rc = sillstone_digest_blake2b_512_raw(data, len(data), out)
|
|
206
|
+
if rc != 0:
|
|
207
|
+
raise FfiError("blake2b_512", rc)
|
|
208
|
+
return out.raw
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def blake3(data: bytes) -> bytes:
|
|
212
|
+
"""Computes 32-byte BLAKE3 digest."""
|
|
213
|
+
out = ctypes.create_string_buffer(32)
|
|
214
|
+
rc = sillstone_digest_blake3_raw(data, len(data), out)
|
|
215
|
+
if rc != 0:
|
|
216
|
+
raise FfiError("blake3", rc)
|
|
217
|
+
return out.raw
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def dst1(data: list[float]) -> list[float]:
|
|
221
|
+
"""Computes orthonormal DST-I over input float array."""
|
|
222
|
+
c_in = (ctypes.c_double * len(data))(*data)
|
|
223
|
+
c_out = (ctypes.c_double * len(data))()
|
|
224
|
+
rc = sillstone_math_dst1_raw(c_in, len(data), c_out)
|
|
225
|
+
if rc != 0:
|
|
226
|
+
raise FfiError("dst1", rc)
|
|
227
|
+
return list(c_out)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def dst2(data: list[float]) -> list[float]:
|
|
231
|
+
"""Computes orthonormal DST-II over input float array."""
|
|
232
|
+
c_in = (ctypes.c_double * len(data))(*data)
|
|
233
|
+
c_out = (ctypes.c_double * len(data))()
|
|
234
|
+
rc = sillstone_math_dst2_raw(c_in, len(data), c_out)
|
|
235
|
+
if rc != 0:
|
|
236
|
+
raise FfiError("dst2", rc)
|
|
237
|
+
return list(c_out)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def dst3(data: list[float]) -> list[float]:
|
|
241
|
+
"""Computes orthonormal DST-III over input float array."""
|
|
242
|
+
c_in = (ctypes.c_double * len(data))(*data)
|
|
243
|
+
c_out = (ctypes.c_double * len(data))()
|
|
244
|
+
rc = sillstone_math_dst3_raw(c_in, len(data), c_out)
|
|
245
|
+
if rc != 0:
|
|
246
|
+
raise FfiError("dst3", rc)
|
|
247
|
+
return list(c_out)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def grapheme_count(text: str) -> int:
|
|
251
|
+
"""Counts UAX #29 grapheme clusters in a string."""
|
|
252
|
+
raw = text.encode("utf-8")
|
|
253
|
+
count = ctypes.c_size_t()
|
|
254
|
+
rc = sillstone_unicode_grapheme_count_raw(raw, len(raw), ctypes.byref(count))
|
|
255
|
+
if rc != 0:
|
|
256
|
+
raise FfiError("grapheme_count", rc)
|
|
257
|
+
return count.value
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def char_category(char: str) -> int:
|
|
261
|
+
"""Returns general category tag for a single character."""
|
|
262
|
+
cp = ord(char[0])
|
|
263
|
+
cat = ctypes.c_uint8()
|
|
264
|
+
rc = sillstone_unicode_char_category_raw(cp, ctypes.byref(cat))
|
|
265
|
+
if rc != 0:
|
|
266
|
+
raise FfiError("char_category", rc)
|
|
267
|
+
return cat.value
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def phash(data: bytes, width: int, height: int, layout: int = 0) -> int:
|
|
271
|
+
"""Computes 64-bit perceptual hash (pHash) of an image."""
|
|
272
|
+
out = ctypes.c_uint64()
|
|
273
|
+
rc = sillstone_image_phash_raw(data, len(data), width, height, layout, ctypes.byref(out))
|
|
274
|
+
if rc != 0:
|
|
275
|
+
raise FfiError("phash", rc)
|
|
276
|
+
return out.value
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def ahash(data: bytes, width: int, height: int, layout: int = 0) -> int:
|
|
280
|
+
"""Computes 64-bit average hash (aHash) of an image."""
|
|
281
|
+
out = ctypes.c_uint64()
|
|
282
|
+
rc = sillstone_image_ahash_raw(data, len(data), width, height, layout, ctypes.byref(out))
|
|
283
|
+
if rc != 0:
|
|
284
|
+
raise FfiError("ahash", rc)
|
|
285
|
+
return out.value
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def dhash(data: bytes, width: int, height: int, layout: int = 0) -> int:
|
|
289
|
+
"""Computes 64-bit difference hash (dHash) of an image."""
|
|
290
|
+
out = ctypes.c_uint64()
|
|
291
|
+
rc = sillstone_image_dhash_raw(data, len(data), width, height, layout, ctypes.byref(out))
|
|
292
|
+
if rc != 0:
|
|
293
|
+
raise FfiError("dhash", rc)
|
|
294
|
+
return out.value
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def whash(data: bytes, width: int, height: int, layout: int = 0) -> int:
|
|
298
|
+
"""Computes 64-bit wavelet hash (wHash) of an image."""
|
|
299
|
+
out = ctypes.c_uint64()
|
|
300
|
+
rc = sillstone_image_whash_raw(data, len(data), width, height, layout, ctypes.byref(out))
|
|
301
|
+
if rc != 0:
|
|
302
|
+
raise FfiError("whash", rc)
|
|
303
|
+
return out.value
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def colorhash(data: bytes, width: int, height: int, layout: int = 0) -> int:
|
|
307
|
+
"""Computes 64-bit color distribution hash of an image."""
|
|
308
|
+
out = ctypes.c_uint64()
|
|
309
|
+
rc = sillstone_image_colorhash_raw(data, len(data), width, height, layout, ctypes.byref(out))
|
|
310
|
+
if rc != 0:
|
|
311
|
+
raise FfiError("colorhash", rc)
|
|
312
|
+
return out.value
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def tlsh(data: bytes) -> str:
|
|
316
|
+
"""Computes 70-hex-char TrendMicro Locality Sensitive Hash string."""
|
|
317
|
+
out = ctypes.create_string_buffer(72)
|
|
318
|
+
rc = sillstone_text_tlsh_raw(data, len(data), out)
|
|
319
|
+
if rc != 0:
|
|
320
|
+
raise FfiError("tlsh", rc)
|
|
321
|
+
return out.raw.decode("ascii", errors="replace")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def tlsh_distance(hash_a: str | bytes, hash_b: str | bytes) -> int:
|
|
325
|
+
"""Computes TLSH distance between two 70-hex-char hashes."""
|
|
326
|
+
a_bytes = hash_a.encode("ascii") if isinstance(hash_a, str) else hash_a
|
|
327
|
+
b_bytes = hash_b.encode("ascii") if isinstance(hash_b, str) else hash_b
|
|
328
|
+
out = ctypes.c_uint32()
|
|
329
|
+
rc = sillstone_text_tlsh_distance_raw(a_bytes, b_bytes, ctypes.byref(out))
|
|
330
|
+
if rc != 0:
|
|
331
|
+
raise FfiError("tlsh_distance", rc)
|
|
332
|
+
return out.value
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def fnv1a64(data: bytes) -> int:
|
|
336
|
+
"""The 64-bit FNV-1a fold the reference sub-digests use."""
|
|
337
|
+
h = 0xCBF29CE484222325
|
|
338
|
+
for b in data:
|
|
339
|
+
h ^= b
|
|
340
|
+
h = (h * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
|
|
341
|
+
return h
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def detect(data: bytes) -> Detection:
|
|
345
|
+
"""Sniffs container and modality without full decode."""
|
|
346
|
+
fmt = ctypes.c_uint32()
|
|
347
|
+
mod = ctypes.c_uint32()
|
|
348
|
+
pending = ctypes.c_uint32()
|
|
349
|
+
rc = sillstone_curator_detect_raw(
|
|
350
|
+
data, len(data), ctypes.byref(fmt), ctypes.byref(mod), ctypes.byref(pending)
|
|
351
|
+
)
|
|
352
|
+
if rc != 0:
|
|
353
|
+
raise FfiError("curator_detect", rc)
|
|
354
|
+
return Detection(
|
|
355
|
+
format=FORMATS[fmt.value],
|
|
356
|
+
modality=MODALITIES[mod.value],
|
|
357
|
+
pending=pending.value == 1,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def content_hash(data: bytes) -> bytes:
|
|
362
|
+
"""Computes 32-byte tier-1 content digest."""
|
|
363
|
+
buf = (ctypes.c_ubyte * 32)()
|
|
364
|
+
out_len = ctypes.c_size_t()
|
|
365
|
+
rc = sillstone_curator_content_hash_raw(data, len(data), buf, len(buf), ctypes.byref(out_len))
|
|
366
|
+
if rc != 0:
|
|
367
|
+
raise FfiError("curator_content_hash", rc)
|
|
368
|
+
return bytes(buf)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _stream_op(raw_fn, data: bytes) -> bytes:
|
|
372
|
+
out = ctypes.c_void_p()
|
|
373
|
+
out_len = ctypes.c_size_t()
|
|
374
|
+
rc = raw_fn(data, len(data), ctypes.byref(out), ctypes.byref(out_len))
|
|
375
|
+
if rc != 0:
|
|
376
|
+
raise FfiError(raw_fn.__name__, rc)
|
|
377
|
+
try:
|
|
378
|
+
return ctypes.string_at(out, out_len.value)
|
|
379
|
+
finally:
|
|
380
|
+
sillstone_curator_free_raw(out, out_len.value)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def signature(data: bytes) -> Signature:
|
|
384
|
+
"""Computes tier-2 signature stream and parses it into a Signature object."""
|
|
385
|
+
return parse_signature(_stream_op(sillstone_curator_signature_raw, data))
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def describe(data: bytes) -> Description:
|
|
389
|
+
"""Computes both tiers and parses the describe stream."""
|
|
390
|
+
return parse_describe(_stream_op(sillstone_curator_describe_raw, data))
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def match_signatures(a: bytes, b: bytes) -> MatchResult:
|
|
394
|
+
"""Scores two raw inputs across modalities."""
|
|
395
|
+
tag = ctypes.c_uint32()
|
|
396
|
+
slots = (ctypes.c_uint64 * 4)()
|
|
397
|
+
matched = ctypes.c_uint32()
|
|
398
|
+
rc = sillstone_curator_match_raw(
|
|
399
|
+
a, len(a), b, len(b), ctypes.byref(tag), slots, len(slots), ctypes.byref(matched)
|
|
400
|
+
)
|
|
401
|
+
if rc != 0:
|
|
402
|
+
raise FfiError("curator_match", rc)
|
|
403
|
+
return MatchResult(
|
|
404
|
+
tag=MATCH_TAGS[tag.value],
|
|
405
|
+
slots=tuple(int(s) for s in slots),
|
|
406
|
+
matched=matched.value == 1,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def parse_signature(raw: bytes) -> Signature:
|
|
410
|
+
"""Re-expresses the canonical signature stream as a Signature."""
|
|
411
|
+
if len(raw) < 1:
|
|
412
|
+
raise ValueError("signature stream is empty")
|
|
413
|
+
tag = raw[0]
|
|
414
|
+
if tag >= len(SIGNATURE_TAGS):
|
|
415
|
+
raise ValueError(f"unknown signature tag {tag}")
|
|
416
|
+
modality = SIGNATURE_TAGS[tag]
|
|
417
|
+
if modality == "image":
|
|
418
|
+
if len(raw) != 9:
|
|
419
|
+
raise ValueError("image stream is not tag + u64")
|
|
420
|
+
return Signature(modality=modality, phash=int.from_bytes(raw[1:9], "big"))
|
|
421
|
+
if modality == "audio":
|
|
422
|
+
if len(raw) < 13:
|
|
423
|
+
raise ValueError("audio stream is shorter than its 13-byte header")
|
|
424
|
+
peak_count = int.from_bytes(raw[1:9], "big")
|
|
425
|
+
frames = int.from_bytes(raw[9:13], "big")
|
|
426
|
+
payload = raw[13:]
|
|
427
|
+
if peak_count != len(payload) or len(payload) % 6 != 0:
|
|
428
|
+
raise ValueError("audio peak count disagrees with the payload")
|
|
429
|
+
peaks = tuple(
|
|
430
|
+
(
|
|
431
|
+
int.from_bytes(payload[i : i + 4], "little"),
|
|
432
|
+
int.from_bytes(payload[i + 4 : i + 6], "little"),
|
|
433
|
+
)
|
|
434
|
+
for i in range(0, len(payload), 6)
|
|
435
|
+
)
|
|
436
|
+
return Signature(
|
|
437
|
+
modality=modality,
|
|
438
|
+
peak_count=peak_count,
|
|
439
|
+
frames=frames,
|
|
440
|
+
peaks=peaks,
|
|
441
|
+
peaks_bytes=payload,
|
|
442
|
+
)
|
|
443
|
+
if modality == "text":
|
|
444
|
+
if len(raw) < 9:
|
|
445
|
+
raise ValueError("text stream is shorter than its 9-byte header")
|
|
446
|
+
word_count = int.from_bytes(raw[1:9], "big")
|
|
447
|
+
payload = raw[9:]
|
|
448
|
+
if word_count != len(payload) or len(payload) % 8 != 0:
|
|
449
|
+
raise ValueError("text word count disagrees with the payload")
|
|
450
|
+
words = tuple(int.from_bytes(payload[i : i + 8], "big") for i in range(0, len(payload), 8))
|
|
451
|
+
return Signature(modality=modality, word_count=word_count, words=words)
|
|
452
|
+
if modality == "binary":
|
|
453
|
+
if len(raw) < 9:
|
|
454
|
+
raise ValueError("binary stream is shorter than its 9-byte header")
|
|
455
|
+
chunk_count = int.from_bytes(raw[1:9], "big")
|
|
456
|
+
payload = raw[9:]
|
|
457
|
+
if chunk_count != len(payload) or len(payload) % 32 != 0:
|
|
458
|
+
raise ValueError("binary chunk count disagrees with the payload")
|
|
459
|
+
return Signature(modality=modality, chunk_count=chunk_count, digest_bytes=payload)
|
|
460
|
+
# video
|
|
461
|
+
if len(raw) < 18:
|
|
462
|
+
raise ValueError("video stream is shorter than its 18-byte header")
|
|
463
|
+
frame_count = int.from_bytes(raw[1:5], "big")
|
|
464
|
+
width = int.from_bytes(raw[5:9], "big")
|
|
465
|
+
height = int.from_bytes(raw[9:13], "big")
|
|
466
|
+
duration_bits = int.from_bytes(raw[13:21], "big")
|
|
467
|
+
rest = raw[21:]
|
|
468
|
+
if frame_count > len(rest) or frame_count % 8 != 0:
|
|
469
|
+
raise ValueError("video frame count disagrees with the payload")
|
|
470
|
+
frames_payload = rest[:frame_count]
|
|
471
|
+
minhash_payload = rest[frame_count:]
|
|
472
|
+
if len(minhash_payload) % 8 != 0:
|
|
473
|
+
raise ValueError("video minhash payload is not whole words")
|
|
474
|
+
return Signature(
|
|
475
|
+
modality=modality,
|
|
476
|
+
frame_count=frame_count,
|
|
477
|
+
width=width,
|
|
478
|
+
height=height,
|
|
479
|
+
duration_bits=duration_bits,
|
|
480
|
+
frame_hashes=tuple(
|
|
481
|
+
int.from_bytes(frames_payload[i : i + 8], "little")
|
|
482
|
+
for i in range(0, len(frames_payload), 8)
|
|
483
|
+
),
|
|
484
|
+
minhash=tuple(
|
|
485
|
+
int.from_bytes(minhash_payload[i : i + 8], "little")
|
|
486
|
+
for i in range(0, len(minhash_payload), 8)
|
|
487
|
+
),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def parse_describe(raw: bytes) -> Description:
|
|
492
|
+
"""Re-expresses the canonical describe stream as a Description."""
|
|
493
|
+
if len(raw) < 35:
|
|
494
|
+
raise ValueError("describe stream is shorter than its 35-byte header")
|
|
495
|
+
fmt = raw[0]
|
|
496
|
+
mod = raw[1]
|
|
497
|
+
if fmt >= len(FORMATS) or mod >= len(MODALITIES):
|
|
498
|
+
raise ValueError(f"unknown describe codes {fmt}/{mod}")
|
|
499
|
+
tier1 = raw[2:34].hex()
|
|
500
|
+
modality = MODALITIES[mod]
|
|
501
|
+
sig_end = len(raw) - _facts_len(modality)
|
|
502
|
+
sig = parse_signature(raw[34:sig_end])
|
|
503
|
+
facts_raw = raw[sig_end:]
|
|
504
|
+
facts: dict = {}
|
|
505
|
+
if modality == "image":
|
|
506
|
+
_need(facts_raw, 16)
|
|
507
|
+
facts = {"width": _be32(facts_raw, 0), "height": _be32(facts_raw, 4), "keypoints": _be64(facts_raw, 8)}
|
|
508
|
+
elif modality == "audio":
|
|
509
|
+
_need(facts_raw, 18)
|
|
510
|
+
facts = {
|
|
511
|
+
"sample_rate": _be32(facts_raw, 0),
|
|
512
|
+
"channels": int.from_bytes(facts_raw[4:6], "big"),
|
|
513
|
+
"frames": _be32(facts_raw, 6),
|
|
514
|
+
"peaks": _be64(facts_raw, 10),
|
|
515
|
+
}
|
|
516
|
+
elif modality == "text":
|
|
517
|
+
_need(facts_raw, 16)
|
|
518
|
+
facts = {"words": _be64(facts_raw, 0), "canonical_len": _be64(facts_raw, 8)}
|
|
519
|
+
elif modality == "binary":
|
|
520
|
+
_need(facts_raw, 16)
|
|
521
|
+
facts = {"len": _be64(facts_raw, 0), "chunks": _be64(facts_raw, 8)}
|
|
522
|
+
else:
|
|
523
|
+
_need(facts_raw, 24)
|
|
524
|
+
facts = {
|
|
525
|
+
"width": _be32(facts_raw, 0),
|
|
526
|
+
"height": _be32(facts_raw, 4),
|
|
527
|
+
"duration_bits": _be64(facts_raw, 8),
|
|
528
|
+
"frames_sampled": _be64(facts_raw, 16),
|
|
529
|
+
}
|
|
530
|
+
return Description(format=FORMATS[fmt], modality=modality, tier1=tier1, signature=sig, facts=facts)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _be32(raw: bytes, off: int) -> int:
|
|
534
|
+
return int.from_bytes(raw[off : off + 4], "big")
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _be64(raw: bytes, off: int) -> int:
|
|
538
|
+
return int.from_bytes(raw[off : off + 8], "big")
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _need(raw: bytes, n: int) -> None:
|
|
542
|
+
if len(raw) != n:
|
|
543
|
+
raise ValueError(f"facts block is {len(raw)} bytes, want {n}")
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _facts_len(modality: str) -> int:
|
|
547
|
+
if modality == "audio":
|
|
548
|
+
return 18
|
|
549
|
+
if modality == "video":
|
|
550
|
+
return 24
|
|
551
|
+
return 16
|
sillstone/_generated.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Generated by sillstone-idl-gen. DO NOT EDIT.
|
|
2
|
+
import ctypes
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
def _load_lib():
|
|
7
|
+
ext = '.dll' if sys.platform == 'win32' else ('.dylib' if sys.platform == 'darwin' else '.so')
|
|
8
|
+
prefix = '' if sys.platform == 'win32' else 'lib'
|
|
9
|
+
lib_name = f'{prefix}sillstone_ffi{ext}'
|
|
10
|
+
dir_path = os.path.dirname(os.path.abspath(__file__))
|
|
11
|
+
candidates = [
|
|
12
|
+
os.path.join(dir_path, lib_name),
|
|
13
|
+
os.path.join(dir_path, '..', '..', '..', 'target', 'release', lib_name),
|
|
14
|
+
os.path.join(dir_path, '..', '..', '..', 'target', 'debug', lib_name),
|
|
15
|
+
]
|
|
16
|
+
for c in candidates:
|
|
17
|
+
if os.path.exists(c):
|
|
18
|
+
return ctypes.CDLL(c)
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
LIB = _load_lib()
|
|
22
|
+
|
|
23
|
+
def sillstone_version_major_raw(*args):
|
|
24
|
+
if LIB is None:
|
|
25
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
26
|
+
fn = getattr(LIB, 'sillstone_version_major')
|
|
27
|
+
fn.restype = ctypes.c_uint32
|
|
28
|
+
fn.argtypes = []
|
|
29
|
+
return fn(*args)
|
|
30
|
+
|
|
31
|
+
def sillstone_version_minor_raw(*args):
|
|
32
|
+
if LIB is None:
|
|
33
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
34
|
+
fn = getattr(LIB, 'sillstone_version_minor')
|
|
35
|
+
fn.restype = ctypes.c_uint32
|
|
36
|
+
fn.argtypes = []
|
|
37
|
+
return fn(*args)
|
|
38
|
+
|
|
39
|
+
def sillstone_version_patch_raw(*args):
|
|
40
|
+
if LIB is None:
|
|
41
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
42
|
+
fn = getattr(LIB, 'sillstone_version_patch')
|
|
43
|
+
fn.restype = ctypes.c_uint32
|
|
44
|
+
fn.argtypes = []
|
|
45
|
+
return fn(*args)
|
|
46
|
+
|
|
47
|
+
def sillstone_buffer_checksum_raw(*args):
|
|
48
|
+
if LIB is None:
|
|
49
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
50
|
+
fn = getattr(LIB, 'sillstone_buffer_checksum')
|
|
51
|
+
fn.restype = ctypes.c_uint64
|
|
52
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
|
53
|
+
return fn(*args)
|
|
54
|
+
|
|
55
|
+
def sillstone_digest_sha256_raw(*args):
|
|
56
|
+
if LIB is None:
|
|
57
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
58
|
+
fn = getattr(LIB, 'sillstone_digest_sha256')
|
|
59
|
+
fn.restype = ctypes.c_int32
|
|
60
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p]
|
|
61
|
+
return fn(*args)
|
|
62
|
+
|
|
63
|
+
def sillstone_digest_sha3_256_raw(*args):
|
|
64
|
+
if LIB is None:
|
|
65
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
66
|
+
fn = getattr(LIB, 'sillstone_digest_sha3_256')
|
|
67
|
+
fn.restype = ctypes.c_int32
|
|
68
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p]
|
|
69
|
+
return fn(*args)
|
|
70
|
+
|
|
71
|
+
def sillstone_digest_sha3_512_raw(*args):
|
|
72
|
+
if LIB is None:
|
|
73
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
74
|
+
fn = getattr(LIB, 'sillstone_digest_sha3_512')
|
|
75
|
+
fn.restype = ctypes.c_int32
|
|
76
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p]
|
|
77
|
+
return fn(*args)
|
|
78
|
+
|
|
79
|
+
def sillstone_digest_blake2b_512_raw(*args):
|
|
80
|
+
if LIB is None:
|
|
81
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
82
|
+
fn = getattr(LIB, 'sillstone_digest_blake2b_512')
|
|
83
|
+
fn.restype = ctypes.c_int32
|
|
84
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p]
|
|
85
|
+
return fn(*args)
|
|
86
|
+
|
|
87
|
+
def sillstone_digest_blake3_raw(*args):
|
|
88
|
+
if LIB is None:
|
|
89
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
90
|
+
fn = getattr(LIB, 'sillstone_digest_blake3')
|
|
91
|
+
fn.restype = ctypes.c_int32
|
|
92
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p]
|
|
93
|
+
return fn(*args)
|
|
94
|
+
|
|
95
|
+
def sillstone_math_dst1_raw(*args):
|
|
96
|
+
if LIB is None:
|
|
97
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
98
|
+
fn = getattr(LIB, 'sillstone_math_dst1')
|
|
99
|
+
fn.restype = ctypes.c_int32
|
|
100
|
+
fn.argtypes = [ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, ctypes.POINTER(ctypes.c_double)]
|
|
101
|
+
return fn(*args)
|
|
102
|
+
|
|
103
|
+
def sillstone_math_dst2_raw(*args):
|
|
104
|
+
if LIB is None:
|
|
105
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
106
|
+
fn = getattr(LIB, 'sillstone_math_dst2')
|
|
107
|
+
fn.restype = ctypes.c_int32
|
|
108
|
+
fn.argtypes = [ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, ctypes.POINTER(ctypes.c_double)]
|
|
109
|
+
return fn(*args)
|
|
110
|
+
|
|
111
|
+
def sillstone_math_dst3_raw(*args):
|
|
112
|
+
if LIB is None:
|
|
113
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
114
|
+
fn = getattr(LIB, 'sillstone_math_dst3')
|
|
115
|
+
fn.restype = ctypes.c_int32
|
|
116
|
+
fn.argtypes = [ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, ctypes.POINTER(ctypes.c_double)]
|
|
117
|
+
return fn(*args)
|
|
118
|
+
|
|
119
|
+
def sillstone_unicode_grapheme_count_raw(*args):
|
|
120
|
+
if LIB is None:
|
|
121
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
122
|
+
fn = getattr(LIB, 'sillstone_unicode_grapheme_count')
|
|
123
|
+
fn.restype = ctypes.c_int32
|
|
124
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
|
|
125
|
+
return fn(*args)
|
|
126
|
+
|
|
127
|
+
def sillstone_unicode_char_category_raw(*args):
|
|
128
|
+
if LIB is None:
|
|
129
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
130
|
+
fn = getattr(LIB, 'sillstone_unicode_char_category')
|
|
131
|
+
fn.restype = ctypes.c_int32
|
|
132
|
+
fn.argtypes = [ctypes.c_uint32, ctypes.c_void_p]
|
|
133
|
+
return fn(*args)
|
|
134
|
+
|
|
135
|
+
def sillstone_curator_detect_raw(*args):
|
|
136
|
+
if LIB is None:
|
|
137
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
138
|
+
fn = getattr(LIB, 'sillstone_curator_detect')
|
|
139
|
+
fn.restype = ctypes.c_int32
|
|
140
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32)]
|
|
141
|
+
return fn(*args)
|
|
142
|
+
|
|
143
|
+
def sillstone_curator_content_hash_raw(*args):
|
|
144
|
+
if LIB is None:
|
|
145
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
146
|
+
fn = getattr(LIB, 'sillstone_curator_content_hash')
|
|
147
|
+
fn.restype = ctypes.c_int32
|
|
148
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
|
|
149
|
+
return fn(*args)
|
|
150
|
+
|
|
151
|
+
def sillstone_curator_signature_raw(*args):
|
|
152
|
+
if LIB is None:
|
|
153
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
154
|
+
fn = getattr(LIB, 'sillstone_curator_signature')
|
|
155
|
+
fn.restype = ctypes.c_int32
|
|
156
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_size_t)]
|
|
157
|
+
return fn(*args)
|
|
158
|
+
|
|
159
|
+
def sillstone_curator_describe_raw(*args):
|
|
160
|
+
if LIB is None:
|
|
161
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
162
|
+
fn = getattr(LIB, 'sillstone_curator_describe')
|
|
163
|
+
fn.restype = ctypes.c_int32
|
|
164
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_size_t)]
|
|
165
|
+
return fn(*args)
|
|
166
|
+
|
|
167
|
+
def sillstone_curator_match_raw(*args):
|
|
168
|
+
if LIB is None:
|
|
169
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
170
|
+
fn = getattr(LIB, 'sillstone_curator_match')
|
|
171
|
+
fn.restype = ctypes.c_int32
|
|
172
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint64), ctypes.c_size_t, ctypes.POINTER(ctypes.c_uint32)]
|
|
173
|
+
return fn(*args)
|
|
174
|
+
|
|
175
|
+
def sillstone_curator_free_raw(*args):
|
|
176
|
+
if LIB is None:
|
|
177
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
178
|
+
fn = getattr(LIB, 'sillstone_curator_free')
|
|
179
|
+
fn.restype = None
|
|
180
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
|
181
|
+
return fn(*args)
|
|
182
|
+
|
|
183
|
+
def sillstone_image_phash_raw(*args):
|
|
184
|
+
if LIB is None:
|
|
185
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
186
|
+
fn = getattr(LIB, 'sillstone_image_phash')
|
|
187
|
+
fn.restype = ctypes.c_int32
|
|
188
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint64)]
|
|
189
|
+
return fn(*args)
|
|
190
|
+
|
|
191
|
+
def sillstone_image_ahash_raw(*args):
|
|
192
|
+
if LIB is None:
|
|
193
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
194
|
+
fn = getattr(LIB, 'sillstone_image_ahash')
|
|
195
|
+
fn.restype = ctypes.c_int32
|
|
196
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint64)]
|
|
197
|
+
return fn(*args)
|
|
198
|
+
|
|
199
|
+
def sillstone_image_dhash_raw(*args):
|
|
200
|
+
if LIB is None:
|
|
201
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
202
|
+
fn = getattr(LIB, 'sillstone_image_dhash')
|
|
203
|
+
fn.restype = ctypes.c_int32
|
|
204
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint64)]
|
|
205
|
+
return fn(*args)
|
|
206
|
+
|
|
207
|
+
def sillstone_image_whash_raw(*args):
|
|
208
|
+
if LIB is None:
|
|
209
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
210
|
+
fn = getattr(LIB, 'sillstone_image_whash')
|
|
211
|
+
fn.restype = ctypes.c_int32
|
|
212
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint64)]
|
|
213
|
+
return fn(*args)
|
|
214
|
+
|
|
215
|
+
def sillstone_image_colorhash_raw(*args):
|
|
216
|
+
if LIB is None:
|
|
217
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
218
|
+
fn = getattr(LIB, 'sillstone_image_colorhash')
|
|
219
|
+
fn.restype = ctypes.c_int32
|
|
220
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint64)]
|
|
221
|
+
return fn(*args)
|
|
222
|
+
|
|
223
|
+
def sillstone_text_tlsh_raw(*args):
|
|
224
|
+
if LIB is None:
|
|
225
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
226
|
+
fn = getattr(LIB, 'sillstone_text_tlsh')
|
|
227
|
+
fn.restype = ctypes.c_int32
|
|
228
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p]
|
|
229
|
+
return fn(*args)
|
|
230
|
+
|
|
231
|
+
def sillstone_text_tlsh_distance_raw(*args):
|
|
232
|
+
if LIB is None:
|
|
233
|
+
raise RuntimeError('sillstone_ffi library not loaded')
|
|
234
|
+
fn = getattr(LIB, 'sillstone_text_tlsh_distance')
|
|
235
|
+
fn.restype = ctypes.c_int32
|
|
236
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint32)]
|
|
237
|
+
return fn(*args)
|
|
238
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sillstone
|
|
3
|
+
Version: 0.2.0b1
|
|
4
|
+
Summary: Pure-Rust zero-dependency multimedia, hashing, and array computing suite
|
|
5
|
+
Author: Sillstone Contributors
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/sillstone/sillstone
|
|
8
|
+
Project-URL: Repository, https://github.com/sillstone/sillstone
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
sillstone/__init__.py,sha256=7R5WusbC1YvlT5w5balSircs6sYC3dAp8Se3l0nby4s,17881
|
|
2
|
+
sillstone/_generated.py,sha256=Yye6k-Z1f7fxOx4I2fWwn-PcQoNcPnFKUGdlev1eqZI,9406
|
|
3
|
+
sillstone-0.2.0b1.dist-info/METADATA,sha256=5fuGC3dGUAFnk0CFMBGALJq5Dy2TMyXuCmh2TJOrERw,535
|
|
4
|
+
sillstone-0.2.0b1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
sillstone-0.2.0b1.dist-info/top_level.txt,sha256=gU-OK-Ivn1TF9HzJTn8UAmC2-gZ3anvkUA2dOTMTDMg,10
|
|
6
|
+
sillstone-0.2.0b1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sillstone
|