anydist
Approximate nearest-neighbor search on arbitrary data with arbitrary dissimilarity functions, using Hierarchical Navigable Small World graphs (HNSW; Malkov & Yashunin).
Unlike most HNSW libraries, which index numeric vectors under a fixed set of metrics, this one accepts any Python objects and any dissimilarity function you write — including non-metric ones. It also has the usual fast path: a set of built-in metrics over numeric vectors, computed in SIMD C kernels with no Python on the hot path, with parallel bulk insertion and parallel batch queries. Between the two there is a third door: hand it a C function pointer — from numba, ctypes, Cython or an extension module of your own — and your dissimilarity runs on that same GIL-free hot path (see C metrics).
The index stays live. You can add to it and delete from it after it is built, interleaved with queries, without a rebuild — which not every library offers: nmslib and annoy are batch indexes, where a point added after the build costs a full rebuild to become findable (see Living indexes).
Two implementations:
HNSWFast— the one you want: a Cython implementation over typed, array-backed data structures.HNSWis an alias for it, and a normal install builds it.HNSWPure— the pure-Python, dict-based reference implementation, no dependencies beyond the standard library. It exists to document the algorithm readably and to check the fast path against, not as a deployment option: with a built-in metric it is ~30x slower to build and ~40x slower to query (measured on 5k 20-dimensional vectors), and even with a Python callback — where the callback itself dominates — it is ~1.5x slower.
HNSW never falls back to HNSWPure. Without the compiled extension
there is no HNSW, and asking for it raises ImportError saying so — an
order of magnitude is not something to lose silently to a build that did not
happen. If you want the pure implementation, name it:
from anydist import HNSWPure as HNSW # deliberately, penalty and all
In a hurry? Quickstart is two code blocks, and the API reference documents every constructor argument and method in one place — go straight there if you would rather read signatures than prose.
This code has been developed together with fishdbc, an incremental density-based clustering algorithm that leverages this library for scalability and allowing arbitrary dissimilarity functions.
Installation
pip install anydist
or, from a checkout of this repository:
pip install .
Windows is not supported: MSVC, the compiler Windows Pythons are built with, lacks the atomic builtins the parallel code needs. Under WSL, the Linux wheels work as usual.
PyPI has prebuilt wheels for the common platforms. Anywhere else, and from a
checkout, pip compiles the Cython extension, so you need a C compiler with
OpenMP; on macOS, brew install libomp first (or point LIBOMP_PREFIX at
another libomp). That extension is what gives you HNSWFast. A failed build
shows up the first time you ask for HNSW, since there is no fallback to hide
it, but you can check directly:
import anydist
assert anydist.HNSWFast is not None
On x86-64 Linux, the portable build already selects AVX2 kernels at load time on CPUs that have them, so it is not leaving the obvious speedup on the table. For a further boost tied to the machine you build on (at the cost of a non-portable binary):
HNSW_NATIVE=1 pip install .
On macOS the package carries its own copy of the OpenMP runtime, libomp, and
a process must never load two: libomp aborts when it finds another copy of
itself already running. An extension of your own that uses OpenMP alongside
anydist should therefore build against this copy rather than bring one:
anydist.get_openmp_dir() returns its directory, and its docstring has the
compiler and linker flags. fishdbc does exactly this.
Quickstart
Arbitrary objects, your own dissimilarity function:
from anydist import HNSW
def jaccard(a, b):
return 1 - len(a & b) / len(a | b)
index = HNSW(jaccard) # the dissimilarity is the 1st argument
index.update(my_sets) # insert them all
neighbors = index.search(query_set, k=10) # [(id, distance), ...], nearest first
Ids are insertion order: the id in a result is the position of that element
in the sequence you inserted. The index is a read-only sequence of those
elements, so index[id] gives one back, len(index) counts them, and
iteration and slicing work.
Numeric vectors and a built-in metric — no Python callback on the hot path:
index = HNSW('euclidean') # vector length inferred from the first row
index.update(X) # X: (n, dim) array, inserted in parallel
I, D = index.search_batch(Q, k=10) # (n_queries, k) ids and distances, in parallel
With a built-in metric, update inserts across all cores; with a callback
metric it stays serial by default, since only you can say whether your
function is safe to run on several threads — pass num_threads=N to opt in.
add(elem) is still there for genuinely incremental use, where you interleave
inserts and queries.
index[id] works in both modes, but they store elements differently: a
callback metric keeps your objects, while a built-in metric keeps rows of a
typed matrix and hands you back a copy of one — rounded to float32, since that
is the default storage there (see float32).
Multithreading
update and search_batch both take num_threads: None (the default)
picks for you, a positive value is a thread count, and -1 (or any value
≤ 0) means one thread per CPU.
The rule is that anything we wrote defaults to every core, anything you wrote
defaults to one, because only you can say whether your dissimilarity function
d — the callable you passed as metric — is thread-safe. What you get once
you opt in depends on the metric, because the GIL does:
- Built-in metric — ours, and stateless, so the kernels are
nogiland the threads are real on any interpreter build. Measured on 12 CPUs: 5.5x build / 5.0x query at 50k x 32, and 4.3x / 3.7x at Fashion-MNIST's 60k x 784. Don't read those as an efficiency: the machine's cores are not all the same speed, so what a given box can offer is its own question. - C metric (a function pointer you supplied) — the call is
nogiltoo, so opting in is the only way a user-written distance gets real threads on a stock interpreter: measured 4.3x at one-per-core (n=4k, dim=16, a deliberately slowjit_metric). It still defaults to serial, becausenogilbuys the ability to run yourdin parallel, not the safety of doing so — mutable state reached throughuser_data, a memo table, a counter, all race, with no GIL here to accidentally serialise them. - Callback metric — the callback runs under a thread state re-attached per distance. On a free-threaded interpreter that is real parallelism; on a stock GIL build it pays off only when each distance holds the GIL briefly and releases it for a long time — measured 3.0x at 2 threads for a 40k-dimensional NumPy distance, but slower than serial at 128 dimensions, where contention and the thread-state attach cost more than the op. Measure before believing.
These figures are indicative, not a spec — see About these numbers for the method and its resolution limits.
index.update(X) # built-in metric: all cores
index.update(objs) # your metric: one thread
index.update(objs, num_threads=-1) # opt in: "my d is thread-safe"
index.update(objs, num_threads=4) # opt in, four threads
I, D = index.search_batch(Q, num_threads=1) # force a single-threaded query
search_batch is worth calling for a callback metric even serially: it is the
same graph walk as a search loop, but it hands back the whole batch as two
(n_queries, k) arrays — neighbor ids and their distances — instead of a list
of lists.
chunksize, the constructor's or a call's own, interacts with this: workers take chunks of
consecutive ids, which is what keeps parallel insertion correct when the
input is order-local (sorted, time-series, grouped records) — two near
neighbors inserted at the same instant never see each other, and the edge
between them is lost. The default adapts to the batch; raise it if your input
is strongly ordered.
Writing a callback metric that scales
On a free-threaded interpreter there is one rule, and it is not about how you write the function — it is about what the function reads:
A parallel metric must not read a shared mutable object on every call. Reading
self.somethingor a closed-over variable costs 3-4x at eight threads. Read parameters from module globals or default arguments instead.
CPython gives some objects immortal or deferred reference counts, so touching
them from many threads is free: None, small ints, literals, module-level
functions, and all classes (including ones built at runtime by type(),
namedtuple, or a class statement inside a function). Everything else is
ordinary, and an object that every thread increfs on every distance call turns
one cache line into a contention point. Measured on the same laptop (n=10 000
tuples of 8 floats, ef=25, 8 threads, free-threaded 3.13):
| metric written as | speedup at 8 threads |
|---|---|
module-level def, reads only its arguments |
3.53x |
| closure that reads its captured variable | 0.82x — slower than serial |
| closure that ignores it | 3.08x |
bound method that reads self |
1.19x |
bound method that ignores self |
2.94x |
Note the pairs: same callable form, same arithmetic, only the read differs. The
form is irrelevant — functools.partial and lambdas are fine. So a metric with
parameters should carry them like this:
# slow in parallel: `self.p` and the closure cell are one shared object each
class Metric:
def __call__(self, a, b):
return sum((x - y) ** self.p for x, y in zip(a, b))
# all of these scale (3.3-3.5x measured):
P = 2
def metric(a, b): # module-level global
return sum((x - y) ** P for x, y in zip(a, b))
def make_metric(p):
def metric(a, b, p=p): # default-argument binding
return sum((x - y) ** p for x, y in zip(a, b))
return metric
Per-element objects are not a problem: your data points are thousands of distinct objects, so the traffic spreads. It is the single shared one that hurts.
If threading makes things slower, suspect numpy. There is a bug in numpy
(numpy#32298) where an array
that has been through pickle, copy.deepcopy, or a joblib cache ends up
sharing one small internal object that every thread has to synchronise on. The
array is otherwise identical, and single-threaded work is unaffected — but on a
free-threaded build the contention can turn a speedup into a slowdown.
Usually you need not care: update() detects and repairs it whenever you hand
it an array. The case it cannot reach is a list of rows you extracted yourself,
because then the library never sees the array. Repair it at the source:
from anydist import canonicalize_dtype
rows = list(canonicalize_dtype(X)) # X came from pickle / joblib / a cache
index.update(rows, num_threads=-1)
So: parallel inserts slower than serial, on data that came out of a cache, is
worth one try of the two lines above — see BUGS.md, which also
lists the other issue worth knowing about.
Built-in metrics
Passing a name instead of a callable computes the distance in a SIMD C kernel with no Python on the hot path. Available names (aliases in parentheses):
| Name | Distance | Notes |
|---|---|---|
euclidean (l2) |
sqrt(sum (x-y)^2) |
|
l2sqr (sqeuclidean) |
sum (x-y)^2 |
ranks identically to l2, skips the sqrt |
cosine (cosinesimil) |
1 - cos_sim |
normalizes for you; the value scipy and sklearn report |
angular (angulardist) |
arccos(cos_sim) / pi |
normalizes for you; ranks as cosine does, and obeys the triangle inequality |
dot |
1 - dot(x, y) |
normalizes nothing — cosine distance only if you pre-normalized |
negdotprod |
-dot(x, y) |
the same space as dot, shifted by 1 |
l1 (manhattan) |
sum |x-y| |
|
linf (chebyshev) |
max |x-y| |
The names follow nmslib's dense-vector spaces. dot and negdotprod differ by
an additive constant, and every decision the search makes is a comparison, so
they build identical graphs and return identical orders — choose between them
by the number you want back. dot is non-negative on unit-length rows, which
a downstream consumer of the weights may need; negdotprod is what code
ported from nmslib expects.
Cosine distance
Three names get you cosine-like behavior, and they differ in whether they normalize for you and in what number comes back. All three return the same neighbor lists on unit-length rows, so this is a choice about values and cost, not about quality.
Just want cosine distance? Use cosine. It divides by the length of each
of the two vectors itself, so your rows do not have to be unit-length, and it
returns 1 - cos_sim — the same value scipy, scikit-learn, nmslib
(cosinesimil) and hnswlib report.
index = HNSW('cosine')
index.update(X) # any lengths; normalizing is on us
index.search(q, k=10) # distances are 1 - cos(q, x), in [0, 2]
Need the triangle inequality? Use angular. Same normalization, but it
returns the angle between the vectors scaled to [0, 1] instead of
1 - cos_sim. Since a smaller angle is exactly a larger cosine, it ranks
candidates identically to cosine; unlike cosine, it is a true metric on
directions. The extra arccos is not free — it is the only libm call in any
built-in kernel, and it costs roughly a third of query time at 50 dimensions —
so reach for angular when something downstream needs metric behavior, not by
default.
Already normalized, and want the cheapest option? Use dot. It normalizes
nothing, so on unit rows 1 - dot(x, y) is cosine distance, computed with a
single accumulator instead of three.
import numpy as np
from anydist import HNSW
X = np.asarray(X, dtype=np.float64)
X /= np.linalg.norm(X, axis=1, keepdims=True) # unit rows (drop zero rows first)
index = HNSW('dot')
index.update(X)
q = q / np.linalg.norm(q) # the query too: dot normalizes nothing
index.search(q, k=10)
A zero row has no direction, so cosine and angular both report distance 0
for it rather than a NaN.
C metrics: numba, ctypes, an extension module
A built-in metric is fast because it never enters Python; a callback is flexible because it is Python. This section is for when you want both — and it asks more of you than the two options above, so skip it unless a Python callback has turned out to be your bottleneck.
Give HNSW a C function pointer instead of a Python callable, and your
distance runs with no GIL and no Python on the hot path, exactly where a
built-in metric runs. Parallel insertion and parallel queries then become
possible on a stock interpreter — but you have to ask for them:
index.update(X, num_threads=-1) # -1 = one thread per core
I, D = index.search_batch(Q, k=10, num_threads=-1)
A C metric still defaults to one thread, like a Python callback and unlike
a built-in metric. nogil buys the ability to run your distance in
parallel, not the safety of doing so: state reached through user_data, a
memo table or a counter all race, and there is no GIL here to serialise them
by accident. Only you can say whether your d is thread-safe, so the opt-in
is yours to make.
The signature is fixed, and it is the one scipy's LowLevelCallable users will
recognise:
double f(const double *a, const double *b, Py_ssize_t n, void *user_data);
Writing one in Python: jit_metric
Nobody wants to spell that signature in numba by hand, so the decorator generates it from the two-argument form:
from anydist import HNSW, jit_metric
@jit_metric # needs numba installed
def manhattan(a, b): # a, b: float64 arrays of length dim
s = 0.0
for i in range(a.shape[0]):
s += abs(a[i] - b[i])
return s
index = HNSW(manhattan)
index.update(X, num_threads=-1) # opt in: "my d is thread-safe"
I, D = index.search_batch(Q, k=10, num_threads=-1)
It returns a numba.cfunc, which HNSW recognises by its .address. The body
must compile in nopython mode — loops over the two arrays are the idiom,
arbitrary Python is not; if you need arbitrary Python, pass the plain function
and pay the GIL per distance. @jit_metric(cache=True) persists the compiled
kernel across processes, which matters for a benchmark harness that runs one
cell per process; it requires the function to live in an importable file.
What the pointer buys, measured on the same 12 CPUs (stock-GIL CPython 3.14, n=5000, dim=32, m=16, ef=100, k=10) with the same distance written both ways — the callback being the obvious numpy one-liner:
| metric | threads | build | query |
|---|---|---|---|
| Python callback | 1 | 5.34 s | 1.63 ms/q |
C metric (jit_metric) |
1 | 0.20 s | 0.032 ms/q |
| C metric | 12 | 0.044 s | 0.008 ms/q |
The 12-thread row is opt-in (num_threads=-1) — a C metric is serial by
default, see Multithreading — and it is safe here because
this metric is a pure function of its two arguments.
26x on build and 50x on query before threading, 4.6x and 4.3x more from it. The serial ratio is the part that moves with your metric: a callback whose body does real work narrows it, one written in pure Python widens it. The threading factor is the part a callback cannot have on a stock build at all.
Pointing it at a library you already have
The pointer is duck-typed, so nothing here is imported unless you use it. Any of these shapes is accepted:
- scipy
LowLevelCallable—.functionis aPyCapsule,.user_datacarries parameters. scipy already normalises numba, ctypes and Cython into it, so this one shape covers all three. - numba
cfunc— has.address. - ctypes function pointer — how an existing
.soreaches Python. - anything else exposing
.addressor a.functioncapsule (plus an optional.user_data), including aPyCapsulefrom your own extension module.
import ctypes
dbl = ctypes.POINTER(ctypes.c_double)
lib = ctypes.CDLL("./libdtw.so")
lib.dtw_metric.restype = ctypes.c_double
lib.dtw_metric.argtypes = [dbl, dbl, ctypes.c_ssize_t, ctypes.c_void_p]
index = HNSW(lib.dtw_metric) # DTW, in C, across all cores
Parameters go through user_data, which is read from every one of those
shapes: keep them in a struct (or a single ctypes cell) that outlives the
index, and expose its address as .user_data next to the pointer.
jit_metric does not expose user_data — a closure over module-level
constants covers the parameterised case with less ceremony, and a hand-built
cfunc or a LowLevelCallable is there when you want the pointer itself.
One thing to know before you take this door: getting the signature wrong crashes the process instead of raising an exception. How much protection you get depends on what you hand over:
All three forms declare their signature somewhere, and all three are checked
against double (const double *, const double *, Py_ssize_t, void *), raising
a CSignatureWarning if they disagree:
- A capsule carries it in its name — a
scipy.LowLevelCallable, or anything else whose.functionis a named PyCapsule. Spelling is not the test:const, spacing, parameter names andPy_ssize_t/ssize_t/npy_intpall pass; the types have to line up. - A ctypes function pointer carries it as
_restype_/_argtypes_, and a numba cfunc carries the same on its.ctypesview. Either pointer spelling is accepted for the vectors —POINTER(c_double)or a barec_void_p.jit_metricbuilds the signature for you, so that path cannot disagree in the first place.
What is genuinely unchecked is a bare address: an unnamed capsule, or a pointer whose declaration you never wrote down. There the crash contract stands in full.
The check warns rather than refuses, because it recognises a list of spellings rather than parsing C, and an unfamiliar typedef must not be able to block a pointer that is in fact correct. If that is your situation:
warnings.simplefilter("ignore", anydist.CSignatureWarning)
and please open an issue with the spelling, so the list can grow.
One limit is worth knowing before you write the metric rather than after:
your vectors must all be the same length. n is a single dimension,
inferred from the first row and passed to every call. Variable-length data
stays on the callback path, which imposes nothing on what your objects are.
Otherwise a C metric follows the built-in-metric path rather than the callback
one, which the API reference covers argument by argument. One
of those defaults is worth a second look, because it is the only place that
path assumes something about your function rather than ours: select is
chosen from the vector length, on the reasoning that a short vector is cheap
to re-measure. That holds for the built-in kernels, whose work is linear in
the length. If yours is expensive at any length — an alignment, a search, a
simulation — pass select='edges' and it will read stored weights instead
of calling you O(m²) times per row.
API
Constructor
HNSW(metric, m=5, ef=200, m0=None, level_mult=None,
level_strategy='random', keep_pruned=False, select=None, cache=None,
float32=None, keep_workspace=False, chunksize=None)
-
metric— the dissimilarity, in one of three forms (it need not satisfy the metric axioms in any of them — the name follows the scikit-learn/hdbscan convention, not a metricity requirement): a callabled(x, y)over arbitrary data; a built-in name (see above) over numeric vectors, whose length is inferred from the first one inserted; or a C function pointer over such vectors (numbacfunc,scipy.LowLevelCallable, ctypes pointer, capsule), which runs GIL-free like a built-in one — see C metrics. A pointer is recognised before callability, so an object that is both takes the fast door. -
m,ef,m0,level_mult— the paper's parameters; see Malkov & Yashunin. -
level_strategy— how insertion levels are assigned.'random'is the paper's strategy and the best choice for approximate nearest-neighbor search;'balanced'is a deterministic variant (an element is promoted when its neighborhood is full and no neighbor sits above it) that yields a more balanced structure. -
keep_pruned— the paper's optionalkeepPrunedConnections(Alg. 4, lines 15–17). Off by default. -
select— the neighbor-selection rule.'edges'answers the paper's pruning heuristic (Alg. 4) from edges the graph already has, spending zero extra distance evaluations;'distances'is the heuristic as written, recomputing the candidate-to-result distances;'naive'just takes themnearest (Alg. 3).HNSWFastdefaults to'edges'for a callback metric, and for a built-in one decides at the first vector:'distances'whiledim <= 4*m(recomputing a short vector beats fetching a stored weight),'edges'above that — a C metric follows that same first-vector rule.HNSWPurealways defaults to'edges', since every distance there is a Python call.That first-vector rule takes vector length as a stand-in for what a distance costs, which is true of the built-in kernels. A C metric that is expensive out of proportion to its vector length should be given
select='edges'explicitly — otherwise short rows get'distances', which evaluates your function O(m²) times per row instead of reading stored weights. -
cache— reuse, within a single insert or query, distances computed at a previous level of the same descent. Defaults on for a callback metric (13% faster builds, measured), off for a built-in one — and off for a C metric, which is cheap for the same reason — where the check costs about what it saves. -
float32— store built-in-metric vectors as float32. Defaults on for a built-in metric, as hnswlib and faiss do: it halves both the vector store and the bandwidth of a query's dominant read. It does round the data, so passFalsewhen the exact float64 distances are the point.Trueis rejected for the other two metric forms rather than quietly overridden: a callback metric keeps Python objects and has no typed store to narrow, and a C metric reads its rows asconst double *, so a float32 store would be reinterpreted rather than converted. Both leave the vectors as float64, andindex[i]hands back a copy of the stored row either way. -
keep_workspace— hold the threads' working memory between calls instead of allocating it fresh each time. Only affectsupdate()andsearch_batch()with more than one thread.Each worker needs a set of buffers sized by the number of nodes in the index, so every threaded call pays an allocation proportional to the index — not to the work you asked for. One big
update()never notices. A loop of small ones pays it every time, and it grows as the index does. At 100k nodes and four threads:fresh each call kept update()in chunks of 2004.01 ms per chunk 2.46 ms search_batch()of 64 rows2.11 ms 0.37 ms Off by default because it is a trade, not a free win: the buffers stay allocated —
threads × nodes × 32 bytes, or 44 withcache=True, so 370 to 500 MB at twelve threads and a million nodes — until you callrelease_workspace(). Peak memory is the same either way; what changes is the floor between calls. Turn it on if you build or query in a stream of small batches, leave it off otherwise, and callrelease_workspace()when that phase is over.Results are identical either way, and the setting survives pickling while the buffers themselves do not.
-
chunksize— how many consecutive ids each worker takes in a threadedupdate()that does not pass its own; seeupdatebelow for what the chunk guards.None, the default, adapts it to each batch.HNSWPureaccepts and keeps it, with no parallel insertion to schedule.
Methods
-
update(X, num_threads=None, ef=None, chunksize=None)— insert many elements.Xis a numeric array for a built-in or C metric — anything array-like will do, and it is converted to C-contiguous float64, so integer rows, float32 rows and lists of lists are all accepted and all stored as float64 — or any sequence of objects for a callback metric. Parallel by default for a built-in metric, serial by default for the two you supply (see Multithreading).chunksizesets how many consecutive ids each worker takes: the chunk is what keeps parallel insertion correct on order-local input (sorted, time-series, grouped), where two near neighbors inserted concurrently would never see each other.Nonetakes the constructor'schunksize, and if that isNonetoo the chunk adapts to the batch; set it, on the index or per call, if you know your input is strongly order-local. -
add(elem, ef=None)— insert one element, returning nothing; its id is the previouslen(index). -
search(q, k=None, ef=None)— the (approximate)kclosest elements toqas(id, distance)pairs, sorted by distance.k=Nonereturns allefcandidates found. Soft-deleted nodes are excluded (see below). -
search_batch(Q, k=None, ef=None, num_threads=None)— answer many queries at once, optionally in parallel.Qis(n_queries, dim)for a built-in or C metric, or a sequence of objects for a callback one; returns(indices, distances)of shape(n_queries, k)—kdefaulting toef— padded with-1/infwhere fewer thankneighbors were found. Both classes have it, with the same rows, widths and padding, soI, D = index.search_batch(Q, k)reads the same either way.HNSWFastreturns numpy arrays and can use threads;HNSWPurereturns lists of lists, having no numpy, and loops oversearch, acceptingnum_threadsfor signature parity and ignoring it as itsupdatedoes. -
index[i],len(index), iteration, slicing — the index is a read-only sequence of the elements you inserted, in id order (negative ids count from the end; a built-in or C metric returns copies of the stored rows). Note this is not whatindex[i]meant in this package's predecessor, where it yielded nodei's neighbors;graphsis where those live now. -
mark_deleted(i)/unmark_deleted(i)— soft deletion (see below);index.deleted_countreports how many are currently marked. -
add_recording(elem, ef=None, harvest_cap=0)— insert one element and return every distance computed during the insertion as three parallel sequences(i, j, dist), whereiis always the new id — numpy arrays fromHNSWFast, and fromHNSWPurethe standard library'sarray.array(typecodes'i','i','d'), as compact at 16 bytes a triple. A plainaddrecords nothing and pays no overhead. -
update_recording(X, ..., harvest_cap=0)— the batch form, returning the same three sequences for the whole insertion. It isadd_recordingin a loop, and given the same insertion levels the two harvest the same triples. Note thatHNSWFast's comes back in per-worker order rather than insertion order, so sort byiif you need it.harvest_capbounds what comes back, identically on both:0is off, and a positiveNkeeps only each inserted node'sNshortest triples. It does not change the index — the same distances are computed and the same graph is built either way, so it trades recorded detail for the downstream cost of consuming it. Selection is by distance alone, which can keep the wrong bridge when a nearer cluster fills the quota first; leave it off for streaming workloads with concept drift, where insertion order cannot shuffle that away. Any cap>= 1keeps every node attached, since a node is only ever matched against already-inserted ones. -
freeze()— give up inserting in exchange for the memory only insertion needed (the stored edge weights:n*m0*8bytes at the base layer, plus the smaller per-node blocks above it). There is no way back, as with afrozenset. Underselect='distances', which stores no weights to begin with, it only installs the guard. ReturnsNone: it freezes the index you call it on rather than handing back a frozen copy. -
release_workspace()— free the threads' working memory thatkeep_workspace=Trueholds on to. Safe to call at any time, including when nothing is held; the next threaded call simply allocates again. Present on both classes, so code holding either can call it without asking which. -
graphs— the adjacency as a list of{node: {neighbor: dist}}, one per level, for inspection.
Living indexes
An index you can keep changing is not universal among ANN libraries. Adding 2,000 vectors one at a time to a 20k-node index, with a k=10 query every 100 inserts (dim=32, m=16, serial):
| library | insert after build | μs/insert | μs/delete |
|---|---|---|---|
| this library | yes | 61 | mark_deleted, 0.1 |
| hnswlib | yes | 53 | yes, 0.1 |
| usearch | yes | 73 | yes, 0.6 |
faiss (IndexHNSWFlat) |
yes | 129 | none |
nmslib and annoy are not in the table because they are batch indexes and say so — nmslib: "only static data sets are supported"; annoy: "after calling build, no more items can be added". A point added later is kept, but the structure that answers queries was built once and does not know about it, so getting it in means rebuilding the whole index. That is a different operation from an incremental insert, not a slower one, and there is nothing here to compare it against.
Deletion
mark_deleted(i) drops node i from results; unmark_deleted(i) puts it
back, and deleted_count says how many are out. Deletion is soft, as in
hnswlib: the node stops appearing in results but stays a routing waypoint, so
the graph never gets disconnected and recall for everything else is untouched.
The cost is that deleted nodes keep their memory and still cost distance
computations while routing, so an index that is mostly deleted wants a rebuild.
index.mark_deleted(7)
ids, dists = index.search_batch(Q, k=10) # 7 will not appear
The exclusion happens inside the search, not over its output: deleted nodes
are still traversed, so what comes back is the true nearest live
neighbors — not the unfiltered answer with rows struck out (measured >0.99
recall against brute force over the live set, with half the index deleted).
You get min(k, ef, live) results: the search fills its pool with ef live
candidates before it is willing to stop, so a short list means there really are
that few live nodes, not that the search gave up. (k > ef truncates to ef,
deletions or not — that is the pool size, so keep ef comfortably above k as
usual.)
Persistence
anydist.save and anydist.load take a path or a binary file object:
import anydist
anydist.save(index, "index.pkl")
index = anydist.load("index.pkl") # same graph, same results
There is no saves()/loads() pair returning bytes — pass an io.BytesIO(),
as with numpy.save. Plain pickle (or dill) still works on an index too;
save/load exist for the one thing pickle cannot do on its own.
The metric is the only part that may not travel. A built-in name or a module-level function pickles; a lambda, a closure or a C function pointer does not, and an address would be meaningless in the loading process anyway. Everything expensive — the vectors, the adjacency, the deletions — always serialises, so rebuilding would be a poor answer. Instead, save without the metric and give it back on load:
anydist.save(index, "index.pkl", include_metric=False)
index = anydist.load("index.pkl", metric=my_cfunc)
Including the metric is the default and raises if it cannot be done, so
save never quietly writes a file that load(f) cannot open. You get a
self-contained file or an exception naming the way out — never a surprise in
the process that loads it.
Speed. Saving and loading are close to memory-bandwidth bound, so an index round-trips far faster than it builds. Measured on the machine described under About these numbers, writing to a real file in a temporary directory — as every library in the comparison does, so the figures are like-for-like:
| index | on disk | save | load |
|---|---|---|---|
| Fashion-MNIST, 60k x 784 | 213 MB | 0.18 s | 0.17 s |
| Gaussian, 100k x 128 | 92 MB | 0.09 s | 0.06 s |
| Gaussian, 50k x 32 | 14 MB | 0.02 s | 0.01 s |
Roughly 1.2 GB/s each way — the same order as a memory copy, and two orders of
magnitude cheaper than rebuilding the index, which is the comparison that
matters. (Nothing is fsynced, so this is the cost of handing the bytes to
the OS rather than of a durable write.)
hnswlib is about twice as quick at both, and its file is ~8% smaller
(197 MB against 213 MB on Fashion-MNIST): it writes a fixed-size record per
element straight out of its own arena, where this library pickles, and it
stores no edge weights. Ours keeps them whenever select does — under
select='distances', which stores none, the sizes match. Both are far below
the cost of building, so the difference is unlikely to be what you optimise.
Only load files you trust. save/load are built on pickle, and
unpickling any file runs code that the file chooses. That is pickle's design,
not something this library adds or can screen for: load sees the constructor
arguments only after the interpreter has already executed whatever the stream
asked for. Treat an index file exactly as you would a .py you were about to
run — the format is not a safe interchange format for data from elsewhere.
load is strict in both directions: it refuses a metric= for a file that
already has one (that would replace a metric known to be right with one it
cannot check), and refuses a metric of the wrong kind — the three forms store
their elements differently, so a callback cannot be attached to a graph built
for a built-in name. What it cannot check is that you passed the same
distance; supply a different one and the graph answers for a metric it was not
built for. That much is yours to get right.
Under the hood this is a plain pickle using
persistent_id,
the stdlib's own mechanism for an object the loader must re-supply — there is
no bespoke format here to version. The pickle carries the vectors (or the
elements), the adjacency, the construction parameters and any soft deletions,
so it is self-contained but not small: expect roughly the in-memory size of the
index. There is no separate index-only/mmap format.
The two classes
They take the same constructor arguments and answer the same calls, and both
accept all three metric forms. What only HNSWFast has is parallelism
(update across cores, search_batch across queries), numpy arrays out of the
batch API, and the float32 vector store.
HNSWPure accepts num_threads and inserts serially, and rejects
float32=True having no typed store to narrow. It accepts
keep_workspace=True and ignores it, rather than rejecting it: float32
changes the numbers, so ignoring a request for it would mislead about results,
while keep_workspace only decides when memory is allocated and a class with
no threads runs the same program either way. It does call C metrics, through
ctypes, marshalling both operands on every distance. It is there so the
reference implementation can run the metric you
are actually indexing with — but that marshalling is a fixed per-call cost, so
whether it beats writing the distance in Python depends on how much work the
distance itself does. Against the plain Python loop a jit_metric wraps, the
pointer measured ~2.8x slower at dim=8 and ~5x faster at dim=64. If you
just want a working index at low dimension, that original two-argument function
is still reachable on .py_func_2arg.
The two produce equivalent results only distributionally (ties are broken differently), so compare them via k-NN recall rather than exact neighbor lists.
Performance
On the dense-vector case this is in the same class as the established C++ libraries — which is the point, since that case is the one they optimize.
What the generality is and is not. It is not that nobody else indexes
non-vector data: nmslib ships jaccard_sparse, leven and others, and is a
real competitor on anything in its catalogue. The difference is that a
catalogue is fixed at compile time and a Python callable is not. If your
dissimilarity is in nmslib's list, use nmslib and expect it to win; if it is a
business rule, a weighted ensemble, or anything you would otherwise have to
write C++ for, this library will run it — and, with jit_metric, run it at a
speed the section below quantifies.
Against the other libraries
50k Gaussian vectors in 32 dimensions, m=16, efConstruction=200, k=10, 12 threads. Each figure is a ratio to this library, formed within a repetition and shown with its range over three of them. A ratio whose range spans 1.0 is not a difference; the resolution limit here is 1.06x on build and 1.07x on query, measured by running the identical build twice as a control arm.
Both columns are times, relative to this library, so in both of them lower is faster and 1.00x is us:
| library | build time | query time | recall@ef160 |
|---|---|---|---|
| this library | 1.00x | 1.00x | .976 |
| hnswlib 0.8.0 | 1.03x [0.93-1.10] | 0.95x [0.94-1.06] | .980 |
faiss IndexHNSWFlat |
1.06x [1.00-1.07] | 0.89x [0.88-0.93] | .979 |
| nmslib 2.1.2 (AVX2) | 1.44x [1.42-1.47] | 1.09x [1.06-1.12] | .980 |
| usearch | 1.36x [1.26-1.41] | 1.54x [1.49-1.54] | .978 |
| annoy (trees, not HNSW) | 0.17x [0.16-0.18] | 100x | 1.000 |
hnswlib is indistinguishable from this library on both, which is the
result that matters: the reference implementation of the algorithm, and we are
inside the noise of it. faiss queries ~11% faster. We build ~1.4x faster than
both nmslib and usearch, and query ~1.5x faster than usearch; nmslib's query
time is within a whisker of ours. annoy is a tree index included for
scale, and is the only arm whose parameters were not swept the way the others'
ef was.
Recall is identical to three decimals across every HNSW arm at matched ef,
which is the check that the comparison is like-for-like: the same graph
quality, differing only in how fast it is reached.
Two caveats. nmslib's PyPI wheel is built without SIMD, and warns so on
import; the row above is from a source build (pip install --no-binary :all: --no-build-isolation nmslib — plain --no-binary fails on Python 3.14). And
the larger datasets we measured, at 100k x 128 and Fashion-MNIST, had control
spreads of 2.0x to 5.6x on this machine, so they are omitted rather than
reported: nothing there was resolvable.
An arbitrary metric, measured
kosarak from ann-benchmarks: 74,962 sets of ints, mean size 55.6, Jaccard,
recall@10 against the published ground truth, M=16 efC=100. The comparison is
nmslib's jaccard_sparse, built from source so it has AVX2 — its purpose-built
C++ space against a Python function of ours.
Three ways to give this library the same distance. Every ratio below is formed inside one run, from the minimum of seven alternating repetitions, with a control arm — the same index timed a second time — so the resolution is measured rather than hoped for. The two columns of ratios span ef=100 and ef=400, that is recall 0.90 and 0.95:
| how | against @jit_metric |
against nmslib | resolution |
|---|---|---|---|
| plain Python callable | 7.5–8.1x slower | — | 1.02x |
@jit_metric, 1 thread (pinned) |
— | 1.18–1.20x slower | 1.002x |
@jit_metric, 12 threads |
— | 1.24–1.26x slower | 1.02–1.04x |
So compiling the callable is worth about 8x, and once compiled the gap to a purpose-built C++ space is about 1.2x, whether serial or threaded. That is what writing your distance in Python instead of C++ costs.
Each arm queries through whichever API is faster for it: search_batch for
the compiled metric, a search loop for the Python callable, which serially
on this GIL build beat batching by ~6%. Matching them would widen the 8x
slightly, not narrow it.
For scale rather than for comparison, the serial pinned run measured 0.61
ms/query for @jit_metric against nmslib's 0.50 at recall 0.90, and 0.095
against 0.075 at twelve threads. Absolute figures move between runs on this
machine — the same compiled index measured 0.61 in one run and 0.73 in
another, after a long build had heated the box — which is exactly why the
ratios above are formed within a run and the absolutes are not compared
across them.
The serial rows are pinned to one core with taskset: this machine's cores
are not all the same speed, so an unpinned "1 thread" figure silently depends
on which one it landed on — measured 2.6x apart for the same code. See
About these numbers.
Variable-length objects reach a C metric through a fixed double* signature by
storing [offset, length] in the row and closing over one flat array, so the
vector store is 1.2 MB rather than the ~1.5 GB a padded matrix would need, and
the distance stays exact:
FLAT = np.concatenate(sorted_sets) # every set, back to back
@jit_metric
def jaccard(a, b): # each row is [offset, length]
ao, an = int(a[0]), int(a[1])
bo, bn = int(b[0]), int(b[1])
i = j = inter = 0
while i < an and j < bn: # sorted-merge intersection
x, y = FLAT[ao + i], FLAT[bo + j]
if x == y: inter += 1; i += 1; j += 1
elif x < y: i += 1
else: j += 1
return 1.0 - inter / (an + bn - inter)
When an index is worth building at all. On an expensive metric the answer depends on n, because brute force is linear and the graph is not. Jaccard over random sets, ef=400, serial:
| n | brute force | this library | speedup |
|---|---|---|---|
| 5,000 | 5.04 ms/q | 4.68 ms/q | 1.1x |
| 20,000 | 22.25 ms/q | 9.50 ms/q | 2.3x |
| 80,000 | 87.72 ms/q | 12.81 ms/q | 6.8x |
Below ~10k elements an index barely pays for itself; by ~100k it is most of
the cost. (Recall drifts 0.955 -> 0.825 across those rows at fixed ef, so
part of the last speedup is bought with quality.)
About these numbers
Every figure in this README comes from one throttling laptop, so the method matters more than usual. What was done, and what it means for how much weight a number can carry:
- A control arm. Every comparison includes the same build measured twice. The spread between those two runs is the resolution limit, and no effect smaller than it is claimed. This is not a formality: it caught a 10x "difference" that was two benchmark processes accidentally running at once, and a 3.8x one that was a metric interpolating across the steep part of a recall curve.
- Interleaved, paired repetitions. Arms alternate within a repetition and ratios are formed inside it, so drift in the machine is common-mode and cancels. Summarising each arm separately and dividing would not.
- One process per measurement, with the thread count pinned before any import and the observed value recorded, plus load average, CPU-seconds and timestamps on every row.
- Pinning, where it decides anything. The cores here are not all the same
speed, so an unpinned single-threaded figure depends on which one it landed
on — measured 2.6x apart for identical code. Serial comparisons use
taskset; thread-count speedups are reported as measured, without being turned into an efficiency against a core count that would not mean much.
What this buys is roughly 3-13% resolution single-threaded and 6-7% on the 50k x 32 dataset at twelve threads. On the larger datasets at twelve threads it buys nothing: identical builds differed by 2.0x to 5.6x, so those cells are reported as unresolvable rather than given numbers. Redoing all of this on a quiet, homogeneous machine would tighten every bound here, and is on the list.
Reproducing any of this. Everything above is produced by
benchmarks/ against the anydist you have installed —
python run.py then python analyze.py, with every comparison library
optional. The harness prints your machine's resolution before it prints any
comparison, so you can see which of its own numbers to believe.
Author
Matteo Dell'Amico — della@linux.it
License
BSD 3-clause; see the LICENSE file.
Metadata
Release files for anydist 1.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| anydist-1.0.2.tar.gz | 206.4 kB | Details |
Built distributions (wheels)
Total release size: 78.0 MB
Release files / anydist-1.0.2.tar.gz
| Download URL | anydist-1.0.2.tar.gz |
|---|---|
| Size | 206.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
43da18b0744674de75e835b03cb93285369ca9ff9eb90640e507d7064a1db6f7
|
|
BLAKE2b-256 checksum How to use checksums |
f0250388a94ed47740ec501cc020408b574d0ecf02229d84057d3479f7073bf6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp315-cp315t-musllinux_1_2_x86_64.whl
| Download URL | anydist-1.0.2-cp315-cp315t-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.15 CPython 3.15 free-threading Linux musl 1.2+ x86-64 |
|
SHA-256 checksum How to use checksums |
5e15837b8687c2bb001b2f3e3dee822eb13f8928ed3cea907414c9d8423d8359
|
|
BLAKE2b-256 checksum How to use checksums |
7380fa6374bdba41378df46d49a91a6276fc1af6515550b074cda9d0ff9ae406
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp315-cp315t-musllinux_1_2_aarch64.whl
| Download URL | anydist-1.0.2-cp315-cp315t-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.15 CPython 3.15 free-threading Linux musl 1.2+ ARM64 |
|
SHA-256 checksum How to use checksums |
cac628da2aad5b97989e44e19519d2a6f542d6db3c41d4fdf703d6980f4ceb18
|
|
BLAKE2b-256 checksum How to use checksums |
d25dc8d6ad5188aba19af4dc7a76396451de002d2023955650d5e58ec3675b11
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | anydist-1.0.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.15 CPython 3.15 free-threading Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
37a83d578cbcce8dfe7227aa06eb0bf1c2edeb5b27699ae9ea3fc986a213aef6
|
|
BLAKE2b-256 checksum How to use checksums |
aba63f3e7edbb40307388907a1ef1dcd1341f1992bac2d1c71fca8109595df01
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
| Download URL | anydist-1.0.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.15 CPython 3.15 free-threading Linux glibc 2.17+ ARM64 Linux glibc 2.28+ ARM64 |
|
SHA-256 checksum How to use checksums |
7d531f3331ac20f5013d9d9814f4f14ce62277a5e192c08c643324b409b8d761
|
|
BLAKE2b-256 checksum How to use checksums |
5206e2b109d04832fe34324d76addf063f953191e3c01e9dbc82c3c3c3778afe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp315-cp315t-macosx_15_0_arm64.whl
| Download URL | anydist-1.0.2-cp315-cp315t-macosx_15_0_arm64.whl |
|---|---|
| Size | 716.2 kB |
| Tags | CPython 3.15 CPython 3.15 free-threading macOS 15.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
60f6e5c78e71dc00bc980a979106eab3addd38e444110df99bf350496fddedc2
|
|
BLAKE2b-256 checksum How to use checksums |
541440ee25c209833ad7dea4be7ce2bfd37d7dcab4250b19d95134423d945c83
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp315-cp315-musllinux_1_2_x86_64.whl
| Download URL | anydist-1.0.2-cp315-cp315-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.15 Linux musl 1.2+ x86-64 |
|
SHA-256 checksum How to use checksums |
8f56de84c2128011eba0c89d1fc5d5e9cf5794829435ccd23d1ef56ce8414b46
|
|
BLAKE2b-256 checksum How to use checksums |
1ccf3ccf24f7f653ee1ce61961b7fada46e192f0b87d5c874724a0cec9ff270a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp315-cp315-musllinux_1_2_aarch64.whl
| Download URL | anydist-1.0.2-cp315-cp315-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.15 Linux musl 1.2+ ARM64 |
|
SHA-256 checksum How to use checksums |
748fb179c5444e9f66fff6a56214b09bd4e46b01a0e622ac814da8cefd00911d
|
|
BLAKE2b-256 checksum How to use checksums |
6e6f6cb7b7a9b54d1556a20c9a3ebc18eae012c59cf653dc68b36d767d488c44
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | anydist-1.0.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.15 Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
d8e77e6b827d86091b7afbc46b2e8b9f09643bf2e134819fb607fdc3814bcc8f
|
|
BLAKE2b-256 checksum How to use checksums |
b6b4ee9663546f4de21f38852d053f358a407080717eb68ca68dab604f9803f4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
| Download URL | anydist-1.0.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.15 Linux glibc 2.17+ ARM64 Linux glibc 2.28+ ARM64 |
|
SHA-256 checksum How to use checksums |
d04589184faad15fecfa41a52a73d70db48a8af794f5720626b7cf4a99ae2c92
|
|
BLAKE2b-256 checksum How to use checksums |
4cc154640b56947a17e41191be78ad684c41c1b3633995f889ecfcd5ee332098
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp315-cp315-macosx_15_0_arm64.whl
| Download URL | anydist-1.0.2-cp315-cp315-macosx_15_0_arm64.whl |
|---|---|
| Size | 697.6 kB |
| Tags | CPython 3.15 macOS 15.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
522bd39a520cdbdf5c584656e97d2ff7148ee41b942f45db49d1305b789efe5a
|
|
BLAKE2b-256 checksum How to use checksums |
e7f50ada16ade3ae616ba5e9196aa979f3dd81ecf52a5de4303027f94252cae1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl
| Download URL | anydist-1.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.14 CPython 3.14 free-threading Linux musl 1.2+ x86-64 |
|
SHA-256 checksum How to use checksums |
4b257e43ad7fe6beeaf0bcf512c12a71053f2d1c7129328ede33f6de5642f0b9
|
|
BLAKE2b-256 checksum How to use checksums |
8aafcabef8402119bedc9111e23107b8cf355a739d90bcbcb79e0bb2c0f79eb4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl
| Download URL | anydist-1.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.14 CPython 3.14 free-threading Linux musl 1.2+ ARM64 |
|
SHA-256 checksum How to use checksums |
800c9164834929d61e2fccdba3ba491cb611cb2dc8aebbc2601524b8607db640
|
|
BLAKE2b-256 checksum How to use checksums |
934e46e706669c9e6c7ecb48e7ac395cee7381747a90db414a81b6cf0e8d0191
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | anydist-1.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.14 CPython 3.14 free-threading Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
57593f75b151391aed6aeb36194b23e31a192c6d7560e3b66edfe897a07e8e77
|
|
BLAKE2b-256 checksum How to use checksums |
7f64737467d7d822f147e0a02767f7e6479b02ea6454acbd3b6a77135728cc79
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
| Download URL | anydist-1.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.14 CPython 3.14 free-threading Linux glibc 2.17+ ARM64 Linux glibc 2.28+ ARM64 |
|
SHA-256 checksum How to use checksums |
9b0239b569d253e4409a4facbf9026bc393b23f781ced8dec9585f5bd00012a7
|
|
BLAKE2b-256 checksum How to use checksums |
c3aaf6afa293346d6b04c7e8b019db8b8b1a76323415ca65ee6b31428ef729cb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp314-cp314t-macosx_15_0_arm64.whl
| Download URL | anydist-1.0.2-cp314-cp314t-macosx_15_0_arm64.whl |
|---|---|
| Size | 717.5 kB |
| Tags | CPython 3.14 CPython 3.14 free-threading macOS 15.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
717aed1e3546434bbeeaa7159b428f5694c5d45210259c1168ca8b26e908d159
|
|
BLAKE2b-256 checksum How to use checksums |
eca1fc2a2ab407541777d3c70960920480b8ae58fe59c1c1f47fcec36a894bfa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp314-cp314-musllinux_1_2_x86_64.whl
| Download URL | anydist-1.0.2-cp314-cp314-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.14 Linux musl 1.2+ x86-64 |
|
SHA-256 checksum How to use checksums |
cdf5b7134a1a5ec51ec252275ec90b30644e49dd0f1ce89f78bb6e15732b26a0
|
|
BLAKE2b-256 checksum How to use checksums |
90188cf4123d99103f9b97090914a857ee45916eaf302cbb3d137be3b6d360f0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp314-cp314-musllinux_1_2_aarch64.whl
| Download URL | anydist-1.0.2-cp314-cp314-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.14 Linux musl 1.2+ ARM64 |
|
SHA-256 checksum How to use checksums |
c0bb17ed3083b7631a4ed3844410224abd61f7da4ab78a3ce7f7176eb715e23a
|
|
BLAKE2b-256 checksum How to use checksums |
50c3500445e0944053e99c135767ea3df8bcd7092821cbad4b5ae7a5cd20ea21
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | anydist-1.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.14 Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
310336a9fa0367cdafcbfc1238ca47343b58cfe9dff88f56604e49ecdc215322
|
|
BLAKE2b-256 checksum How to use checksums |
362537b5181e36e1b7f40dd31105bfe8d67db046a06cb86d096f598bccadb1be
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
| Download URL | anydist-1.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.14 Linux glibc 2.17+ ARM64 Linux glibc 2.28+ ARM64 |
|
SHA-256 checksum How to use checksums |
4780b4fdddb264b174da2a7ebb8ec2d13e8d4bc5e6c4d29fbeb848a6b1e651e4
|
|
BLAKE2b-256 checksum How to use checksums |
226fef6ee0fb1f143831b1aecedd79f56cb01228357172bd7915d915a71ddc3e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp314-cp314-macosx_15_0_arm64.whl
| Download URL | anydist-1.0.2-cp314-cp314-macosx_15_0_arm64.whl |
|---|---|
| Size | 697.7 kB |
| Tags | CPython 3.14 macOS 15.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
2c0a22353b17becd6fb4f60db962944c7f9f3a4f83f9961d1f35cd002f2778d8
|
|
BLAKE2b-256 checksum How to use checksums |
84628d47e1226c3e63fe33868015a1e695d88fb4b8bbaf50c36f6dffc33a7a88
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp313-cp313-musllinux_1_2_x86_64.whl
| Download URL | anydist-1.0.2-cp313-cp313-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.13 Linux musl 1.2+ x86-64 |
|
SHA-256 checksum How to use checksums |
0c429980f6cb843da90bfb5ce8156bf6d8a719efc91435d8ad575dc0aeadff04
|
|
BLAKE2b-256 checksum How to use checksums |
8f9f0242e2ed76db83200c1963e1b358e5c00dec04c987db6cd2b3a93731fc6b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp313-cp313-musllinux_1_2_aarch64.whl
| Download URL | anydist-1.0.2-cp313-cp313-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.13 Linux musl 1.2+ ARM64 |
|
SHA-256 checksum How to use checksums |
cbd24cd0f86e32ebb59e9e457286ed5121d4c4682acabaca966a4639e6e9ec4b
|
|
BLAKE2b-256 checksum How to use checksums |
4a65dee9df6e7b57da2617cc82e85d641f4eb90d59fa78465bcbc4a9dfb45b2f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | anydist-1.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.13 Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
6dc464422592d2e0099c7aebd06442d1383094984c5f33dd01363cd067aa51e4
|
|
BLAKE2b-256 checksum How to use checksums |
1d5cb02a9df5f91e0eb1f0dd5aa9f7dd40026172e2da9eb1990832ea5331994f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
| Download URL | anydist-1.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.13 Linux glibc 2.17+ ARM64 Linux glibc 2.28+ ARM64 |
|
SHA-256 checksum How to use checksums |
19ae9c7689a6cd51c73af295c35645fea5852831210e6615796c1b87f6a24b7b
|
|
BLAKE2b-256 checksum How to use checksums |
b121dd8c25b21e974a372739f8ac43c394270cd557dfb41326496231433b342c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp313-cp313-macosx_15_0_arm64.whl
| Download URL | anydist-1.0.2-cp313-cp313-macosx_15_0_arm64.whl |
|---|---|
| Size | 696.0 kB |
| Tags | CPython 3.13 macOS 15.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
7bb6db131596ae353bc8309b56d159e6b7c0eff35aefe164a8b242faf1437273
|
|
BLAKE2b-256 checksum How to use checksums |
d1d399e5e8f0bf2fe7184cd80ebc5fe6b16091392c2bd4d5c570a2ce4b3d2f88
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp312-cp312-musllinux_1_2_x86_64.whl
| Download URL | anydist-1.0.2-cp312-cp312-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.12 Linux musl 1.2+ x86-64 |
|
SHA-256 checksum How to use checksums |
d774426a69762d6068b203aa2c30c7a96ab1fcaa117ad41281620f3955ef061e
|
|
BLAKE2b-256 checksum How to use checksums |
7c4a865ebcfcbaf5b0c2bef2c331e5ee1caa4c0fad3d1cf6e8f7d2bc5262ec3a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp312-cp312-musllinux_1_2_aarch64.whl
| Download URL | anydist-1.0.2-cp312-cp312-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.12 Linux musl 1.2+ ARM64 |
|
SHA-256 checksum How to use checksums |
456dd0abb86afd7c048632c8345e26b48df8d1845b719fd33694ca67e2b3c3b0
|
|
BLAKE2b-256 checksum How to use checksums |
f6ad5bec32fbaa789113cfed15ed7056fa2403d3d1982bf1758fb68edc24677f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | anydist-1.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.12 Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
987ecaa6ada391de3cf9542b86b95ace77c5b2bb7bce502fd2239d5994ad0911
|
|
BLAKE2b-256 checksum How to use checksums |
42d9261f68e30a3776ca54131cd5a95886ce0453a0ce7f11b0299558fa2f3e61
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
| Download URL | anydist-1.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.12 Linux glibc 2.17+ ARM64 Linux glibc 2.28+ ARM64 |
|
SHA-256 checksum How to use checksums |
1b1c56351740cf14118a7f98065ee896532c31d03e690f8c07583c337f62edf4
|
|
BLAKE2b-256 checksum How to use checksums |
15a98d8aa69f261edf94aa3da03eba40130486de9140fafa528f258ac455226f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp312-cp312-macosx_15_0_arm64.whl
| Download URL | anydist-1.0.2-cp312-cp312-macosx_15_0_arm64.whl |
|---|---|
| Size | 693.1 kB |
| Tags | CPython 3.12 macOS 15.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
8e9c463dac56b448f2958e73cc0f99426e6de1322341b5482aad767507c5cec6
|
|
BLAKE2b-256 checksum How to use checksums |
14edb8b10a2ae3af37c51efe5ee214967b8d9793cc447c030a77ffb1b6a1e114
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp311-cp311-musllinux_1_2_x86_64.whl
| Download URL | anydist-1.0.2-cp311-cp311-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.4 MB |
| Tags | CPython 3.11 Linux musl 1.2+ x86-64 |
|
SHA-256 checksum How to use checksums |
2d4131734bc08ee914c394bc83f33bf3005a0b6b56428db5a39dd6cbdd9dbc3a
|
|
BLAKE2b-256 checksum How to use checksums |
d2512bc0bec123446476294ea26b4c1e2e8b5c28b1fd4dbec68f4895c1427f5b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp311-cp311-musllinux_1_2_aarch64.whl
| Download URL | anydist-1.0.2-cp311-cp311-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.11 Linux musl 1.2+ ARM64 |
|
SHA-256 checksum How to use checksums |
074159f1ed2977572d2c3da6f260029ca3c159b8cda5b9132702ce4b6f304b08
|
|
BLAKE2b-256 checksum How to use checksums |
9d8b172b548a214060143818fb3224f02980e708ceaed564b93806d3c50f830a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | anydist-1.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.11 Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
b4ca5d55ddf02769d360e7fec8d389565b7136e47522a102db6893dae784fe50
|
|
BLAKE2b-256 checksum How to use checksums |
ee2300745775c33c6d4935e6662485662e48f06e73439c5e068927bf2e456dff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
| Download URL | anydist-1.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.11 Linux glibc 2.17+ ARM64 Linux glibc 2.28+ ARM64 |
|
SHA-256 checksum How to use checksums |
fd345c900c945ab81143c1d542849f4630d828c6014ed8c712c6ece01ea8639d
|
|
BLAKE2b-256 checksum How to use checksums |
e5501e9e6bbaf8e09985f66d182df1031580380da7fa7c4a745aeef7fb7869e5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp311-cp311-macosx_15_0_arm64.whl
| Download URL | anydist-1.0.2-cp311-cp311-macosx_15_0_arm64.whl |
|---|---|
| Size | 693.0 kB |
| Tags | CPython 3.11 macOS 15.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
bd8769fc85e1e16fcc782d56b0ff46e888e7f927fbe90a91ccf9dd7cf383e574
|
|
BLAKE2b-256 checksum How to use checksums |
bbac55298a22aa0455bd7a0f845306ebd2695ee3cc64529252018bac28c19aa3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp310-cp310-musllinux_1_2_x86_64.whl
| Download URL | anydist-1.0.2-cp310-cp310-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.10 Linux musl 1.2+ x86-64 |
|
SHA-256 checksum How to use checksums |
8d8ee08ae7e7a0e5238314fa48d70c9f17a74be51b945ea51b4b36626234824c
|
|
BLAKE2b-256 checksum How to use checksums |
4287efc6435c536bfb98f1bfbf09336874dfc3fecef175eef6d84b8658b174fc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp310-cp310-musllinux_1_2_aarch64.whl
| Download URL | anydist-1.0.2-cp310-cp310-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.10 Linux musl 1.2+ ARM64 |
|
SHA-256 checksum How to use checksums |
a911fcac20b2b2e403e4ab6d1a472e02a7cb593d1ad006416a4c166ede854837
|
|
BLAKE2b-256 checksum How to use checksums |
334177448b7e86776eb271b563053bb6791e9cec9c1f82ca48531ba9a6db74c8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | anydist-1.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
32ae1b652d73e6605f6fc91ed548959388dbd358d5365552deecef17114fa1f5
|
|
BLAKE2b-256 checksum How to use checksums |
cb186eb5a6f99d68e55357f1e9d806fb3b668b5a985e022ed44f72651936df5f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
| Download URL | anydist-1.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 Linux glibc 2.28+ ARM64 |
|
SHA-256 checksum How to use checksums |
7f49e585db74e1686fa7f6cb34b6fcfb03811eb76f7ed3c0ec10967466b2cb46
|
|
BLAKE2b-256 checksum How to use checksums |
86c992346295d30a718aec5f92480e4bb33b36b04fe223ce1107255dd00cd5b9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / anydist-1.0.2-cp310-cp310-macosx_15_0_arm64.whl
| Download URL | anydist-1.0.2-cp310-cp310-macosx_15_0_arm64.whl |
|---|---|
| Size | 697.0 kB |
| Tags | CPython 3.10 macOS 15.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
7a8d9924cb2a8a835c1dcd32b2ad060bd7efd086bb501ada3743c40a9e23a6c1
|
|
BLAKE2b-256 checksum How to use checksums |
1f75a144adc7cd7b4ee86b1e8897cfbde1344d36e8fe8131e6e290bc95d7dcef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|