Type-safe Ibis table extensions with SQLModel-like interface
Project description
typewing
Type-safe Ibis table extensions with SQLModel-like interface. Create typed models for your database tables and get full IDE autocomplete support while preserving all of Ibis's powerful query building capabilities.
Features
- SQLModel-like API: Define your table schemas using Python type annotations
- Full IDE Autocomplete: Get intelligent code completion for your table columns and their types
- Native Ibis Integration: All Ibis query operations work seamlessly
- Type Safety: Leverage Python's type system for safer database queries
- Field Aliases: Map Python field names to different database column names
- Field Metadata: Add descriptions and other metadata to your fields
Installation
pip install typewing
Or with uv:
uv add typewing
Quick Start
from typewing import IbisModel, Field
import ibis
# Define your model with type annotations
class User(IbisModel):
__tablename__ = "users"
id: int
name: str
email: str
age: int | None = Field(description="User's age in years")
is_active: bool = Field(alias="active")
# Connect to your database
con = ibis.duckdb.connect()
# Bind the model to get a typed table
UserTable = User.bind(con)
# Use all of Ibis's query building with full type safety!
query = (
UserTable
.filter(UserTable.age > 18)
.filter(UserTable.is_active == True)
.select("name", "email")
.order_by("name")
.limit(10)
)
# Execute and get results
results = query.execute()
Detailed Usage
Defining Models
Models are defined by inheriting from IbisModel and using type annotations:
from typewing import IbisModel, Field
class Product(IbisModel):
# Explicit table name (optional - defaults to lowercase class name)
__tablename__ = "products"
# Simple typed fields
product_id: int
name: str
price: float
# Optional fields using Union type
description: str | None
# Fields with metadata
category: str = Field(description="Product category")
# Fields with database column aliases
is_available: bool = Field(alias="available")
Table Names
If you don't specify __tablename__, the table name defaults to the lowercase class name:
class User(IbisModel):
id: int
name: str
# Table name will be "user"
Field Aliases
Use the alias parameter to map Python field names to different database column names:
class User(IbisModel):
user_id: int = Field(alias="id")
full_name: str = Field(alias="name")
is_active: bool = Field(alias="active")
# Now you can use Python-style names in your code:
UserTable = User.bind(con)
query = UserTable.filter(UserTable.is_active == True)
# But it maps to the actual database column "active"
Binding to a Database
Before you can query, bind your model to an Ibis connection:
import ibis
# Create a connection (DuckDB, PostgreSQL, etc.)
con = ibis.duckdb.connect()
# Bind the model
UserTable = User.bind(con)
# Optionally override the table name
CustomTable = User.bind(con, table_name="custom_users")
Query Building
The bound table supports all Ibis operations:
# Filtering
active_users = UserTable.filter(UserTable.is_active == True)
# Selecting columns
names = UserTable.select("name", "email")
# Chaining operations
query = (
UserTable
.filter(UserTable.age > 18)
.filter(UserTable.name.like("A%"))
.select("name", "email", "age")
.order_by(UserTable.age.desc())
.limit(10)
)
# Aggregations
user_count = UserTable.count().execute()
avg_age = UserTable.age.mean().execute()
# Grouping
by_category = (
ProductTable
.group_by("category")
.aggregate(
count=ProductTable.count(),
avg_price=ProductTable.price.mean()
)
)
# Joins (with other ibis tables)
orders = con.table("orders")
query = UserTable.join(orders, UserTable.id == orders.user_id)
Executing Queries
Execute queries using the .execute() method:
# Returns a pandas DataFrame
results = query.execute()
# Iterate over results
for index, row in results.iterrows():
print(row["name"], row["email"])
Working with Multiple Backends
typewing works with any Ibis backend:
# DuckDB
con = ibis.duckdb.connect()
# PostgreSQL
con = ibis.postgres.connect(
host="localhost",
database="mydb",
user="user",
password="password"
)
# SQLite
con = ibis.sqlite.connect("database.db")
# And many more: BigQuery, Snowflake, Trino, etc.
UserTable = User.bind(con)
IDE Autocomplete Support
One of the key features is full IDE autocomplete support. When you access fields on your bound table, your IDE will:
- Show all available fields
- Display their types
- Show field descriptions (from
Field()metadata) - Provide autocomplete for Ibis methods
UserTable = User.bind(con)
# Your IDE will autocomplete:
UserTable.name # knows it's a string column
UserTable.age # knows it's an int | None column
UserTable.is_active # knows it's a bool column
UserTable.filter( # autocompletes Ibis methods
Advanced Usage
Getting Field Metadata
Access field information programmatically:
# Get all fields
fields = User.get_fields()
# Get field names
field_names = User.get_field_names()
# ['id', 'name', 'email', 'age', 'is_active']
# Get field types
field_types = User.get_field_types()
# {'id': int, 'name': str, 'email': str, ...}
# Get table name
table_name = User.get_table_name()
# 'users'
# Get column name (handles aliases)
col_name = User.get_column_name('is_active')
# 'active'
Using with Ibis Expressions
You can mix typed tables with regular Ibis expressions:
import ibis
UserTable = User.bind(con)
# Use ibis functions
query = UserTable.filter(
ibis.and_(
UserTable.age > 18,
UserTable.name.length() > 3
)
)
# Window functions
query = UserTable.mutate(
rank=ibis.rank().over(order_by=UserTable.age.desc())
)
Comparison with SQLModel
If you're familiar with SQLModel, here's how typewing compares:
| Feature | SQLModel | typewing |
|---|---|---|
| Type annotations | ||
| IDE autocomplete | ||
| Field metadata | ||
| ORM-style queries | (uses Ibis) | |
| Multiple backends | Limited | (20+ via Ibis) |
| Complex queries | Via SQLAlchemy | Native Ibis |
| Data validation | ||
| Model instances | (returns DataFrames) |
typewing is designed for:
- Complex analytical queries where Ibis excels
- Working with multiple database backends with a unified API
- Type-safe query building without ORM overhead
- Data engineering/analysis workflows
Examples
Example 1: User Analytics
from typewing import IbisModel, Field
import ibis
class User(IbisModel):
__tablename__ = "users"
id: int
email: str
signup_date: str # Or use datetime if your backend supports it
age: int | None
country: str
con = ibis.duckdb.connect("analytics.db")
UserTable = User.bind(con)
# Find users by country
us_users = UserTable.filter(UserTable.country == "US")
# Age distribution
age_stats = UserTable.filter(UserTable.age.notnull()).aggregate(
avg_age=UserTable.age.mean(),
min_age=UserTable.age.min(),
max_age=UserTable.age.max(),
count=UserTable.count()
).execute()
print(f"Average age: {age_stats['avg_age'][0]:.1f}")
Example 2: E-commerce Products
class Product(IbisModel):
__tablename__ = "products"
id: int
name: str
category: str
price: float
stock: int
is_active: bool = Field(alias="active")
con = ibis.postgres.connect(...)
ProductTable = Product.bind(con)
# Find affordable products in stock
query = (
ProductTable
.filter(ProductTable.is_active == True)
.filter(ProductTable.stock > 0)
.filter(ProductTable.price < 50)
.order_by(ProductTable.price.asc())
)
affordable_products = query.execute()
Example 3: Sales Analytics
class Sale(IbisModel):
__tablename__ = "sales"
sale_id: int
product_id: int
quantity: int
revenue: float
sale_date: str
con = ibis.duckdb.connect()
SaleTable = Sale.bind(con)
# Revenue by day
daily_revenue = (
SaleTable
.group_by("sale_date")
.aggregate(
total_revenue=SaleTable.revenue.sum(),
total_quantity=SaleTable.quantity.sum(),
num_sales=SaleTable.count()
)
.order_by("sale_date")
)
results = daily_revenue.execute()
Testing
Run the test suite:
# Install dev dependencies
uv sync --extra dev
# Run tests
uv run pytest tests/ -v
# With coverage
uv run pytest tests/ --cov=typewing --cov-report=html
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
License
This project is licensed under the MIT License.
Acknowledgments
- Built on top of Ibis - the portable dataframe library
- Inspired by SQLModel - the SQL database framework
Related Projects
- Ibis - The portable Python dataframe library
- SQLModel - SQL databases in Python, designed for simplicity
- SQLAlchemy - The Python SQL toolkit
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 typewing-0.0.1.tar.gz.
File metadata
- Download URL: typewing-0.0.1.tar.gz
- Upload date:
- Size: 37.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b4083589d70bd33ac7e23224872d6296cdea0e41a1190b1ab823b9db4897055f
|
|
| MD5 |
d86f9c4d6d1932bd31c84af23a864354
|
|
| BLAKE2b-256 |
592e45b560910ab2bcc962f6de5746adddb0c1e987add4f28e9a1c71bee36190
|
File details
Details for the file typewing-0.0.1-py3-none-any.whl.
File metadata
- Download URL: typewing-0.0.1-py3-none-any.whl
- Upload date:
- Size: 7.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
76cb7636c840e8196a99e76db524f29069357c6063b160462c7873f1727f5164
|
|
| MD5 |
b2604ab21d519730666a68baa2e66ef0
|
|
| BLAKE2b-256 |
6c183ff31a4945c8359035a12c77399f3c6c14951551ded90bc29de23ca18d01
|