AsyncIO MongoDB Object Document Mapper for Python using type hints
—
ODMantic provides native MongoDB BSON types for precise data representation and serialization. These types ensure proper handling of MongoDB-specific data formats.
from odmantic import ObjectId, WithBsonSerializer
from odmantic.bson import Int64, Long, Decimal128, Binary, Regex
from decimal import DecimalMongoDB's primary key type providing unique document identifiers.
class ObjectId:
"""MongoDB ObjectId type for unique document identification."""
def __init__(self, oid=None):
"""
Create ObjectId from hex string, bytes, or generate new one.
Args:
oid: Hex string, bytes, or None for auto-generation
"""
def __str__(self):
"""
String representation as 24-character hex string.
Returns:
str: Hex string representation
"""
def __repr__(self):
"""
Python representation.
Returns:
str: ObjectId('hex_string')
"""
@property
def generation_time(self):
"""
Time when ObjectId was generated.
Returns:
datetime: Generation timestamp
"""
@classmethod
def from_datetime(cls, dt):
"""
Create ObjectId from datetime.
Args:
dt: datetime object
Returns:
ObjectId: ObjectId with timestamp from datetime
"""
@classmethod
def is_valid(cls, oid):
"""
Check if string/bytes represents valid ObjectId.
Args:
oid: String or bytes to validate
Returns:
bool: True if valid ObjectId format
"""Mixin class for custom BSON serialization behavior.
class WithBsonSerializer:
"""Mixin for custom BSON serialization."""
def __bson__(self):
"""
Custom BSON serialization method.
Returns:
Any: BSON-serializable representation
"""Additional MongoDB BSON types for specialized use cases.
class Int64:
"""64-bit integer type for MongoDB."""
def __init__(self, value: int):
"""
Create 64-bit integer.
Args:
value: Integer value
"""
Long = Int64 # Alias for Int64
class Decimal128:
"""128-bit decimal type for high-precision numbers."""
def __init__(self, value: Union[str, int, float, Decimal]):
"""
Create 128-bit decimal.
Args:
value: Decimal, string, int, or float value
"""
def to_decimal(self) -> Decimal:
"""
Convert to Python decimal.Decimal.
Returns:
Decimal: Python decimal representation
"""
class Binary:
"""Binary data type for MongoDB."""
def __init__(self, data: bytes, subtype: int = 0):
"""
Create binary data.
Args:
data: Bytes data
subtype: Binary subtype (0-255)
"""
class Regex:
"""Regular expression type for MongoDB."""
def __init__(self, pattern: str, flags: int = 0):
"""
Create regex.
Args:
pattern: Regular expression pattern string
flags: Regex flags (integer)
"""
@property
def pattern(self) -> str:
"""
Get regex pattern.
Returns:
str: Regular expression pattern
"""
@property
def flags(self) -> int:
"""
Get regex flags.
Returns:
int: Regular expression flags
"""from odmantic import Model, ObjectId
from datetime import datetime
class User(Model):
name: str
# Default ObjectId primary key (automatic)
# id: ObjectId is added automatically
class Post(Model):
title: str
author_id: ObjectId # Reference to User
content: str
# Creating ObjectIds
def objectid_examples():
# Auto-generated ObjectId
user_id = ObjectId()
print(f"Generated: {user_id}")
# From hex string
existing_id = ObjectId("507f1f77bcf86cd799439011")
print(f"From string: {existing_id}")
# From datetime (useful for date-based queries)
yesterday = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
yesterday_id = ObjectId.from_datetime(yesterday)
print(f"From datetime: {yesterday_id}")
# Validation
if ObjectId.is_valid("507f1f77bcf86cd799439011"):
print("Valid ObjectId format")
# Generation time
print(f"Generated at: {user_id.generation_time}")
# Using ObjectIds in models
async def model_objectid_example(engine):
# Create user with auto-generated ID
user = User(name="John Doe")
await engine.save(user)
print(f"User ID: {user.id}")
# Create post referencing the user
post = Post(
title="My First Post",
author_id=user.id, # Reference using ObjectId
content="Hello world!"
)
await engine.save(post)
# Query by ObjectId
found_user = await engine.find_one(User, User.id == user.id)
user_posts = await engine.find(Post, Post.author_id == user.id)class CustomModel(Model):
# Custom ObjectId field as primary key
custom_id: ObjectId = Field(primary_field=True, default_factory=ObjectId)
name: str
value: int
def custom_primary_example():
# Model uses custom_id as _id in MongoDB
model = CustomModel(name="test", value=42)
print(f"Custom ID: {model.custom_id}")
# Can also set explicitly
specific_id = ObjectId("507f1f77bcf86cd799439011")
model2 = CustomModel(custom_id=specific_id, name="specific", value=100)from datetime import datetime, timedelta
async def date_based_queries(engine):
# Find documents created since yesterday
yesterday = datetime.utcnow() - timedelta(days=1)
yesterday_id = ObjectId.from_datetime(yesterday)
recent_posts = await engine.find(Post, Post.id >= yesterday_id)
# Find documents from specific date range
week_ago = datetime.utcnow() - timedelta(days=7)
week_ago_id = ObjectId.from_datetime(week_ago)
last_week_posts = await engine.find(
Post,
Post.id >= week_ago_id,
Post.id < yesterday_id
)from odmantic import WithBsonSerializer
from decimal import Decimal
import json
class CustomSerializable(WithBsonSerializer):
def __init__(self, data):
self.data = data
def __bson__(self):
# Custom serialization for BSON
return {
"type": "custom",
"data": json.dumps(self.data),
"version": 1
}
class DocumentWithCustom(Model):
name: str
custom_field: CustomSerializable
def custom_serialization_example():
# Create document with custom serialization
custom_data = CustomSerializable({"key": "value", "number": 42})
doc = DocumentWithCustom(name="test", custom_field=custom_data)
# When saved, custom_field will be serialized using __bson__ methodfrom odmantic.bson import Int64, Decimal128, Binary, Regex
from decimal import Decimal
class AdvancedDocument(Model):
name: str
large_number: Int64
precise_decimal: Decimal128
binary_data: Binary
pattern: Regex
def extended_types_example():
# Int64 for large integers
large_num = Int64(9223372036854775807)
# Decimal128 for high precision
precise = Decimal128(Decimal("99999.999999999999999999999999999"))
# Binary for byte data
binary = Binary(b"binary data here", subtype=0)
# Regex for pattern matching
regex = Regex(r"^[a-zA-Z]+$", flags=0)
doc = AdvancedDocument(
name="advanced",
large_number=large_num,
precise_decimal=precise,
binary_data=binary,
pattern=regex
)import bson
from odmantic.bson import ObjectId
def raw_bson_examples():
# Converting to/from raw BSON
data = {
"_id": ObjectId(),
"name": "John",
"age": 30,
"balance": Decimal128("1234.56")
}
# Encode to BSON bytes
bson_bytes = bson.encode(data)
print(f"BSON size: {len(bson_bytes)} bytes")
# Decode from BSON bytes
decoded = bson.decode(bson_bytes)
print(f"Decoded: {decoded}")
# ODMantic handles this automatically, but you can work with raw BSON if neededfrom pydantic import field_validator
class ValidatedModel(Model):
id_field: ObjectId
int64_field: Int64
decimal_field: Decimal128
@field_validator('id_field', mode='before')
@classmethod
def validate_objectid(cls, v):
if isinstance(v, str):
if ObjectId.is_valid(v):
return ObjectId(v)
else:
raise ValueError('Invalid ObjectId format')
return v
@field_validator('decimal_field', mode='before')
@classmethod
def validate_decimal(cls, v):
if isinstance(v, (int, float, str)):
return Decimal128(str(v))
return v
def validation_example():
# Valid conversions
model = ValidatedModel(
id_field="507f1f77bcf86cd799439011", # String -> ObjectId
int64_field=123456789, # int -> Int64
decimal_field="123.456" # str -> Decimal128
)
print(f"ID: {model.id_field}")
print(f"Int64: {model.int64_field}")
print(f"Decimal: {model.decimal_field}")async def bson_type_queries(engine):
# Query by ObjectId
user_id = ObjectId("507f1f77bcf86cd799439011")
user = await engine.find_one(User, User.id == user_id)
# Query by ObjectId string (automatic conversion)
user = await engine.find_one(User, User.id == "507f1f77bcf86cd799439011")
# Query with Decimal128
products = await engine.find(Product, Product.price >= Decimal128("99.99"))
# Query with Int64
large_orders = await engine.find(Order, Order.total_cents >= Int64(10000))
# Regex queries
pattern_match = await engine.find(
User,
match(User.name, Regex(r"^John", flags=re.IGNORECASE))
)Install with Tessl CLI
npx tessl i tessl/pypi-odmantic