protlabel 4.6.0__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.
protlabel/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """protlabel — Embedding Annotation Transfer (EAT) engine.
2
+
3
+ Nearest-neighbour label transfer in protein-language-model embedding space,
4
+ with the goPredSim reliability index. Pure numpy (plus the standard library);
5
+ no protspace imports.
6
+ """
7
+
8
+ from importlib.metadata import PackageNotFoundError, version
9
+
10
+ from protlabel.lookup import Lookup
11
+ from protlabel.transfer import Prediction, eat
12
+
13
+ try:
14
+ # Report the installed distribution version rather than a hard-coded literal
15
+ # that would silently drift across releases.
16
+ __version__ = version("protlabel")
17
+ except PackageNotFoundError: # pragma: no cover - source/uninstalled fallback
18
+ __version__ = "0.0.0"
19
+
20
+ __all__ = ["Lookup", "Prediction", "eat", "__version__"]
protlabel/backends.py ADDED
@@ -0,0 +1,183 @@
1
+ """Exact (brute-force) k-nearest-neighbour search over reference embeddings.
2
+
3
+ Distances are computed with a chunked BLAS matrix product (numpy ``@``) plus
4
+ ``argpartition`` — the GEMM path, which is roughly an order of magnitude faster
5
+ than a naive per-pair distance loop while staying pure-numpy (no third-party
6
+ distance/ANN library).
7
+
8
+ The query axis is chunked and, adaptively, bounded against the reference axis so
9
+ the per-chunk distance block is kept at or below ``max_block_bytes`` (default
10
+ 256 MiB) regardless of ``n_refs``. This keeps peak memory close to the
11
+ reference matrix itself even at Swiss-Prot scale (~570 000 references).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import warnings
17
+
18
+ import numpy as np
19
+
20
+ _METRICS = {"euclidean", "cosine"}
21
+
22
+ # The float32 GEMM distance loses precision to catastrophic cancellation for
23
+ # high-norm pLM embeddings, so its top-k *selection* (argpartition) can drop a
24
+ # true nearest whose float32 distance is noise. The exact float64 rerank can
25
+ # only reorder the candidates selection kept — it cannot recover a nearest that
26
+ # was never selected. So over-fetch a wider candidate pool before the rerank;
27
+ # the pad gives small k (e.g. the default k=1) a real margin, the factor scales
28
+ # it for larger k. Cost is still O(b * k_pool * d) << the O(b * n_refs) GEMM.
29
+ _RERANK_OVERFETCH = 2 # multiplicative widening of the candidate pool
30
+ _RERANK_PAD = 16 # additive floor so small k still over-fetches
31
+
32
+ # Peak bytes-per-element the float64 rerank holds at once for its
33
+ # (eff_chunk, k_pool, d) tensors: _exact_distances materialises two float64
34
+ # copies simultaneously (the upcast candidates and their difference/normalised
35
+ # form) = 16 B/elem; 24 leaves headroom for the transient f32->f64 upcast.
36
+ _RERANK_PEAK_BYTES = 24
37
+
38
+
39
+ def _rerank_pool_size(k: int, n_refs: int) -> int:
40
+ """Number of float32-selected candidates to rerank in float64 for a top-k."""
41
+ return min(n_refs, max(k * _RERANK_OVERFETCH, k + _RERANK_PAD))
42
+
43
+
44
+ def _exact_distances(block: np.ndarray, sel: np.ndarray, metric: str) -> np.ndarray:
45
+ """Distances from each query (block[i]) to its pool candidates (sel[i]) in float64.
46
+
47
+ The fast GEMM block selects a candidate pool; this recomputes their
48
+ distances by direct subtraction in float64 to avoid the catastrophic
49
+ cancellation of ``||q||^2 - 2 q.r + ||r||^2`` for near-identical vectors.
50
+ Cost is O(b * k_pool * d), not O(b * n_refs), so it is cheap.
51
+ """
52
+ blk = block[:, None, :].astype(np.float64) # (b, 1, d)
53
+ sel = sel.astype(np.float64) # (b, k, d)
54
+ if metric == "euclidean":
55
+ diff = blk - sel
56
+ return np.sqrt(np.einsum("bkd,bkd->bk", diff, diff))
57
+ bn = np.linalg.norm(blk, axis=2, keepdims=True)
58
+ bn = np.where(bn == 0.0, 1.0, bn)
59
+ sn = np.linalg.norm(sel, axis=2, keepdims=True)
60
+ sn = np.where(sn == 0.0, 1.0, sn)
61
+ cos = np.einsum("bkd,bkd->bk", blk / bn, sel / sn)
62
+ return np.clip(1.0 - cos, 0.0, 2.0)
63
+
64
+
65
+ def nearest(
66
+ queries: np.ndarray,
67
+ refs: np.ndarray,
68
+ k: int,
69
+ metric: str = "euclidean",
70
+ chunk: int = 4096,
71
+ max_block_bytes: int = 256 * 1024 * 1024,
72
+ ) -> tuple[np.ndarray, np.ndarray]:
73
+ """Return (idx, dist) of the k nearest *references* per query.
74
+
75
+ idx[i] -> indices into ``refs`` of the k nearest, ascending by distance.
76
+ dist[i] -> the corresponding distances.
77
+ k is capped to the number of references.
78
+
79
+ Memory behaviour
80
+ ----------------
81
+ The per-chunk distance block has shape ``(query_chunk, n_refs)``. When
82
+ ``n_refs`` is large (e.g. Swiss-Prot ~570 000), even a modest ``chunk`` of
83
+ 4096 yields a multi-GiB block. To bound this, the effective query-chunk
84
+ size ``eff_chunk`` is computed as ``min(chunk, max_block_bytes // (n_refs *
85
+ 8))`` so each block stays at or below ``max_block_bytes`` (default 256 MiB)
86
+ independent of ``n_refs``. The factor 8 budgets for a float64-sized block;
87
+ each metric holds a single copy of the reference matrix (cosine folds the
88
+ per-reference norm into the dot product rather than storing a normalized
89
+ second copy) plus a few float32 ``(query_chunk, n_refs)`` temporaries, so the
90
+ real peak is comparable to (not far below) that budget. The exact-distance
91
+ recompute touches the over-fetched candidate pool (a small multiple of k, not
92
+ n_refs); ``eff_chunk`` is additionally capped so those ``(eff_chunk, k_pool,
93
+ d)`` float64 temporaries also stay within ``max_block_bytes`` (they scale with
94
+ the embedding dim, so a small reference set queried at high dim would
95
+ otherwise blow the budget). Peak memory therefore stays bounded — close to
96
+ the reference matrix at Swiss-Prot scale, laptop-feasible.
97
+ """
98
+ if metric not in _METRICS:
99
+ raise ValueError(f"Unknown metric {metric!r}; expected 'euclidean' or 'cosine'")
100
+
101
+ if k < 1:
102
+ raise ValueError("k must be >= 1")
103
+
104
+ queries = np.ascontiguousarray(queries, dtype=np.float32)
105
+ refs = np.ascontiguousarray(refs, dtype=np.float32)
106
+ n_refs = refs.shape[0]
107
+ if n_refs == 0:
108
+ raise ValueError("refs must contain at least one reference embedding")
109
+ k = min(k, n_refs)
110
+ k_pool = _rerank_pool_size(k, n_refs)
111
+
112
+ # Adaptively shrink the query chunk so BOTH per-chunk tensors stay within
113
+ # max_block_bytes:
114
+ # * the (eff_chunk, n_refs) distance block (float64-sized = 8 B/row-elem),
115
+ # * the (eff_chunk, k_pool, d) float64 rerank temporaries, which scale with
116
+ # the embedding dim d (not n_refs), so the block budget alone leaves them
117
+ # unbounded for small-reference / high-dim / many-query workloads.
118
+ dim = refs.shape[1]
119
+ block_row_bytes = max(1, n_refs * 8)
120
+ rerank_row_bytes = max(1, k_pool * dim * _RERANK_PEAK_BYTES)
121
+ eff_chunk = max(
122
+ 1,
123
+ min(
124
+ chunk,
125
+ max_block_bytes // block_row_bytes,
126
+ max_block_bytes // rerank_row_bytes,
127
+ ),
128
+ )
129
+
130
+ idx_out = np.empty((queries.shape[0], k), dtype=np.int64)
131
+ dist_out = np.empty((queries.shape[0], k), dtype=np.float32)
132
+
133
+ # Per-reference squared norms (shape (n_refs,), tiny) — used by both metrics:
134
+ # euclidean: ||q-r||^2 = ||q||^2 - 2 q.r + ||r||^2
135
+ # cosine: cos = q.r / (||q|| ||r||) — folding ||r|| in here (rather than
136
+ # storing a second, full-size normalized copy of the reference
137
+ # matrix) keeps cosine at 1x reference memory, like euclidean.
138
+ refs_sq = np.einsum("ij,ij->i", refs, refs)
139
+ refs_norm = np.sqrt(refs_sq) if metric == "cosine" else None
140
+
141
+ # Row index for the per-chunk fancy-indexing; sliced to each block's length
142
+ # so it is allocated once rather than per chunk.
143
+ rows_full = np.arange(eff_chunk)[:, None]
144
+
145
+ for start in range(0, queries.shape[0], eff_chunk):
146
+ block = queries[start : start + eff_chunk]
147
+ with warnings.catch_warnings():
148
+ # GEMM on near-overflowing float16-origin values can emit harmless
149
+ # RuntimeWarnings; the inputs are upcast float32 so values are safe.
150
+ warnings.simplefilter("ignore", RuntimeWarning)
151
+ if metric == "euclidean":
152
+ block_sq = np.einsum("ij,ij->i", block, block)
153
+ cross = block @ refs.T # (b, n_refs), BLAS GEMM
154
+ d2 = block_sq[:, None] - 2.0 * cross + refs_sq[None, :]
155
+ np.maximum(d2, 0.0, out=d2) # clip tiny negative fp artifacts
156
+ d = np.sqrt(d2, dtype=np.float32)
157
+ else: # cosine
158
+ cross = block @ refs.T # (b, n_refs) raw dot products, BLAS GEMM
159
+ block_norm = np.sqrt(np.einsum("ij,ij->i", block, block))
160
+ # Zero-magnitude rows -> safe norm 1; the dot product is 0 there,
161
+ # so the cosine distance stays a finite 1.0 (matches normalizing a
162
+ # zero vector to a zero vector).
163
+ safe_b = np.where(block_norm == 0.0, 1.0, block_norm)
164
+ safe_r = np.where(refs_norm == 0.0, 1.0, refs_norm)
165
+ d = (1.0 - cross / (safe_b[:, None] * safe_r[None, :])).astype(
166
+ np.float32
167
+ )
168
+ np.clip(d, 0.0, 2.0, out=d) # cosine distance in [0, 2]
169
+
170
+ # Over-fetch a candidate pool with the fast (float32) block, recompute
171
+ # the pool's distances exactly in float64, then take the true top-k from
172
+ # it — so both the *selection* and the reported distance are robust to
173
+ # the float32 GEMM's catastrophic cancellation for near-identical (or
174
+ # high-norm) vectors. Reranking a too-narrow pool cannot recover a true
175
+ # nearest the float32 selection dropped; see _rerank_pool_size.
176
+ cand = np.argpartition(d, kth=k_pool - 1, axis=1)[:, :k_pool] # unsorted pool
177
+ rows = rows_full[: block.shape[0]]
178
+ exact = _exact_distances(block, refs[cand], metric) # (b, k_pool) float64
179
+ order = np.argsort(exact, axis=1)[:, :k] # true top-k, ascending by exact dist
180
+ idx_out[start : start + block.shape[0]] = cand[rows, order]
181
+ dist_out[start : start + block.shape[0]] = exact[rows, order].astype(np.float32)
182
+
183
+ return idx_out, dist_out
protlabel/lookup.py ADDED
@@ -0,0 +1,75 @@
1
+ """A persistable reference lookup: embeddings + ids + labels, plus query().
2
+
3
+ Serialized as a single .npz sidecar so it can live next to a bundle or in a
4
+ cache dir and be rebuilt on demand from the source HDF5.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+
14
+ from protlabel.transfer import Prediction, eat
15
+
16
+
17
+ @dataclass
18
+ class Lookup:
19
+ """Reference set for embedding annotation transfer."""
20
+
21
+ embeddings: np.ndarray # (N, D) float32
22
+ ids: list[str]
23
+ labels: list[str]
24
+ metric: str = "euclidean"
25
+ model: str = field(default="")
26
+
27
+ def query(
28
+ self, query_emb: np.ndarray, query_ids: list[str], *, k: int = 1
29
+ ) -> list[Prediction]:
30
+ """Transfer labels to query embeddings from this lookup."""
31
+ return eat(
32
+ query_emb,
33
+ query_ids,
34
+ self.embeddings,
35
+ self.ids,
36
+ self.labels,
37
+ k=k,
38
+ metric=self.metric,
39
+ )
40
+
41
+ def save(self, path: Path) -> None:
42
+ """Serialize to a .npz sidecar.
43
+
44
+ Embeddings are stored as float32 (lossless round-trip); ids/labels are
45
+ stored as unicode arrays rather than pickled object arrays so the sidecar
46
+ can be loaded with ``allow_pickle=False`` (no arbitrary-code-execution
47
+ surface on load). ids/labels must not contain trailing NUL bytes, which
48
+ numpy's fixed-width unicode arrays strip on round-trip.
49
+ """
50
+ path = Path(path)
51
+ path.parent.mkdir(parents=True, exist_ok=True)
52
+ np.savez(
53
+ path,
54
+ # asarray, not astype: no copy of the (potentially multi-GB) matrix
55
+ # when it is already float32 (the common case).
56
+ embeddings=np.asarray(self.embeddings, dtype=np.float32),
57
+ ids=np.asarray(self.ids, dtype=np.str_),
58
+ labels=np.asarray(self.labels, dtype=np.str_),
59
+ metric=np.asarray(self.metric, dtype=np.str_),
60
+ model=np.asarray(self.model, dtype=np.str_),
61
+ )
62
+
63
+ @classmethod
64
+ def load(cls, path: Path) -> Lookup:
65
+ """Load a .npz sidecar (with pickling disabled)."""
66
+ with np.load(path, allow_pickle=False) as data:
67
+ return cls(
68
+ # asarray, not astype: the npz array is already float32 (save
69
+ # writes it as such), so this avoids a redundant full copy.
70
+ embeddings=np.asarray(data["embeddings"], dtype=np.float32),
71
+ ids=[str(x) for x in data["ids"]],
72
+ labels=[str(x) for x in data["labels"]],
73
+ metric=str(data["metric"]),
74
+ model=str(data["model"]),
75
+ )
@@ -0,0 +1,45 @@
1
+ """goPredSim reliability index: map an embedding distance to a [0,1] confidence.
2
+
3
+ Cosine: s(d) = 1 - d (clamped to [0,1]; cosine distance in [0,2])
4
+ Euclidean: s(d) = 0.5 / (0.5 + d) (1.0 at d=0, 0.5 at d=0.5, ->0 as d->inf)
5
+
6
+ Both branches return a value in [0, 1], and a non-finite distance (NaN/inf) maps
7
+ to 0.0 so an invalid neighbour never yields a spuriously high confidence.
8
+
9
+ The ``protspace transfer`` CLI defaults to cosine — ``1 - d`` is the cosine
10
+ similarity, which is directly interpretable and naturally bounded. The lower-level
11
+ ``protlabel`` engine API (``eat``/``Lookup``/``nearest``) keeps euclidean as its
12
+ default to match the published goPredSim reference implementation.
13
+
14
+ The euclidean transform ``0.5 / (0.5 + d)`` is the published goPredSim constant,
15
+ calibrated against ProtT5 distances. On embedding spaces whose raw distances are
16
+ much larger than ~0.5 (common) it collapses toward 0 even for near neighbours, so
17
+ the *ordering* of confidences stays meaningful but the absolute value is hard to
18
+ interpret.
19
+
20
+ Reference: Littmann et al., Sci Rep 2021 (Eq. 5); goPredSim calc_reliability_index.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import math
26
+
27
+
28
+ def similarity(distance: float, metric: str) -> float:
29
+ """Per-neighbour distance->similarity (the goPredSim reliability transform).
30
+
31
+ Always returns a value in [0, 1]. The kNN backend never emits a negative
32
+ distance (euclidean is a clamped ``sqrt``; cosine distance is in [0, 2]), so
33
+ the negative-distance branch is purely defensive for direct callers of this
34
+ function: a negative distance is treated as 0, and a non-finite distance
35
+ (NaN/inf) maps to 0.0 so an invalid neighbour never yields a spuriously high
36
+ confidence.
37
+ """
38
+ if not math.isfinite(distance):
39
+ return 0.0
40
+ if metric == "cosine":
41
+ return min(1.0, max(0.0, 1.0 - distance))
42
+ if metric == "euclidean":
43
+ # d >= 0 makes 0.5 / (0.5 + d) fall in (0, 1] already; no clamp needed.
44
+ return 0.5 / (0.5 + max(0.0, distance))
45
+ raise ValueError(f"Unknown metric {metric!r}; expected 'euclidean' or 'cosine'")
protlabel/transfer.py ADDED
@@ -0,0 +1,97 @@
1
+ """Embedding annotation transfer: kNN -> reliability index -> transferred label.
2
+
3
+ Implements the goPredSim aggregation (Littmann et al. 2021, Eq. 5):
4
+ RI(p) = (1/eff_k) * sum over neighbours carrying label p of similarity(d),
5
+ where ``eff_k`` is the number of neighbours actually used (``k`` capped to the
6
+ number of references). The transferred label is argmax RI(p); its source is the
7
+ nearest neighbour carrying that label. Ties are broken deterministically by
8
+ smallest source distance, then by lexically smallest label, so the result never
9
+ depends on the (arbitrary) ordering of equidistant neighbours.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+
16
+ import numpy as np
17
+
18
+ from protlabel.backends import nearest
19
+ from protlabel.reliability import similarity
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Prediction:
24
+ """One transferred annotation for a query protein."""
25
+
26
+ query_id: str
27
+ label: str
28
+ source_id: str
29
+ distance: float
30
+ reliability: float
31
+ k: int
32
+ metric: str
33
+
34
+
35
+ def eat(
36
+ query_emb: np.ndarray,
37
+ query_ids: list[str],
38
+ ref_emb: np.ndarray,
39
+ ref_ids: list[str],
40
+ ref_labels: list[str],
41
+ *,
42
+ k: int = 1,
43
+ metric: str = "euclidean",
44
+ ) -> list[Prediction]:
45
+ """Transfer the best-guess label to each query from its k nearest references."""
46
+ if not (len(ref_ids) == len(ref_labels) == ref_emb.shape[0]):
47
+ raise ValueError("ref_emb, ref_ids and ref_labels must have equal length")
48
+ if ref_emb.shape[0] == 0:
49
+ raise ValueError("No reference embeddings to transfer from")
50
+ if len(query_ids) != query_emb.shape[0]:
51
+ raise ValueError("query_emb and query_ids must have equal length")
52
+
53
+ idx, dist = nearest(query_emb, ref_emb, k=k, metric=metric)
54
+ eff_k = idx.shape[1]
55
+ predictions: list[Prediction] = []
56
+
57
+ for qi, query_id in enumerate(query_ids):
58
+ neigh_idx = idx[qi]
59
+ neigh_dist = dist[qi]
60
+ # Accumulate RI per label and track the nearest source per label.
61
+ ri_by_label: dict[str, float] = {}
62
+ nearest_src: dict[str, tuple[float, str]] = {}
63
+ for j, ref_i in enumerate(neigh_idx):
64
+ lab = ref_labels[ref_i]
65
+ d = float(neigh_dist[j])
66
+ ri_by_label[lab] = ri_by_label.get(lab, 0.0) + similarity(d, metric)
67
+ # Break exact-distance ties on the lexically smallest source id so the
68
+ # reported provenance is independent of the (arbitrary) neighbour order
69
+ # among equidistant references, matching the label tie-break below.
70
+ candidate = (d, ref_ids[ref_i])
71
+ if lab not in nearest_src or candidate < nearest_src[lab]:
72
+ nearest_src[lab] = candidate
73
+ # Pick the highest-RI label; break ties deterministically by smallest
74
+ # source distance, then lexically smallest label. This makes the choice
75
+ # independent of the order of equidistant neighbours (whose argsort order
76
+ # is otherwise arbitrary). For distinct distances the nearest neighbour's
77
+ # label wins, as before.
78
+ best_label = min(
79
+ ri_by_label,
80
+ key=lambda p: (-ri_by_label[p], nearest_src[p][0], p),
81
+ )
82
+ # Normalise by eff_k (the goPredSim 1/k term, k capped to n_refs).
83
+ ri = ri_by_label[best_label] / eff_k
84
+ src_dist, src_id = nearest_src[best_label]
85
+ predictions.append(
86
+ Prediction(
87
+ query_id=query_id,
88
+ label=best_label,
89
+ source_id=src_id,
90
+ distance=src_dist,
91
+ reliability=ri,
92
+ k=eff_k,
93
+ metric=metric,
94
+ )
95
+ )
96
+
97
+ return predictions
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: protlabel
3
+ Version: 4.6.0
4
+ Summary: Embedding Annotation Transfer (EAT): nearest-neighbour label transfer in protein-language-model embedding space
5
+ Project-URL: Homepage, https://github.com/tsenoner/protspace
6
+ Project-URL: Repository, https://github.com/tsenoner/protspace
7
+ Author-email: Tobias Senoner <tobias.senoner@tum.de>
8
+ License-Expression: GPL-3.0
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: numpy>=1.23.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ # protlabel
14
+
15
+ **Embedding Annotation Transfer (EAT) engine** — nearest-neighbour label transfer in
16
+ protein-language-model (pLM) embedding space, with the goPredSim reliability index.
17
+
18
+ `protlabel` is a small, dependency-light library (numpy only). It is
19
+ ProtSpace-agnostic by design — it imports nothing from `protspace` — so it is
20
+ independently testable and reusable from notebooks, `protspace_uniprot`, or any
21
+ other project. The `protspace transfer` CLI is a thin consumer of this engine.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install protlabel
27
+ ```
28
+
29
+ ## Use
30
+
31
+ ```python
32
+ import numpy as np
33
+ from protlabel import eat, Lookup
34
+
35
+ preds = eat(
36
+ query_emb=np.random.rand(3, 1024).astype("float32"),
37
+ query_ids=["Q1", "Q2", "Q3"],
38
+ ref_emb=np.random.rand(100, 1024).astype("float32"),
39
+ ref_ids=[f"R{i}" for i in range(100)],
40
+ ref_labels=["toxin", "enzyme"] * 50,
41
+ k=1,
42
+ metric="cosine", # or "euclidean" (the goPredSim default)
43
+ )
44
+ for p in preds:
45
+ print(p.query_id, p.label, round(p.reliability, 3))
46
+ ```
47
+
48
+ `Lookup` builds and serialises a reusable reference set (`.npz` sidecar) so the
49
+ reference matrix can be rebuilt on demand rather than shipped.
50
+
51
+ ## Method
52
+
53
+ Nearest-neighbour transfer in the *original* pLM embedding space (not a 2-D/3-D
54
+ projection), with the goPredSim reliability index — see Littmann et al.,
55
+ *Sci Rep* 2021 (Eq. 5) and Heinzinger et al., *NAR Genom Bioinform* 2022.
56
+
57
+ Distances are computed with an exact, chunked brute-force search (numpy BLAS GEMM
58
+ + `argpartition`); queries are processed in batches. No approximate-nearest-neighbour
59
+ index is needed at Swiss-Prot scale.
60
+
61
+ ## License
62
+
63
+ GPL-3.0 — part of the [ProtSpace](https://github.com/tsenoner/protspace) project.
@@ -0,0 +1,8 @@
1
+ protlabel/__init__.py,sha256=jPPzf3kdVoIBe7iH6ysIKevIQfoYKNcmr6soTZEqduk,726
2
+ protlabel/backends.py,sha256=bhLVnRtEjPD4yEbLU9vEv3GxTe95Sk5eNQGL3ICaFh8,9147
3
+ protlabel/lookup.py,sha256=XoFzeS5_lhHTZQ4BS7QqOQZx2mEfE2rjp_T8yy1hrrs,2689
4
+ protlabel/reliability.py,sha256=B58s27sjecxan2SLZ18GqD34-mvbks_iOwa_z_vSpQw,2118
5
+ protlabel/transfer.py,sha256=4dYGiS0iITZMtqqX1ryqyR8OHD87mQPwq-Zt-TK8ISg,3677
6
+ protlabel-4.6.0.dist-info/METADATA,sha256=Xa4P_x7DgocsIQ8CrGbvFaYBdMR2-_N8T0-8a7O9p7g,2179
7
+ protlabel-4.6.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
+ protlabel-4.6.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any