Declarative query filters for Python objects
Project description
cmdfilter
cmdfilter helps you turn a user query like title:Core pb:6.5 stock:yes into a reusable filter for Python objects.
It is built for the common backend task where you need to:
- accept compact text filters from CLI, admin panels, bots, or simple search boxes
- parse them once
- match the parsed command against one object or thousands of objects without reparsing
If you need a small Python library for declarative query parsing and repeatable filtering over dict-like data, this is exactly what cmdfilter is for.
cmdfilter is a small filtering library built around declarative layers.
The core idea is:
- Define reusable filter grades.
- Build selector layers on top of those grades.
- Parse a user query into an immutable
ParsedCommand. - Reuse that parsed command against one object or many objects.
The current API is reentrant by design:
Layeris immutable configuration.ParsedLayerstores parsed runtime arguments.Selector.parse(...)does not mutate the selector.Selector.match(...)andSelector.match_many(...)can safely reuse the same parsed command.- Runtime matching context is attached during
parse(...)and reused bymatch(...).
Installation
pip install cmdfilter
From a local checkout:
pip install .
For development:
pip install -e .[dev]
Quick Start
from cmdfilter import IS_ANY_FLOAT_LE, IS_ANY_STR_EQ, Selector, layer
items = [
{"title": "Core", "price": 6.0},
{"title": "Rift", "price": 7.0},
]
selector = Selector(
[
layer("title", IS_ANY_STR_EQ, ("title", "t"), keys="title"),
layer("price_below", IS_ANY_FLOAT_LE, ("price_below", "pb"), keys="price"),
]
)
parsed = selector.parse("t:Core pb:6.5")
print(parsed.as_dict())
# {(0, "title"): ("t", "Core"), (1, "price_below"): ("pb", "6.5")}
print(selector.match(parsed, items[0]))
# (True, None)
print(selector.match_many(parsed, items))
# [(0, None)]
This example shows the core workflow:
- declare filterable fields once
- parse a compact user command once
- reuse the parsed command for single-item and bulk matching
Lifecycle: parse -> match
The intended flow is:
parsed = selector.parse("pb:6.5")
result = selector.match(parsed, one_item)
results = selector.match_many(parsed, many_items)
This split is useful when:
- parsing is more expensive than matching
- the same query must be reused many times
- you want a stable parsed snapshot for tests or debugging
Building Custom Grades
You can define your own matching logic with grade(...).
The library also ships with built-in grades such as IS_ANY_STR_EQ, IS_ANY_FLOAT_LE, and IS_ALL_INT_GE.
These presets live in an internal module and are re-exported from the public cmdfilter package.
from functools import reduce
from operator import mul
from cmdfilter import Selector, grade, layer, to_float
volume_at_least = grade(
type_method=to_float,
eval_method=lambda values, arg: reduce(mul, values) / 1000,
match_method=lambda volume, arg: volume >= arg,
group_keys=True,
cost=10,
)
selector = Selector(
[
layer(
"volume_above",
volume_at_least,
("vol_above", "va"),
keys=("width", "depth", "height"),
)
]
)
Rules of thumb:
type_method(raw)converts raw query values to the target typearg_method(arg)is optional post-processing for parsed argumentseval_method(value, arg)is used whengroup_keys=Falseeval_method(values_tuple, arg)is used whengroup_keys=Truematch_method(eval_result)is used whengroup_keys=Falsematch_method(eval_result, arg)is used whengroup_keys=True- If needed,
arg_method(...),eval_method(...), andmatch_method(...)may additionally accept*, context=None grade(..., context="name")orgrade(..., context=("name1", "name2"))declares which context fields a grade uses
Example with runtime context:
from cmdfilter import Selector, grade, layer, to_str
case_aware_title = grade(
type_method=to_str,
eval_method=lambda value, arg, *, context=None: value if context and context.case else value.lower(),
match_method=lambda values, *, context=None: any(
item == ("RIFT" if context and context.case else "rift") for item in values
),
context="case",
)
selector = Selector([layer("title", case_aware_title, ("title", "t"), keys="title")])
parsed = selector.parse("t:RIFT", context={"case": False})
print(selector.match(parsed, {"title": "Rift"}))
# (True, ('rift',))
To change runtime behavior, parse again with a different context:
parsed_case_sensitive = selector.parse("t:RIFT", context={"case": True})
print(selector.match(parsed_case_sensitive, {"title": "Rift"}))
# (False, ('Rift',))
Layer Metadata
layer(...) accepts about= as a public convenience argument.
That value is stored on Layer.help_text and rendered by Selector.about().
from cmdfilter import IS_ANY_STR_EQ, Selector, layer
selector = Selector(
[
layer("title", IS_ANY_STR_EQ, ("title", "t"), keys="title", about="Exact title match"),
]
)
print(selector.layers[0].help_text)
# Exact title match
print(selector.about())
# [title|t]:<str> - Exact title match
Parsing Sources
Selector.parse(...) accepts either a string command or a dictionary.
String example:
parsed = selector.parse("Rift yellow pb:7.5")
Dictionary example:
parsed = selector.parse({"t": "Core", "pb": 6.5})
You can also attach runtime context during parsing:
parsed = selector.parse("t:RIFT", context={"case": False})
print(parsed.context.case)
# False
Unknown options are preserved in ParsedCommand.unparsed.
parsed = selector.parse("t:Core unknown:value")
print(parsed.unparsed)
# {"unknown": "value"}
Injecting Extra Filters
inject= lets you merge external filters into the query at parse time.
parsed = selector.parse("t:Core", inject={"pb": 6.5})
This is useful for system-imposed constraints such as tenant, visibility, or stock filters.
Runtime Context
context= is an optional runtime channel for custom grades.
grade(..., context=...)declares which named fields a grade wantsSelector.parse(..., context=...)stores those fields insideParsedCommand.contextand also makes them available toarg_method(...)during parsingSelector.match(...)andSelector.match_many(...)use the context already stored inParsedCommand- Missing declared context fields are filled with
None
This is useful when the query text stays the same, but matching behavior must vary between calls, for example:
- case-sensitive vs case-insensitive string comparison
- locale-specific normalization
- sort or comparison mode switches
- feature-flagged matching logic
When matching behavior must change, create another parsed snapshot with Selector.parse(..., context=...).
Main Public Objects
Grade: immutable filter behavior definitionLayer: immutable selector configurationParsedLayer: parsed layer with runtime argumentParsedCommand: immutable parsed query snapshot with optional runtime contextSelector: parser and matcher
Import note:
- Public import path:
cmdfilter
When It Fits Best
cmdfilter is a good fit when:
- you have a list of dicts, records, or row-like objects
- users need short
option:valuestyle filters - the same parsed filter must be reused many times
- you want filtering rules to stay explicit and testable in Python code
It is probably not the right tool if you need:
- full text indexing
- SQL generation
- dataframe-style analytics
- spreadsheet file reading or writing
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cmdfilter-0.1.1.tar.gz.
File metadata
- Download URL: cmdfilter-0.1.1.tar.gz
- Upload date:
- Size: 23.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de2d97939854197af8b7739e02c4922cde701c4615c4c95d6b0433d4fe721f4f
|
|
| MD5 |
d7422d4a67e70b6fc3af839867e38030
|
|
| BLAKE2b-256 |
0a9b2518f967cba20463feab5d2d765e16f761c0d6099f3336fe7841f9728c36
|
File details
Details for the file cmdfilter-0.1.1-py3-none-any.whl.
File metadata
- Download URL: cmdfilter-0.1.1-py3-none-any.whl
- Upload date:
- Size: 19.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd00708512e0f6833227131f5c4809666b862f68117992153e09821e7d917242
|
|
| MD5 |
ce50a57477924b3631ab739c7d1f652f
|
|
| BLAKE2b-256 |
146f5de0a2fe9104b0b6353512d7294b41a1321bab75fcfe673eaa305e044cac
|