b3c32 0.0.2__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.
b3c32/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ # python/src/b3c32/__init__.py
2
+ """
3
+ b3c32: compact, hand-writable, prefix-matchable XOF hashes.
4
+ Author: Marcus Grant
5
+ Date: 2026-07-24
6
+ License: Apache-2.0
7
+ """
8
+
9
+ from b3c32.core import (
10
+ CROCKFORD32_ALPHABET,
11
+ coerce_crockford_b32,
12
+ decode_crockford_b32,
13
+ encode_crockford_b32,
14
+ hash_b32,
15
+ hash_digest,
16
+ )
17
+ from b3c32.errors import CoercionError, UncertifiedWidthError
18
+ from b3c32.smoke import verify_conformance
19
+
20
+ __all__ = [
21
+ "CROCKFORD32_ALPHABET",
22
+ "CoercionError",
23
+ "UncertifiedWidthError",
24
+ "coerce_crockford_b32",
25
+ "decode_crockford_b32",
26
+ "encode_crockford_b32",
27
+ "hash_b32",
28
+ "hash_digest",
29
+ "verify_conformance",
30
+ ]
b3c32/core.py ADDED
@@ -0,0 +1,132 @@
1
+ # python/src/b3c32/core.py
2
+ """
3
+ BLAKE3 hashing and Crockford Base32 codec for b3c32 codes.
4
+ Author: Marcus Grant
5
+ Date: 2026-01-26
6
+ Revisions: [2026-07-24]
7
+ License: Apache-2.0
8
+ """
9
+
10
+ from blake3 import blake3
11
+
12
+ from b3c32.errors import CoercionError, UncertifiedWidthError
13
+
14
+ _CERTIFIED_BITS = frozenset({120})
15
+ _TRANS_CROCKFORD_AMBIG = str.maketrans({"O": "0", "I": "1", "L": "1"})
16
+
17
+ CROCKFORD32_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
18
+
19
+
20
+ def hash_digest(data: bytes, bits: int) -> bytes:
21
+ """Compute the content digest at a certified width.
22
+
23
+ Unkeyed BLAKE3 XOF sliced to bits, gated on the certified set.
24
+
25
+ Args:
26
+ data: The bytes to hash.
27
+ bits: Digest width; must be in the certified set.
28
+
29
+ Returns:
30
+ The digest of bits // 8 bytes.
31
+
32
+ Raises:
33
+ UncertifiedWidthError: bits is not a certified width.
34
+ """
35
+ if bits not in _CERTIFIED_BITS:
36
+ raise UncertifiedWidthError(bits)
37
+ return blake3(data).digest(length=bits // 8)
38
+
39
+
40
+ def encode_crockford_b32(data: bytes) -> str:
41
+ """Encode bytes as Crockford Base32, low-pad bitstream.
42
+
43
+ Bits are taken MSB-first as a single stream, grouped into 5-bit
44
+ units from the left; a final partial group is zero-extended in the
45
+ least-significant positions (low-pad) per the Base32-for-Humans
46
+ draft, Section 3.1.
47
+
48
+ Args:
49
+ data: The bytes to encode.
50
+
51
+ Returns:
52
+ Crockford Base32 string, length ceil(len(data) * 8 / 5).
53
+ """
54
+ num, bit_count = int.from_bytes(data, byteorder="big"), len(data) * 8
55
+ symbol_count = (bit_count + 4) // 5 # ceil(bits / 5)
56
+ num <<= (5 - bit_count % 5) % 5 # low-pad to next 5-bit boundary
57
+ symbols = []
58
+ for i in range(symbol_count):
59
+ symbol_num = (num >> (5 * (symbol_count - 1 - i))) & 0b11111
60
+ symbols.append(CROCKFORD32_ALPHABET[symbol_num])
61
+ return "".join(symbols)
62
+
63
+
64
+ def hash_b32(data: bytes, bits: int) -> str:
65
+ """Compute the canonical code at a certified width.
66
+
67
+ Composes hash_digest and the Crockford encoder.
68
+
69
+ Args:
70
+ data: The bytes to hash.
71
+ bits: Digest width; must be in the certified set.
72
+
73
+ Returns:
74
+ Crockford Base32 code of bits // 5 characters.
75
+
76
+ Raises:
77
+ UncertifiedWidthError: bits is not a certified width.
78
+ """
79
+ return encode_crockford_b32(hash_digest(data, bits))
80
+
81
+
82
+ def decode_crockford_b32(code: str) -> bytes:
83
+ """Decode canonical Crockford Base32 to bytes.
84
+
85
+ Symbols are taken MSB-first in 5-bit groups. Trailing bits that do
86
+ not complete a byte are discarded: they are pad the encoder added to
87
+ fill a whole symbol, never part of the input. Reference
88
+ implementation of the encoding's inverse.
89
+
90
+ Args:
91
+ code: Canonical (uppercase, alphabet-only) Crockford string.
92
+
93
+ Returns:
94
+ The decoded bytes.
95
+
96
+ Raises:
97
+ ValueError: A character is outside the Crockford alphabet.
98
+ """
99
+ accumulated_int = 0
100
+ for symbol in code:
101
+ symbol_int_value = CROCKFORD32_ALPHABET.index(symbol)
102
+ # Shift left 5 to make room, OR to append this symbol's bits
103
+ accumulated_int = (accumulated_int << 5) | symbol_int_value
104
+ bit_count = 5 * len(code)
105
+ # Trailing bits past the last whole byte are encoder pad, so drop them
106
+ accumulated_int >>= bit_count % 8
107
+ return accumulated_int.to_bytes(bit_count // 8, "big")
108
+
109
+
110
+ def coerce_crockford_b32(code: str) -> str:
111
+ """Coerce user-supplied code for non-strict decodes/lookups.
112
+
113
+ Args:
114
+ code: User-supplied code string.
115
+
116
+ Returns:
117
+ Canonical uppercase string with ambiguous chars normalized.
118
+
119
+ Raises:
120
+ CoercionError: If code is empty or contains invalid characters.
121
+ """
122
+ s = code.strip().upper()
123
+ s = s.replace("-", "").replace(" ", "")
124
+ s = s.translate(_TRANS_CROCKFORD_AMBIG)
125
+
126
+ if not s:
127
+ raise CoercionError()
128
+
129
+ for ch in s:
130
+ if ch not in CROCKFORD32_ALPHABET:
131
+ raise CoercionError(ch)
132
+ return s
b3c32/errors.py ADDED
@@ -0,0 +1,35 @@
1
+ # python/src/b3c32/errors.py
2
+ """
3
+ Exception types for b3c32.
4
+ Author: Marcus Grant
5
+ Date: 2026-07-24
6
+ License: Apache-2.0
7
+ """
8
+
9
+
10
+ class UncertifiedWidthError(ValueError):
11
+ """Raised when a requested digest width is not in the certified set.
12
+
13
+ Subclasses ValueError so naive callers still catch it; distinct so
14
+ consumer smoke tests can assert the gate survived an upgrade.
15
+ """
16
+
17
+ def __init__(self, bits: int) -> None:
18
+ self.bits = bits
19
+ super().__init__(f"digest width {bits} bits is not certified by contract")
20
+
21
+
22
+ class CoercionError(ValueError):
23
+ """Raised when user-supplied code cannot be coerced to canonical form.
24
+
25
+ Carries the offending character, or None when the input was empty
26
+ after normalization.
27
+ """
28
+
29
+ def __init__(self, char: str | None = None) -> None:
30
+ self.char = char
31
+ if char is None:
32
+ msg = "cannot coerce input, empty after normalization"
33
+ else:
34
+ msg = f"cannot coerce input, offending char: {char}"
35
+ super().__init__(msg)
b3c32/smoke.py ADDED
@@ -0,0 +1,65 @@
1
+ # python/src/b3c32/smoke.py
2
+ """
3
+ Consumer smoke checks for contract drift detection.
4
+ Author: Marcus Grant
5
+ Date: 2026-07-27
6
+ License: Apache-2.0
7
+ """
8
+
9
+ from b3c32 import (
10
+ CoercionError,
11
+ UncertifiedWidthError,
12
+ coerce_crockford_b32,
13
+ decode_crockford_b32,
14
+ encode_crockford_b32,
15
+ hash_b32,
16
+ )
17
+
18
+
19
+ def verify_conformance() -> None:
20
+ """Assert the installed b3c32 still honors the contract surface.
21
+
22
+ One curated assertion per contract claim a consumer depends on.
23
+ Raises AssertionError naming the failed claim, or the library's
24
+ own error types if raise behavior itself has drifted.
25
+
26
+ Scope: drift detection through the public API only. Deep
27
+ certification, including XOF output past the 64-byte block and
28
+ exhaustive codec domains, is the library's own suite against the
29
+ pinned reference file. A consumer build passing here is the
30
+ certified surface at 120 bits; it is not itself a certification.
31
+ """
32
+ code = hash_b32(bytes(i % 251 for i in range(0)), 120)
33
+ msg = f"smoke: pipeline vector mismatch at 120 bits, got {code}"
34
+ assert code == "NW9MKEFNZ6GTD8209QN3DQ69", msg
35
+
36
+ digest_wide = bytes(range(20))
37
+ narrow = encode_crockford_b32(digest_wide[:15])
38
+ wide = encode_crockford_b32(digest_wide)
39
+ msg = "smoke: 40-bit prefix relation broken on aligned widths"
40
+ assert wide.startswith(narrow), msg
41
+
42
+ encoded = encode_crockford_b32(b"foobar")
43
+ msg = f"smoke: non-aligned encode mismatch, got {encoded}"
44
+ assert encoded == "CSQPYRK1E8", msg
45
+
46
+ decoded = decode_crockford_b32("CSQPYRK1E8")
47
+ assert decoded == b"foobar", f"smoke: decode inversion mismatch, got {decoded!r}"
48
+
49
+ coerced = coerce_crockford_b32("oil-1")
50
+ msg = f"smoke: coercion mismatch, got {coerced}"
51
+ assert coerced == "0111", msg
52
+
53
+ try:
54
+ coerce_crockford_b32("!")
55
+ except CoercionError:
56
+ pass
57
+ else:
58
+ raise AssertionError("smoke: CoercionError not raised on invalid char")
59
+
60
+ try:
61
+ hash_b32(b"", 160)
62
+ except UncertifiedWidthError:
63
+ pass
64
+ else:
65
+ raise AssertionError("smoke: UncertifiedWidthError not raised at 160")
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.5
2
+ Name: b3c32
3
+ Version: 0.0.2
4
+ Summary: Compact, hand-writable, prefix-matchable BLAKE3 XOF hashes in Crockford Base32
5
+ Project-URL: Homepage, https://github.com/marcus-grant/b3c32
6
+ License-Expression: Apache-2.0
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Topic :: Security :: Cryptography
11
+ Requires-Python: >=3.12
12
+ Requires-Dist: blake3
13
+ Description-Content-Type: text/markdown
14
+
15
+ # b3c32 (Python)
16
+
17
+ Compact, hand-writable, prefix-matchable XOF hashes and identities for
18
+ anything.
19
+
20
+ This is the Python implementation of b3c32, and until otherwise stated
21
+ it is the reference implementation: the contract artifacts at the
22
+ project root are certified against it, and other language
23
+ implementations are certified against the same frozen vectors. That
24
+ status is expected to change once a native Rust implementation exists.
25
+
26
+ Codes are unkeyed BLAKE3 sliced on 40-bit boundaries, encoded as
27
+ low-pad bitstream Crockford Base32. The result is short enough to write
28
+ by hand, unambiguous enough to read aloud, and prefix-matchable so a
29
+ truncated code still resolves.
30
+
31
+ ## Use
32
+
33
+ ```python
34
+ from b3c32 import hash_b32
35
+
36
+ code = hash_b32(b"hello", 120)
37
+ ```
38
+
39
+ Width is in bits. 120 is the current certified width. This is not a
40
+ ceiling on usefulness: slice the returned code at any multiple of 40
41
+ bits, which is 5 bytes or 8 characters, and the short form is a true
42
+ string prefix of the long one. Store the full code, index a prefix.
43
+
44
+ The package also exports `hash_digest` for raw digest bytes, the codec
45
+ functions `encode_crockford_b32`, `decode_crockford_b32`, and
46
+ `coerce_crockford_b32`, the alphabet as `CROCKFORD32_ALPHABET`, and the
47
+ error types `UncertifiedWidthError` and `CoercionError`.
48
+
49
+ ## Consuming
50
+
51
+ Pin an exact version. The project is pre-1.0 and no compatibility
52
+ policy is in force, so any release may change the contract. Call
53
+ `verify_conformance` in your own suite to learn when it does:
54
+
55
+ ```python
56
+ from b3c32 import verify_conformance
57
+
58
+ def test_b3c32_contract():
59
+ verify_conformance()
60
+ ```
61
+
62
+ It raises AssertionError naming the failed claim. Its scope is drift
63
+ detection through the public API at the certified width; deep
64
+ certification is this project's own suite against the pinned BLAKE3
65
+ reference vectors.
66
+
67
+ ## Status
68
+
69
+ Pre-release. Certification hardening is in progress. The full scheme
70
+ definition, the normative conformance contract, and the frozen vectors
71
+ live at the project root:
72
+ <https://github.com/marcus-grant/b3c32>
73
+
@@ -0,0 +1,7 @@
1
+ b3c32/__init__.py,sha256=jQItlLbASG-f3Nki5wVStNy0aAufFbDAFvCttjQBHn8,663
2
+ b3c32/core.py,sha256=V2jwI32F_wig3NZ4GaYsafqiViy8WkpJwAYh4Xzuw1w,3938
3
+ b3c32/errors.py,sha256=Hdmda5peD4Ih-ZKENh36iDcr3-rnaAIO-ulXZbRLlrA,1034
4
+ b3c32/smoke.py,sha256=bNl-8oLgOwg74ZgmiZG1skE1IR6P5LUzZ7teufFRktY,2129
5
+ b3c32-0.0.2.dist-info/METADATA,sha256=J-qXbOOLT5wC9Nc2tzRnylSa-uFjUyT5Qc9bfdMR5PY,2544
6
+ b3c32-0.0.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
7
+ b3c32-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any