A consumer-side query layer over the dsviper binding.
It adds nothing to the binding — native Python functional tools (generators, itertools,
comprehensions, sorted/set/dict) are already expressive over the Viper value model.
Unlike its Node twin @digitalsubstrate/dsviper-query,
this package needs no bridges. Viper values carry __eq__/__hash__/__lt__, so
set/dict/sorted operate on them directly — value-identity and ordering come for
free (there is no canonicalKey/hashKey, which the JS side must supply because Map/Set
key by identity). The whole layer is query(), the free verbs, a fluent Query, and a
Mongo-like match().
A value reaching a Python chain is either a native scalar (already decoded — int,
float, str, …) or a wrapped Viper value (containers, keys, ids). The stateless spine
works on both with stock tools and needs no library:
from dsviper_query import query
# query() is a lazy (key, document) generator; a comprehension chains over it directly.
top = [key for key, doc in query(state, attachment) if doc["score"] > 1000][:10]The stateful / relational verbs (distinct, group_by, join, union/intersect/
except_, to_dict) work directly on wrapped values — the JS identity wall does not
exist here. Python's set/dict call __hash__ and __eq__, so value-equal values
collapse (equal ValueMaps are one entry, not two) and width-variants stay distinct
(Int8(1) != Int64(1)). No primitive key:
from dsviper_query import distinct, group_by, order_by, join
unique = distinct(values) # keys on the value itself
by_kind = group_by(values, lambda v: v["kind"]) # dict[key, items]
ordered = order_by(values, lambda v: v["score"]) # native <, total order
enriched = join(left, right, lambda l: l["id"], lambda r: r["id"], lambda l, r: (l, r))order_by rides the runtime's trans-type total order (via native <), so even a
heterogeneous collection sorts deterministically, and None keys sort last.
On top of the verbs sits an ergonomic layer — pure sugar over the same tools, adding no power, only reach.
from_(iterable) — a fluent, lazy chain. Wrap any iterable (typically query(...)) and
chain the spine and the verbs; keys()/values() project the (key, doc) halves; terminals
(to_list, group_by, first, count, …) consume it. A Query is single-use — every
operation consumes the underlying lazy iterator, so a second operation on the same Query
raises Query already consumed rather than silently yielding an empty stream. Re-derive a
fresh Query from the source per chain.
from dsviper_query import from_, query
names = (from_(query(state, attachment))
.values() # drop the key half
.where({"dept": "eng", "age": {"$gte": 30}})
.order_by(lambda doc: doc["age"])
.map(lambda doc: doc["name"])
.take(5)
.to_list())match(spec) — a declarative, Mongo-like filter compiled to a (doc) -> bool predicate
(also accepted directly by .where). Implicit equality per field, operators
$eq $ne $gt $gte $lt $lte $in $nin $exists $regex $not $where, and combinators
$and $or $nor $not. Dotted paths resolve through nested dict access (or get_in/at for a
wrapped document; a missing path is simply unmatched, never raises). Equality is the value's
native == (type-aware), ordering its native < (total).
from dsviper_query import match
wanted = match({
"$or": [{"status": "active"}, {"score": {"$gt": 1000}}],
"addr.city": {"$in": ["paris", "london"]},
})
hits = from_(query(state, attachment)).values().where(wanted).to_list()match is strict — a malformed spec raises at compile time rather than silently matching
nothing: an unknown operator, an unknown top-level $-combinator, a condition mixing an
operator and a plain key (a typo'd $op), or an ill-typed operand ($and/$or/$nor want a
list, $in/$nin an iterable, $where a callable). An absent field never matches an order
comparison ($gt/$gte/$lt/$lte); use $exists to test presence.
The ambition is deliberately minimal: the operators above, the combinators, and a $where
escape hatch — no $options, $elemMatch, $size, or $type. Reach for a $where (or a
plain predicate) beyond that.
| Export | Kind | Notes |
|---|---|---|
query(state, attachment, *, key_pred=None, encoded=True) |
source | a lazy (key, document) generator over an immutable CommitState; nil-skipped; key_pred pushes down before get() |
rows(attachment_getting, attachment, …) |
source | the engine given an AttachmentGetting directly (the testable seam) |
from_(iterable) / Query |
fluent | chainable, lazy, single-use wrapper over the verbs |
match(spec, *, field=get_field) |
filter | compiles a Mongo-like spec to a (doc) -> bool predicate |
get_field(doc, path) |
accessor | dotted-path read (nested dict, or get_in/at for wrapped); MISSING on a miss |
compare_values(a, b) |
primitive | total order — native <, None last |
distinct, order_by, group_by, join, union, intersect, except_, to_dict |
verbs | stdlib one-liners on hashable/comparable values |
query accepts only an immutable CommitState — it is the sole type carrying the
immutability the lazy scan is sound over (its keys()/get() see one frozen snapshot at a
fixed commit_id). Mutable sources (CommitMutableState, Database) are rejected: their
store can change between keys() and get(). To query live state, snapshot first —
CommitState.state(commit_database, commit_id) — then query that.
- Python >= 3.10.
dsviper>= 1.2.18 (the trans-type total order thatorder_by/$gtride).
Experimental (0.x). The surface may change. The match/from_ DSL is pure sugar over the
verbs — native Python tools plus the value dunders already express the whole algebra; the DSL
only makes common shapes terser.
MIT