A Python library for modeling queries, filters, expressions, grouping, and aggregations as object structures
Project description
Therismos
θερισμός
Greek; noun
Harvest.
A Python library for modeling queries, filters, expressions, grouping, and aggregations as object structures.
Features
- Backend-agnostic modeling: Build expressions, filters, and aggregations independent of any specific backend
- Declarative DSL: Natural Python syntax for building complex queries
- Type safety: Optional field type declarations with automatic casting
- Immutable structures: All nodes are immutable and thread-safe
- Automatic normalization: Compound expressions are automatically flattened
- Powerful optimizer: Detects contradictions, tautologies, and simplification opportunities
- Grammar-based serialization: Convert expressions to/from compact strings for URLs and APIs
- Visitor pattern: Extensible architecture for converting to any backend format
- Optimization tracking: Optional tracking of all optimization transformations
Installation
pip install therismos
Or using uv:
uv pip install therismos
Expressions
Therismos provides a comprehensive expression system for modeling filters and conditions as object structures using an Abstract Syntax Tree (AST) approach.
Quick Start
from therismos import F, optimize
# Define fields
age = F("age", int)
name = F("name")
status = F("status")
# Build expressions using natural Python syntax
expr = (age > 18) & (name == "Alice") | (status == "admin")
# Optimize the expression
optimized, records = optimize(expr)
# More complex example: detect contradictions
contradiction = (age < 30) & (age > 40)
result, _ = optimize(contradiction)
# result is FALSE
# Aggregate OR equality chains
multi_status = (status == "active") | (status == "pending") | (status == "completed")
result, _ = optimize(multi_status)
# result is: status IN ("active", "pending", "completed")
Expression Types
Atomic Expressions
- Comparisons:
==,!=,<,<=,>,>= - Regex matching:
field.matches(pattern, flags=None) - Membership:
field.is_in(*values)orfield.is_one_of(iterable) - Null checking:
field.is_null(),field.is_not_null() - Constants:
TRUE,FALSE
Compound Expressions
- AND:
expr1 & expr2orAllExpr(expr1, expr2, ...) - OR:
expr1 | expr2orAnyExpr(expr1, expr2, ...) - NOT:
~exprorNotExpr(expr)
Type Casting
Fields can declare expected types for automatic value casting:
age = F("age", int)
price = F("price", float)
# Values are automatically cast
expr = age == "42" # value is stored as string
casted = expr.casted_value() # returns integer 42
Custom cast functions are also supported:
def normalize_email(value):
return str(value).strip().lower()
email = F("email", normalize_email)
Optimization
The optimizer applies various rules to simplify expressions and detect logical issues.
Basic Examples
from therismos import optimize, F, TRUE, FALSE, AllExpr, AnyExpr
age = F("age")
# Identity elimination
expr = AllExpr(age > 18, TRUE, age < 65)
result, _ = optimize(expr)
# result is: AllExpr(age > 18, age < 65)
# Contradiction detection
expr = (age == 25) & (age != 25)
result, _ = optimize(expr)
# result is: FALSE
# Tautology detection
expr = (age < 30) | (age >= 30)
result, _ = optimize(expr)
# result is: TRUE
# NOT simplification (De Morgan's laws)
expr = ~((age > 18) & (name == "Alice"))
result, _ = optimize(expr)
# result is: (age <= 18) OR (name != "Alice")
Optimization Rules Reference
The optimizer implements the following transformation rules:
Atomic Expression Simplifications
| Rule | Before | After |
|---|---|---|
| Empty IN to FALSE | f IN () |
FALSE |
| Single-value IN to Eq | f IN (v) |
f == v |
NOT Expression Simplifications
| Rule | Before | After |
|---|---|---|
| NOT of TRUE | NOT(TRUE) |
FALSE |
| NOT of FALSE | NOT(FALSE) |
TRUE |
| Double negation | NOT(NOT(x)) |
x |
| De Morgan's law (AND) | NOT(a AND b) |
NOT(a) OR NOT(b) |
| De Morgan's law (OR) | NOT(a OR b) |
NOT(a) AND NOT(b) |
AND Expression Simplifications
| Rule | Before | After |
|---|---|---|
| Empty AND | AND() |
TRUE |
| Single operand | AND(x) |
x |
| FALSE propagation | AND(..., FALSE, ...) |
FALSE |
| TRUE elimination | AND(..., TRUE, ...) |
AND(...) (TRUE removed) |
| All TRUE | AND(TRUE, TRUE, ...) |
TRUE |
| Eq/Eq same value | (f == v) AND (f == v) |
f == v |
| Eq/Eq different values | (f == v1) AND (f == v2) |
FALSE |
| Eq/In intersection (member) | (f == v) AND (f IN (v, ...)) |
f == v |
| Eq/In intersection (non-member) | (f == v) AND (f IN (...)) |
FALSE (v not in set) |
| In/In intersection (empty) | (f IN (v1, v2)) AND (f IN (v3, v4)) |
FALSE (no overlap) |
| In/In intersection (single) | (f IN (v1, v2)) AND (f IN (v2, v3)) |
f == v2 |
| In/In intersection (multiple) | (f IN (v1, v2, v3)) AND (f IN (v2, v3, v4)) |
f IN (v2, v3) |
OR Expression Simplifications
| Rule | Before | After |
|---|---|---|
| Empty OR | OR() |
FALSE |
| Single operand | OR(x) |
x |
| TRUE propagation | OR(..., TRUE, ...) |
TRUE |
| FALSE elimination | OR(..., FALSE, ...) |
OR(...) (FALSE removed) |
| All FALSE | OR(FALSE, FALSE, ...) |
FALSE |
| Eq/Eq union | (f == v1) OR (f == v2) |
f IN (v1, v2) |
| Eq/In union | (f == v) OR (f IN (v2, v3)) |
f IN (v, v2, v3) |
| In/In union | (f IN (v1, v2)) OR (f IN (v3, v4)) |
f IN (v1, v2, v3, v4) |
Contradiction Detection (AND)
| Pattern | Result |
|---|---|
(f == v) AND (f != v) |
FALSE |
f.is_null() AND f.is_not_null() |
FALSE |
(f < a) AND (f > b) where b >= a |
FALSE |
(f <= a) AND (f > a) |
FALSE |
(f >= b) AND (f < b) |
FALSE |
Tautology Detection (OR)
| Pattern | Result |
|---|---|
(f == v) OR (f != v) |
TRUE |
f.is_null() OR f.is_not_null() |
TRUE |
(f < v) OR (f >= v) |
TRUE |
(f <= v) OR (f > v) |
TRUE |
Complex Real-World Example: Detecting Accidental Contradictions
The optimizer is particularly valuable for catching accidentally contradictory conditions in complex business logic. Here's a realistic scenario where multiple nested requirements create an impossible condition:
from therismos import F, optimize, FALSE
# Define fields
user_age = F("age", int)
user_role = F("role")
user_status = F("status")
account_tier = F("account_tier")
dept = F("department")
experience = F("experience_years", int)
available = F("available", bool)
# Complex filter built incrementally by different team members
# Each level seemed reasonable in isolation, but together they create a contradiction
complex_filter = (
(
# Level 1: Nested OR conditions for base eligibility
(
(
# Premium account holders
(account_tier == "premium") &
(
(user_role == "developer") |
(user_role == "designer")
)
) |
(
# OR enterprise users with experience
(account_tier == "enterprise") &
(experience >= 5) &
(dept.is_in("engineering", "design"))
)
) &
# Level 2: Status and department requirements with nesting
(
(
(user_status == "active") &
(
# Nested department-specific conditions
(
(dept == "engineering") &
(experience >= 2)
) |
(
(dept == "design") &
(user_role.is_in("designer", "lead_designer"))
)
)
) |
# OR admin override
(user_role == "admin")
) &
# Level 3: First age requirement
(user_age >= 25) &
# Level 4: Second age requirement nested with other conditions
(
(user_age <= 50) &
(
# More nesting for additional validation
(account_tier.is_in("premium", "enterprise", "trial")) |
(user_role == "admin")
)
)
) & (
# Level 5: Someone later added "additional validation"
# without realizing it contradicts the previous age requirements!
(user_age < 25) & # Must be under 25
(user_age > 50) # AND must be over 50 (impossible!)
) &
(available == True)
)
# The contradiction occurs because:
# - Earlier levels require: 25 <= age <= 50
# - Final level requires: age < 25 AND age > 50
# - These conditions cannot both be true!
result, records = optimize(complex_filter)
print(f"Optimized result: {result}")
# Output: FalseExpr()
print(f"Is FALSE: {result is FALSE}")
# Output: True
print(f"Optimization steps that revealed the contradiction:")
for i, record in enumerate(records, 1):
print(f"Step {i}: {record.reason}")
if "Contradiction" in record.reason:
print(f" *** This step detected the contradiction! ***")
# Example output:
# Step 1: OR equality chain aggregation to IN
# Step 2: Optimize children in AND
# Step 3: Optimize children in OR
# Step 4: Optimize children in AND
# Step 5: Contradiction detected in AND
# *** This step detected the contradiction! ***
# By examining the 'before' expression in the contradiction record,
# you can identify exactly which requirements conflict with each other
# and trace back through your business logic to find the source.
The optimizer's tracking feature is invaluable for debugging complex business rules, especially when:
- Multiple developers contribute conditions to the same filter over time
- Requirements evolve and accidentally introduce conflicts
- Combining filters from different parts of the application
- Migrating or refactoring legacy filtering logic
- Building user-facing query builders where users can create invalid combinations
Optimization Tracking
Track optimization changes:
result, records = optimize(expr)
for record in records:
print(f"Applied: {record.reason}")
print(f"Before: {record.before}")
print(f"After: {record.after}")
You can also use a collecting parameter to accumulate records across multiple optimizations:
my_records = []
result1, _ = optimize(expr1, my_records)
result2, _ = optimize(expr2, my_records)
# my_records now contains all optimization steps from both calls
Expression Evaluation
Expressions can be evaluated against actual data to determine if the data satisfies the filter criteria. This is useful for:
- In-memory filtering when a database query is not needed
- Testing and validating filter logic
- Client-side filtering before sending data to a backend
- Data validation and access control checks
Basic Evaluation
The eval() method evaluates an expression against a dictionary of field values:
from therismos import F
age = F("age")
status = F("status")
# Build an expression
expr = (age > 18) & (status == "active")
# Evaluate against data
data = {"age": 25, "status": "active"}
result = expr.eval(data) # Returns True
data = {"age": 15, "status": "active"}
result = expr.eval(data) # Returns False
Evaluation with Type Casting
When fields have declared types, values are automatically cast during evaluation:
age = F("age", int)
expr = age >= 18
# String values are automatically cast to int
result = expr.eval({"age": "25"}) # Returns True (string "25" cast to int 25)
# This will raise TypeError or ValueError if casting fails
try:
expr.eval({"age": "not_a_number"})
except (TypeError, ValueError):
print("Invalid age value")
Evaluating Membership and Regex
All expression types support evaluation:
import re
# IN expressions
status = F("status")
expr = status.is_in("active", "pending", "approved")
result = expr.eval({"status": "active"}) # Returns True
# Regex matching
email = F("email")
expr = email.matches(r".*@example\.com$", re.IGNORECASE)
result = expr.eval({"email": "user@example.com"}) # Returns True
# Null checking
phone = F("phone")
expr = phone.is_null()
result = expr.eval({"phone": None}) # Returns True
Complex Evaluation Examples
Compound expressions evaluate all nested conditions:
age = F("age", int)
country = F("country")
verified = F("verified")
subscription = F("subscription")
# Complex eligibility check
expr = (
(age >= 18) &
(country.is_one_of(["US", "UK", "CA"])) &
((verified == True) | (subscription.is_in("premium", "enterprise")))
)
# Adult in allowed country with verification
result = expr.eval({
"age": 25,
"country": "US",
"verified": True,
"subscription": "free"
}) # Returns True
# Adult in allowed country with premium subscription (unverified)
result = expr.eval({
"age": 30,
"country": "UK",
"verified": False,
"subscription": "premium"
}) # Returns True
# Minor (fails age requirement)
result = expr.eval({
"age": 16,
"country": "US",
"verified": True,
"subscription": "premium"
}) # Returns False
Evaluation with Optimized Expressions
You can optimize expressions before evaluation for better performance or to catch logical issues:
age = F("age", int)
status = F("status")
# Build a complex expression
expr = (
((age > 18) | (age > 25)) & # Redundant condition
(status == "active") &
((age < 30) | (age >= 30)) # Tautology
)
# Optimize first
optimized, _ = optimize(expr)
# optimized is simplified to: (age > 18) AND TRUE AND (status == "active")
# which further simplifies to: (age > 18) AND (status == "active")
# Then evaluate the optimized expression
result = optimized.eval({"age": 25, "status": "active"}) # Returns True
Error Handling
Evaluation raises exceptions for invalid data:
age = F("age")
expr = age > 18
# Missing field raises KeyError
try:
expr.eval({"name": "Alice"}) # age field is missing
except KeyError:
print("Required field 'age' not found")
# Invalid type casting raises TypeError or ValueError
age_typed = F("age", int)
expr = age_typed > 18
try:
expr.eval({"age": "not_a_number"})
except (TypeError, ValueError):
print("Cannot cast value to required type")
Converting to other formats
Therismos uses the visitor pattern to enable extensible conversions of expressions to any format. You can implement custom visitors or use the built-in ones.
Custom Visitors
Implement custom visitors to convert expressions to any format:
from therismos import ExprVisitor
class SQLVisitor:
def visit_eq(self, expr):
return f"{expr.field.name} = ?"
def visit_all(self, expr):
parts = [e.accept(self) for e in expr.exprs]
return " AND ".join(parts)
# ... implement other visit methods
visitor = SQLVisitor()
sql = expr.accept(visitor)
Built-in Visitors
Therismos provides several built-in visitors for common use cases:
StringVisitor
Converts expressions to human-readable string representation:
from therismos import F, StringVisitor
age = F("age")
name = F("name")
expr = (age > 18) & (name == "Alice")
visitor = StringVisitor()
result = expr.accept(visitor)
# Output: "(age > 18 AND name = 'Alice')"
CountVisitor
Counts the number of nodes in an expression tree:
from therismos import F, CountVisitor
age = F("age")
name = F("name")
expr = (age > 18) & (name == "Alice")
visitor = CountVisitor()
count = expr.accept(visitor)
# Output: 3 (1 AllExpr + 2 atomic expressions)
DictVisitor
Converts expressions to dictionary representation for serialization:
from therismos import F, DictVisitor
age = F("age")
expr = age > 18
visitor = DictVisitor()
result = expr.accept(visitor)
# Output: {"type": "gt", "field": "age", "value": 18}
For compound expressions, the dictionary is nested:
age = F("age")
name = F("name")
expr = (age > 18) & (name == "Alice")
visitor = DictVisitor()
result = expr.accept(visitor)
# Output: {
# "type": "and",
# "exprs": [
# {"type": "gt", "field": "age", "value": 18},
# {"type": "eq", "field": "name", "value": "Alice"}
# ]
# }
FieldGathererVisitor
Collects all unique field names used in an expression tree:
from therismos import F, FieldGathererVisitor
age = F("age")
name = F("name")
status = F("status")
expr = (age > 18) & (name == "Alice") | (status == "active")
visitor = FieldGathererVisitor()
expr.accept(visitor)
field_names = visitor.field_names
# Output: {"age", "name", "status"}
This is useful for:
- Analyzing which fields are used in complex filters
- Validating that all referenced fields exist in your schema
- Generating documentation or metadata about queries
- Determining required permissions for a query
Backend Converters
MongoVisitor
The MongoVisitor converts therismos expressions to MongoDB query filters compatible with PyMongo and Motor.
Installation:
# For synchronous PyMongo
uv pip install therismos[mongodb]
# For asynchronous Motor
uv pip install therismos[mongodb-async]
Basic Usage:
from therismos import F, optimize
from therismos.expr.visitors.mongo import MongoVisitor
age = F("age")
status = F("status")
country = F("country")
# Build and optimize expression
expr = (age >= 21) & (status == "active") & (country.is_in("US", "UK", "CA"))
optimized, _ = optimize(expr)
# Convert to MongoDB filter
visitor = MongoVisitor()
mongo_filter = optimized.accept(visitor)
# Result: {
# "age": {"$gte": 21},
# "status": "active",
# "country": {"$in": ["US", "UK", "CA"]}
# }
Using with PyMongo:
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["mydb"]
collection = db["users"]
# Use the generated filter
results = collection.find(mongo_filter)
for doc in results:
print(doc)
Using with Motor (async):
import asyncio
from motor.motor_asyncio import AsyncIOMotorClient
async def find_users():
client = AsyncIOMotorClient("mongodb://localhost:27017/")
db = client["mydb"]
collection = db["users"]
# Use the generated filter
cursor = collection.find(mongo_filter)
results = await cursor.to_list(length=100)
return results
asyncio.run(find_users())
Advanced Features:
The MongoVisitor handles all therismos expression types:
import re
from therismos import F, TRUE, FALSE
from therismos.expr.visitors.mongo import MongoVisitor
email = F("email")
age = F("age")
name = F("name")
status = F("status")
visitor = MongoVisitor()
# Regex matching (with case-insensitive flag)
expr = email.matches(r".*@example\.com$", re.IGNORECASE)
mongo_filter = expr.accept(visitor)
# Result: {"email": {"$regex": ".*@example\\.com$", "$options": "i"}}
# Range queries
expr = (age >= 18) & (age <= 65)
mongo_filter = expr.accept(visitor)
# Result: {"age": {"$gte": 18, "$lte": 65}} (optimized)
# Complex OR conditions
expr = (status == "active") | (status == "pending") | (status == "approved")
optimized_expr, _ = optimize(expr) # Converts to IN
mongo_filter = optimized_expr.accept(visitor)
# Result: {"status": {"$in": ["active", "pending", "approved"]}}
# Null checking
expr = name.is_not_null()
mongo_filter = expr.accept(visitor)
# Result: {"name": {"$ne": null}}
# NOT expressions
expr = ~(age < 18)
mongo_filter = expr.accept(visitor)
# Result: {"$nor": [{"age": {"$lt": 18}}]}
# Constants
true_filter = TRUE.accept(visitor) # Result: {}
false_filter = FALSE.accept(visitor) # Result: {"$expr": false}
Optimization Options:
# By default, simple AND expressions are optimized by merging fields
visitor = MongoVisitor(optimize_simple_and=True)
expr = (age > 18) & (name == "Alice")
mongo_filter = expr.accept(visitor)
# Result: {"age": {"$gt": 18}, "name": "Alice"}
# Disable optimization to always use $and
visitor = MongoVisitor(optimize_simple_and=False)
mongo_filter = expr.accept(visitor)
# Result: {"$and": [{"age": {"$gt": 18}}, {"name": "Alice"}]}
Type Casting:
The MongoVisitor respects field type declarations and automatically casts values:
age = F("age", int)
expr = age.is_in(18, 21, 25) # Values will be cast to int
visitor = MongoVisitor()
mongo_filter = expr.accept(visitor)
# Result: {"age": {"$in": [18, 21, 25]}}
Expression Serialization
Therismos provides grammar-based serialization to convert expressions to/from compact string representations. This is particularly useful for URL query strings, API parameters, and storing filters as text.
Basic Serialization
The Serializer class converts expressions to compact strings without spaces:
from therismos import F, Serializer, Eq, AllExpr, Gt
age = F("age")
score = F("score")
# Create a serializer
serializer = Serializer()
# Serialize an expression
expr = Eq(age, 18)
text = serializer.serialize(expr)
# Result: "age==18"
# Compound expressions
expr = AllExpr(Eq(age, 18), Gt(score, 75))
text = serializer.serialize(expr)
# Result: "(age==18;score>75)"
Deserialization
Convert strings back to expression objects:
# Deserialize a string to an expression
expr = serializer.deserialize("age==18")
# Result: Eq(field=Field(name='age', type_=None), value=18)
# Complex expressions work too
expr = serializer.deserialize('(age>18;status=="active")')
# Result: AllExpr with two conditions
Grammar Operators
The serializer uses a compact grammar optimized for URL usage:
| Python Operator | Grammar Syntax | Example |
|---|---|---|
& (AND) |
; |
age>18;status=="active" |
| (OR) |
, |
status=="active",status=="pending" |
~ (NOT) |
! |
!(age<18) |
== |
== |
age==18 |
!= |
!= |
status!="inactive" |
< |
< |
age<65 |
<= |
<= |
age<=65 |
> |
> |
age>18 |
>= |
>= |
age>=18 |
.is_in() |
=in= |
status=in=("active","pending") |
.matches() |
~regex |
email~regex(".*@example\\.com") |
.is_null() |
==null |
deleted_at==null |
.is_not_null() |
!=null |
created_at!=null |
TRUE |
true() |
true() |
FALSE |
false() |
false() |
Precedence: ! (NOT) > ; (AND) > , (OR)
URL Encoding
For use in URL query strings, enable URL encoding:
# Create a serializer with URL encoding
serializer = Serializer(url_encode=True)
# Serialize with URL encoding
expr = Eq(F("name"), "Alice Smith")
text = serializer.serialize(expr)
# Result: URL-encoded string
# Deserialize automatically decodes
expr = serializer.deserialize(text)
# Result: Original expression
Type Annotations
Control type annotation output:
age = F("age", int)
name = F("name", str)
# Without type annotations (default)
serializer = Serializer()
text = serializer.serialize(Eq(age, 18))
# Result: "age==18"
# With all type annotations
serializer = Serializer(include_all_types=True)
text = serializer.serialize(Eq(age, 18))
# Result: "age{int}==18"
# Complex example with types
expr = AllExpr(Eq(age, 18), Eq(name, "Alice"))
text = serializer.serialize(expr)
# Result: "(age{int}==18;name{str}==\"Alice\")"
Custom Types
Register custom types for serialization:
def uppercase_transform(x):
return str(x).upper()
serializer = Serializer()
serializer.register_type(uppercase_transform, "upper")
# Use the custom type
field = F("code", uppercase_transform)
expr = Eq(field, "abc")
# Serialize with type annotation
serializer_typed = Serializer(include_all_types=True)
serializer_typed.register_type(uppercase_transform, "upper")
text = serializer_typed.serialize(expr)
# Result: "code{upper}==\"ABC\"" (value is transformed)
Automatic Value Casting
When deserializing expressions with type annotations, values are automatically cast to the field's declared type:
import uuid
from therismos import Serializer, F
serializer = Serializer()
serializer.register_type(uuid.UUID, 'uuid.UUID')
# Deserialize with type annotation
expr = serializer.deserialize('user_id{uuid.UUID}=="550e8400-e29b-41d4-a716-446655440000"')
# Value is automatically cast to UUID
assert isinstance(expr.value, uuid.UUID)
assert expr.value == uuid.UUID("550e8400-e29b-41d4-a716-446655440000")
# Evaluation works with both string and UUID data
data_uuid = {"user_id": uuid.UUID("550e8400-e29b-41d4-a716-446655440000")}
data_str = {"user_id": "550e8400-e29b-41d4-a716-446655440000"}
assert expr.eval(data_uuid) is True # Works with UUID
assert expr.eval(data_str) is True # Works with string (cast automatically)
Automatic casting works for all expression types:
from decimal import Decimal
serializer.register_type(Decimal, 'Decimal')
# Comparison expressions
expr = serializer.deserialize('price{Decimal}>="19.99"')
assert isinstance(expr.value, Decimal)
# IN expressions
expr = serializer.deserialize(
'status_id{uuid.UUID}=in=("11111111-1111-1111-1111-111111111111",'
'"22222222-2222-2222-2222-222222222222")'
)
assert all(isinstance(v, uuid.UUID) for v in expr.values)
Implicit Field Types
Define type mappings for field names to avoid repeating type annotations:
import uuid
from decimal import Decimal
from therismos import Serializer
# Define field type mappings
field_types = {
"user_id": uuid.UUID,
"product_id": uuid.UUID,
"order_id": uuid.UUID,
"price": Decimal,
"amount": Decimal,
}
serializer = Serializer(field_types=field_types)
serializer.register_type(uuid.UUID, 'uuid.UUID')
serializer.register_type(Decimal, 'Decimal')
# No type annotation needed - uses implicit mapping
expr = serializer.deserialize('user_id=="550e8400-e29b-41d4-a716-446655440000"')
assert expr.field.type_ is uuid.UUID
assert isinstance(expr.value, uuid.UUID)
# Works in compound expressions
expr = serializer.deserialize(
'user_id=="550e8400-e29b-41d4-a716-446655440000";price>="19.99"'
)
# Both fields automatically get their types
You can also register field types programmatically:
serializer = Serializer()
serializer.register_type(uuid.UUID, 'uuid.UUID')
serializer.register_field_type("account_id", uuid.UUID)
expr = serializer.deserialize('account_id=="11111111-1111-1111-1111-111111111111"')
assert expr.field.type_ is uuid.UUID
Explicit type annotations always override implicit mappings:
field_types = {"amount": Decimal}
serializer = Serializer(field_types=field_types)
# Explicit type overrides implicit
expr = serializer.deserialize('amount{int}=="100"')
assert expr.field.type_ is int # Not Decimal
assert expr.value == 100
Benefits of Implicit Field Types:
- Cleaner serialization: No need to annotate every field
- Centralized schema: Define types once, use everywhere
- Type safety: Automatic casting during deserialization
- Flexibility: Explicit annotations still work and override implicit types
Complete Example
from therismos import F, Serializer, optimize
# Build a complex expression
age = F("age", int)
status = F("status")
country = F("country")
expr = (
(age >= 21) &
((status == "active") | (status == "pending")) &
(country.is_in("US", "UK", "CA"))
)
# Optimize it
optimized, _ = optimize(expr)
# The OR chain becomes an IN expression
# Serialize for a URL query parameter
serializer = Serializer(url_encode=True)
query_param = serializer.serialize(optimized)
# Use in URL: /api/users?filter={query_param}
# Later, deserialize from the URL parameter
received_expr = serializer.deserialize(query_param)
# Use the expression for database queries, validation, etc.
Value Types
The serializer handles various value types:
serializer = Serializer()
# Strings (double-quoted with escapes)
expr = Eq(F("name"), "Alice")
# Result: "name==\"Alice\""
# Numbers (integers and floats)
expr = Eq(F("age"), 25)
# Result: "age==25"
expr = Eq(F("price"), 19.99)
# Result: "price==19.99"
# Booleans
expr = Eq(F("active"), True)
# Result: "active==true"
# Null
expr = Eq(F("value"), None)
# Result: "value==null"
# Identifiers (unquoted - interpreted as strings)
expr = serializer.deserialize("status==active")
# value is the string "active"
Field Names with Dots
The serializer supports nested field references using dot notation:
expr = Eq(F("user.profile.age"), 25)
text = serializer.serialize(expr)
# Result: "user.profile.age==25"
Module Structure
Therismos is organized into the following modules and submodules:
therismos/
├── __init__.py # Main package exports
└── expr/ # Expression module
├── __init__.py # Expression module exports
├── _expr.py # Core expression classes (Expr, Field, operators, etc.)
├── optimizer.py # Expression optimization and simplification
├── serializer.py # Grammar-based string serialization/deserialization
└── visitors/ # Visitor implementations package
├── __init__.py # Core visitor exports
├── _visitors.py # Built-in visitor implementations
└── mongo.py # MongoDB query filter converter
Core Modules
-
therismos.expr: Core expression AST implementation- Expression types:
Eq,Ne,Lt,Le,Gt,Ge,Regex,In,IsNull - Compound expressions:
AllExpr,AnyExpr,NotExpr - Logical constants:
TRUE,FALSE - Field types:
Field,F(helper function) - Visitor protocol:
ExprVisitor - Serialization:
Serializer(grammar-based string conversion)
- Expression types:
-
therismos.expr.optimizer: Expression optimizationoptimize(expr, records=None): Optimize an expression treeOptimizationRecord: Records of optimization transformations
-
therismos.expr.serializer: Grammar-based serializationSerializer: Converts expressions to/from compact string representations- URL encoding support for query parameters
- Type annotation control
- Custom type registration
-
therismos.expr.visitors: Built-in visitor implementationsStringVisitor: Converts expressions to human-readable stringsCountVisitor: Counts nodes in expression treesDictVisitor: Converts expressions to dictionary representationFieldGathererVisitor: Collects all field names used in an expression
-
therismos.expr.visitors.mongo: MongoDB backend converterMongoVisitor: Converts expressions to MongoDB query filters for PyMongo/Motor
Development
Requires Python 3.11 or higher.
Setup
# Install dependencies
uv pip install -e ".[dev]"
# Run tests
pytest
# Run linting
ruff check therismos tests
# Run type checking
mypy therismos
# Run all checks with tox
tox
Testing
The project uses pytest with extensive parametrization for comprehensive test coverage:
# Run all tests
pytest
# Run with coverage
pytest --cov=therismos --cov-report=html
# Run specific test file
pytest tests/test_optimizer.py
License
MIT
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 therismos-0.2.0.tar.gz.
File metadata
- Download URL: therismos-0.2.0.tar.gz
- Upload date:
- Size: 50.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a50196423045505f979460d6163764878d7f783658c0c3667621196bd2adb456
|
|
| MD5 |
d614ce03e305591e09b82f24a00e7bcd
|
|
| BLAKE2b-256 |
b473c4dfc7e53e88606e2e386e839d3da3948b8d66e35f6204a93fa7bdfa3364
|
File details
Details for the file therismos-0.2.0-py3-none-any.whl.
File metadata
- Download URL: therismos-0.2.0-py3-none-any.whl
- Upload date:
- Size: 31.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
265fd54d37afd08af619f02bf616def1c784d7828863e0b276d96d5f9169e055
|
|
| MD5 |
f059c1fe138bda70379c10efe800ebd3
|
|
| BLAKE2b-256 |
223d0699967e1f5fb81a3b38dc4421368bd103a963376dad314a01f1872223a9
|