# async-easy-model
A simplified SQLModel-based ORM for async database operations in Python. async-easy-model provides a clean, intuitive interface for common database operations while leveraging the power of SQLModel and SQLAlchemy.
<p align="center">
<img src="https://img.shields.io/pypi/v/async-easy-model" alt="PyPI Version">
<img src="https://img.shields.io/pypi/pyversions/async-easy-model" alt="Python Versions">
<img src="https://img.shields.io/github/license/puntorigen/easy_model" alt="License">
</p>
## Features
- 🚀 Easy-to-use async database operations with standardized methods
- 🔄 Intuitive APIs with sensible defaults for rapid development
- 📊 Dictionary-based CRUD operations (select, insert, update, delete)
- 🔗 Enhanced relationship handling with eager loading and nested operations
- 🔍 Powerful query methods with flexible ordering support
- ⚙️ Automatic relationship detection and bidirectional setup
- 📱 Support for PostgreSQL, SQLite, and MySQL databases
- 🛠️ Built on top of SQLModel and SQLAlchemy for robust performance
- 📝 Type hints for better IDE support
- 🕒 Automatic `id`, `created_at` and `updated_at` fields provided by default
- ⏰ **PostgreSQL DateTime Compatibility**: Automatic timezone-aware to timezone-naive datetime conversion for PostgreSQL TIMESTAMP WITHOUT TIME ZONE columns
- 🔄 Automatic schema migrations for evolving database models
- 📊 Visualization of database schema using Mermaid ER diagrams
- 📋 **JSON Column Support**: Native support for JSON columns with proper serialization in migrations
## Installation
```bash
pip install async-easy-model
```
## Basic Usage
```python
from async_easy_model import EasyModel, init_db, db_config, Field
from typing import Optional
from datetime import datetime
# Configure your database
db_config.configure_sqlite("database.db")
# Define your model
class User(EasyModel, table=True):
#no need to specify id, created_at or updated_at since EasyModel provides them by default
username: str = Field(unique=True)
email: str
# Initialize your database
async def setup():
await init_db()
# Use it in your async code
async def main():
await setup()
# Create a new user
user = await User.insert({
"username": "john_doe",
"email": "john@example.com"
})
# Get user ID
print(f"New user id: {user.id}")
```
## CRUD Operations
First, let's define some models that we'll use throughout the examples:
```python
from async_easy_model import EasyModel, Field
from typing import Optional, List
from datetime import datetime
class User(EasyModel, table=True):
username: str = Field(unique=True)
email: str
is_active: bool = Field(default=True)
class Post(EasyModel, table=True):
title: str
content: str
user_id: Optional[int] = Field(default=None, foreign_key="user.id")
class Comment(EasyModel, table=True):
text: str
post_id: Optional[int] = Field(default=None, foreign_key="post.id")
user_id: Optional[int] = Field(default=None, foreign_key="user.id")
class Department(EasyModel, table=True):
name: str = Field(unique=True)
class Product(EasyModel, table=True):
name: str
price: float
sales: int = Field(default=0)
class Book(EasyModel, table=True):
title: str
author_id: Optional[int] = Field(default=None, foreign_key="author.id")
class Author(EasyModel, table=True):
name: str
```
### Create (Insert)
```python
# Insert a single record
user = await User.insert({
"username": "john_doe",
"email": "john@example.com"
})
# Insert multiple records
users = await User.insert([
{"username": "user1", "email": "user1@example.com"},
{"username": "user2", "email": "user2@example.com"}
])
# Insert with nested relationships
new_post = await Post.insert({
"title": "My Post",
"content": "Content here",
"user": {"username": "jane_doe"}, # Will automatically link to existing user
"comments": [ # Create multiple comments in a single transaction
{"text": "Great post!", "user": {"username": "reader1"}},
{"text": "Thanks for sharing", "user": {"username": "reader2"}}
]
})
# Access nested data without requerying
print(f"Post by {new_post.user.username} with {len(new_post.comments)} comments")
# Insert with nested one-to-many relationships
publisher = await Publisher.insert({
"name": "Example Publisher",
"books": [ # List of nested objects
{
"title": "Python Mastery",
"genres": [
{"name": "Programming"},
{"name": "Education"}
]
},
{"title": "Data Science Handbook"}
]
})
# Access nested relationships immediately
print(f"Publisher: {publisher.name} with {len(publisher.books)} books")
print(f"First book genres: {[g.name for g in publisher.books[0].genres]}")
```
### Read (Retrieve)
```python
# Select by ID
user = await User.select({"id": 1})
# Select with criteria
users = await User.select({"is_active": True}, all=True)
# Select first matching record
first_user = await User.select({"is_active": True}, first=True)
# Select all records
all_users = await User.select({}, all=True)
# Select with wildcard pattern matching
gmail_users = await User.select({"email": "*@gmail.com"}, all=True)
# Select with ordering
recent_users = await User.select({}, order_by="-created_at", all=True)
# Select with limit
latest_posts = await Post.select({}, order_by="-created_at", limit=5)
# Note: limit > 1 automatically sets all=True
# Select with multiple ordering fields
sorted_users = await User.select({}, order_by=["last_name", "first_name"], all=True)
# Select with relationship ordering
posts_by_author = await Post.select({}, order_by="user.username", all=True)
```
### Update
```python
# Update by ID
user = await User.update({"is_active": False}, 1)
# Update by criteria
count = await User.update(
{"is_active": False},
{"last_login": None} # Set all users without login to inactive
)
# Update with relationships
await User.update(
{"department": {"name": "Sales"}}, # Update department relationship
{"username": "john_doe"}
)
```
### Delete
```python
# Delete by ID
success = await User.delete(1)
# Delete by criteria
deleted_count = await User.delete({"is_active": False})
# Delete with compound criteria
await Post.delete({"user": {"username": "john_doe"}, "is_published": False})
```
## Database Schema Visualization
The package includes a `ModelVisualizer` class that makes it easy to generate Entity-Relationship (ER) diagrams for your database models using Mermaid syntax.
```python
from async_easy_model import EasyModel, init_db, db_config, ModelVisualizer
# Initialize your models and database
await init_db()
# Create a visualizer
visualizer = ModelVisualizer()
# Generate a Mermaid ER diagram
er_diagram = visualizer.mermaid()
print(er_diagram)
# Generate a shareable link to view the diagram online
er_link = visualizer.mermaid_link()
print(er_link)
# Customize the diagram title
visualizer.set_title("My Project Database Schema")
custom_diagram = visualizer.mermaid()
```
### Example Mermaid ER Diagram Output
```mermaid
---
title: EasyModel Table Schemas
config:
layout: elk
---
erDiagram
author {
number id PK
string name "required"
string email
}
book {
number id PK
string title "required"
number author_id FK
string isbn
number published_year
author author "virtual"
tag[] tags "virtual"
}
tag {
number id PK
string name "required"
book[] books "virtual"
}
book_tag {
number id PK
number book_id FK "required"
number tag_id FK "required"
book book "virtual"
tag tag "virtual"
}
review {
number id PK
number book_id FK "required"
number rating "required"
string comment
string reviewer_name "required"
book book "virtual"
}
book ||--o{ author : "author_id"
book_tag ||--o{ book : "book_id"
book_tag ||--o{ tag : "tag_id"
book }o--o{ tag : "many-to-many"
review ||--o{ book : "book_id"
```
The diagram automatically:
- Shows all tables with their fields and data types
- Identifies primary keys (PK) and foreign keys (FK)
- Shows required fields and virtual relationships
- Visualizes relationships between tables with proper cardinality
- Properly handles many-to-many relationships
## Convenient Query Methods
async-easy-model provides simplified methods for common query patterns:
```python
# Get all records with relationships loaded (default)
users = await User.all()
# Get all records ordered by a field (ascending)
users = await User.all(order_by="username")
# Get all records ordered by a field (descending)
newest_users = await User.all(order_by="-created_at")
# Get all records ordered by multiple fields
sorted_users = await User.all(order_by=["last_name", "first_name"])
# Get all records ordered by relationship fields
books = await Book.all(order_by="author.name")
# Get the first record
user = await User.first()
# Get the most recently created user
newest_user = await User.first(order_by="-created_at")
# Get a limited number of records
recent_users = await User.limit(10)
# Get a limited number of records with ordering
top_products = await Product.limit(5, order_by="-sales")
```
## SQLAlchemy/SQLModel Compatibility Layer
**New in v0.4.3**: async-easy-model now includes a compatibility layer that allows you to use familiar SQLAlchemy query patterns alongside EasyModel's simplified API. This enables smooth migration from existing SQLAlchemy/SQLModel code and provides a familiar interface for developers coming from SQLAlchemy backgrounds.
### Query Builder Pattern
The compatibility layer provides a `query()` method that returns an AsyncQuery builder, allowing you to chain filter operations just like SQLAlchemy:
```python
from async_easy_model import EasyModel, Field
from async_easy_model.compat import AsyncQuery # For IDE type hints (optional)
class User(EasyModel, table=True):
username: str = Field(index=True)
email: str
age: int
is_active: bool = Field(default=True)
# Use the query builder pattern
users = await User.query().filter(User.age > 25).all()
user = await User.query().filter_by(username="john").first()
# Chain multiple operations
active_adults = await (
User.query()
.filter(User.age >= 18)
.filter(User.is_active == True)
.order_by(User.username)
.limit(10)
.all()
)
# IDE Support Tip (Optional):
# For full IDE autocomplete, add a type annotation:
query: AsyncQuery[User] = User.query() # Optional - only for IDE support
filtered = query.filter(User.age > 25) # IDE will show all available methods
```
### Available Methods
The compatibility layer provides all common SQLAlchemy patterns:
#### Query Building Methods
- `filter()` - Filter using SQLAlchemy expressions
- `filter_by()` - Filter using keyword arguments
- `order_by()` - Sort results
- `limit()` - Limit number of results
- `offset()` - Skip results
- `join()` - Join related tables
#### Terminal Methods (async)
- `all()` - Get all matching records
- `first()` - Get first matching record
- `one()` - Get exactly one record (raises if not found)
- `one_or_none()` - Get one record or None
- `count()` - Count matching records
- `exists()` - Check if any records match
#### Instance Methods
```python
user = await User.find(1)
await user.save() # Save changes to database
await user.refresh() # Refresh from database
await user.delete_instance() # Delete this record
```
#### Class Methods
```python
# Create new record
user = await User.create(username="jane", email="jane@example.com")
# Find records
user = await User.find(1) # By ID
user = await User.find_by(username="john") # By field
# Bulk operations
users = await User.bulk_create([
{"username": "user1", "email": "user1@example.com"},
{"username": "user2", "email": "user2@example.com"}
])
# Check existence
exists = await User.exists(username="john")
# Count records
count = await User.count()
```
### SQLAlchemy Statement Builders
For advanced use cases, you can access SQLAlchemy statement builders:
```python
# Get SQLAlchemy select statement
stmt = User.select_stmt().where(User.age > 25)
# Use with session
async with User.session() as session:
result = await session.execute(stmt)
users = result.scalars().all()
```
### Mixed Usage Example
You can freely mix EasyModel's simplified API with SQLAlchemy patterns:
```python
# EasyModel style
users = await User.select({"age": {"$gt": 25}})
# SQLAlchemy compatibility style
users = await User.query().filter(User.age > 25).all()
# Both work seamlessly together!
```
## Enhanced Relationship Handling
Using the models defined earlier, here's how to work with relationships:
```python
# Load all relationships automatically
post = await Post.select({"id": 1})
print(post.user.username) # Access related objects directly
# Load specific relationships
post = await Post.get_with_related(1, ["user", "comments"])
# Load relationships after fetching
post = await Post.select({"id": 1}, include_relationships=False)
await post.load_related(["user", "comments"])
# Insert with nested relationships
new_post = await Post.insert({
"title": "My Post",
"content": "Content here",
"user": {"username": "jane_doe"}, # Will automatically link to existing user
"comments": [ # Create multiple comments in a single transaction
{"text": "Great post!", "user": {"username": "reader1"}},
{"text": "Thanks for sharing", "user": {"username": "reader2"}}
]
})
# Access nested data without requerying
print(f"Post by {new_post.user.username} with {len(new_post.comments)} comments")
# Convert to dictionary with nested relationships
post_dict = post.to_dict(include_relationships=True, max_depth=2)
```
## Automatic Relationship Detection
The package can automatically detect and set up bidirectional relationships between models:
```python
class User(EasyModel, table=True):
username: str
class Post(EasyModel, table=True):
title: str
user_id: int = Field(foreign_key="user.id")
# After init_db():
# - post.user relationship is automatically available
# - user.posts relationship is automatically available
```
## Database Configuration
```python
# SQLite Configuration
db_config.configure_sqlite("database.db")
db_config.configure_sqlite(":memory:") # In-memory database
# PostgreSQL Configuration
db_config.configure_postgres(
user="your_user",
password="your_password",
host="localhost",
port="5432",
database="your_database"
)
# MySQL Configuration
db_config.configure_mysql(
user="your_user",
password="your_password",
host="localhost",
port="3306",
database="your_database"
)
# Custom Connection URL
db_config.set_connection_url("postgresql+asyncpg://user:password@localhost:5432/database")
```
### Configurable Default for Relationship Loading
**New in v0.3.9**: You can now set a global default for `include_relationships` behavior across all query methods:
```python
# Configure with default_include_relationships=False for better performance
db_config.configure_sqlite("database.db", default_include_relationships=False)
# Or for PostgreSQL
db_config.configure_postgres(
user="your_user",
password="your_password",
host="localhost",
port="5432",
database="your_database",
default_include_relationships=False # Set global default
)
# Or for MySQL
db_config.configure_mysql(
user="your_user",
password="your_password",
host="localhost",
port="3306",
database="your_database",
default_include_relationships=False # Set global default
)
```
**Benefits:**
- **Performance**: Set `default_include_relationships=False` to avoid loading relationships by default, improving query performance
- **Flexibility**: Still override per method call with explicit `True` or `False` values
- **Backward Compatible**: Defaults to `True` if not specified, maintaining existing behavior
**Usage Examples:**
```python
# With default_include_relationships=False configured:
users = await User.all() # No relationships loaded (faster)
users_with_rels = await User.all(include_relationships=True) # Override to load relationships
# With default_include_relationships=True configured (default behavior):
users = await User.all() # Relationships loaded
users_no_rels = await User.all(include_relationships=False) # Override to skip relationships
```
### Database Initialization Options
**New in v0.4.1**: The `init_db()` function now supports configurable auto-relationships handling:
```python
# Default behavior (auto-detect auto-relationships availability)
await init_db()
# Force enable auto-relationships (will warn if not available)
await init_db(has_auto_relationships=True)
# Force disable auto-relationships
await init_db(has_auto_relationships=False)
# Combined with other parameters
await init_db(
migrate=True,
model_classes=[User, Post, Comment],
has_auto_relationships=False
)
```
**Benefits:**
- **Control**: Explicitly enable or disable auto-relationships functionality
- **Reliability**: Auto-relationships errors now issue warnings instead of stopping database initialization
- **Flexibility**: Can be combined with migration control and specific model classes
- **Robust**: Database initialization continues even if auto-relationships fail
**Parameter Priority:**
1. Explicit `has_auto_relationships` parameter (if provided)
2. Auto-detection of auto-relationships availability (default)
3. Fallback to available functionality
## Documentation
For more detailed documentation, please visit the [GitHub repository](https://github.com/puntorigen/easy-model) or refer to the [DOCS.md](https://github.com/puntorigen/easy-model/blob/master/DOCS.md) file.
## License
This project is licensed under the MIT License - see the LICENSE file for details.
Raw data
{
"_id": null,
"home_page": "https://github.com/puntorigen/easy-model",
"name": "async-easy-model",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.7",
"maintainer_email": null,
"keywords": "orm, sqlmodel, database, async, postgresql, sqlite, mysql",
"author": "Pablo Schaffner",
"author_email": "pablo@puntorigen.com",
"download_url": "https://files.pythonhosted.org/packages/e7/8b/a6e3b027d7013fb1e408983e94794a098f0cca919e61d3cbddab2c5255a6/async_easy_model-0.4.3.tar.gz",
"platform": null,
"description": "# async-easy-model\n\nA simplified SQLModel-based ORM for async database operations in Python. async-easy-model provides a clean, intuitive interface for common database operations while leveraging the power of SQLModel and SQLAlchemy.\n\n<p align=\"center\">\n <img src=\"https://img.shields.io/pypi/v/async-easy-model\" alt=\"PyPI Version\">\n <img src=\"https://img.shields.io/pypi/pyversions/async-easy-model\" alt=\"Python Versions\">\n <img src=\"https://img.shields.io/github/license/puntorigen/easy_model\" alt=\"License\">\n</p>\n\n## Features\n\n- \ud83d\ude80 Easy-to-use async database operations with standardized methods\n- \ud83d\udd04 Intuitive APIs with sensible defaults for rapid development\n- \ud83d\udcca Dictionary-based CRUD operations (select, insert, update, delete)\n- \ud83d\udd17 Enhanced relationship handling with eager loading and nested operations\n- \ud83d\udd0d Powerful query methods with flexible ordering support\n- \u2699\ufe0f Automatic relationship detection and bidirectional setup\n- \ud83d\udcf1 Support for PostgreSQL, SQLite, and MySQL databases\n- \ud83d\udee0\ufe0f Built on top of SQLModel and SQLAlchemy for robust performance\n- \ud83d\udcdd Type hints for better IDE support\n- \ud83d\udd52 Automatic `id`, `created_at` and `updated_at` fields provided by default\n- \u23f0 **PostgreSQL DateTime Compatibility**: Automatic timezone-aware to timezone-naive datetime conversion for PostgreSQL TIMESTAMP WITHOUT TIME ZONE columns\n- \ud83d\udd04 Automatic schema migrations for evolving database models\n- \ud83d\udcca Visualization of database schema using Mermaid ER diagrams\n- \ud83d\udccb **JSON Column Support**: Native support for JSON columns with proper serialization in migrations\n\n## Installation\n\n```bash\npip install async-easy-model\n```\n\n## Basic Usage\n\n```python\nfrom async_easy_model import EasyModel, init_db, db_config, Field\nfrom typing import Optional\nfrom datetime import datetime\n\n# Configure your database\ndb_config.configure_sqlite(\"database.db\")\n\n# Define your model\nclass User(EasyModel, table=True):\n #no need to specify id, created_at or updated_at since EasyModel provides them by default\n username: str = Field(unique=True)\n email: str\n\n# Initialize your database\nasync def setup():\n await init_db()\n\n# Use it in your async code\nasync def main():\n await setup()\n # Create a new user\n user = await User.insert({\n \"username\": \"john_doe\",\n \"email\": \"john@example.com\"\n })\n \n # Get user ID\n print(f\"New user id: {user.id}\")\n```\n\n## CRUD Operations\n\nFirst, let's define some models that we'll use throughout the examples:\n\n```python\nfrom async_easy_model import EasyModel, Field\nfrom typing import Optional, List\nfrom datetime import datetime\n\nclass User(EasyModel, table=True):\n username: str = Field(unique=True)\n email: str\n is_active: bool = Field(default=True)\n \nclass Post(EasyModel, table=True):\n title: str\n content: str\n user_id: Optional[int] = Field(default=None, foreign_key=\"user.id\")\n \nclass Comment(EasyModel, table=True):\n text: str\n post_id: Optional[int] = Field(default=None, foreign_key=\"post.id\")\n user_id: Optional[int] = Field(default=None, foreign_key=\"user.id\")\n \nclass Department(EasyModel, table=True):\n name: str = Field(unique=True)\n \nclass Product(EasyModel, table=True):\n name: str\n price: float\n sales: int = Field(default=0)\n \nclass Book(EasyModel, table=True):\n title: str\n author_id: Optional[int] = Field(default=None, foreign_key=\"author.id\")\n \nclass Author(EasyModel, table=True):\n name: str\n```\n\n### Create (Insert)\n\n```python\n# Insert a single record\nuser = await User.insert({\n \"username\": \"john_doe\",\n \"email\": \"john@example.com\"\n})\n\n# Insert multiple records\nusers = await User.insert([\n {\"username\": \"user1\", \"email\": \"user1@example.com\"},\n {\"username\": \"user2\", \"email\": \"user2@example.com\"}\n])\n\n# Insert with nested relationships\nnew_post = await Post.insert({\n \"title\": \"My Post\",\n \"content\": \"Content here\",\n \"user\": {\"username\": \"jane_doe\"}, # Will automatically link to existing user\n \"comments\": [ # Create multiple comments in a single transaction\n {\"text\": \"Great post!\", \"user\": {\"username\": \"reader1\"}},\n {\"text\": \"Thanks for sharing\", \"user\": {\"username\": \"reader2\"}}\n ]\n})\n# Access nested data without requerying\nprint(f\"Post by {new_post.user.username} with {len(new_post.comments)} comments\")\n\n# Insert with nested one-to-many relationships \npublisher = await Publisher.insert({\n \"name\": \"Example Publisher\",\n \"books\": [ # List of nested objects\n {\n \"title\": \"Python Mastery\",\n \"genres\": [\n {\"name\": \"Programming\"},\n {\"name\": \"Education\"}\n ]\n },\n {\"title\": \"Data Science Handbook\"}\n ]\n})\n# Access nested relationships immediately\nprint(f\"Publisher: {publisher.name} with {len(publisher.books)} books\")\nprint(f\"First book genres: {[g.name for g in publisher.books[0].genres]}\")\n```\n\n### Read (Retrieve)\n\n```python\n# Select by ID\nuser = await User.select({\"id\": 1})\n\n# Select with criteria\nusers = await User.select({\"is_active\": True}, all=True)\n\n# Select first matching record\nfirst_user = await User.select({\"is_active\": True}, first=True)\n\n# Select all records\nall_users = await User.select({}, all=True)\n\n# Select with wildcard pattern matching\ngmail_users = await User.select({\"email\": \"*@gmail.com\"}, all=True)\n\n# Select with ordering\nrecent_users = await User.select({}, order_by=\"-created_at\", all=True)\n\n# Select with limit\nlatest_posts = await Post.select({}, order_by=\"-created_at\", limit=5)\n# Note: limit > 1 automatically sets all=True\n\n# Select with multiple ordering fields\nsorted_users = await User.select({}, order_by=[\"last_name\", \"first_name\"], all=True)\n\n# Select with relationship ordering\nposts_by_author = await Post.select({}, order_by=\"user.username\", all=True)\n```\n\n### Update\n\n```python\n# Update by ID\nuser = await User.update({\"is_active\": False}, 1)\n\n# Update by criteria\ncount = await User.update(\n {\"is_active\": False},\n {\"last_login\": None} # Set all users without login to inactive\n)\n\n# Update with relationships\nawait User.update(\n {\"department\": {\"name\": \"Sales\"}}, # Update department relationship\n {\"username\": \"john_doe\"}\n)\n```\n\n### Delete\n\n```python\n# Delete by ID\nsuccess = await User.delete(1)\n\n# Delete by criteria\ndeleted_count = await User.delete({\"is_active\": False})\n\n# Delete with compound criteria\nawait Post.delete({\"user\": {\"username\": \"john_doe\"}, \"is_published\": False})\n```\n\n## Database Schema Visualization\n\nThe package includes a `ModelVisualizer` class that makes it easy to generate Entity-Relationship (ER) diagrams for your database models using Mermaid syntax.\n\n```python\nfrom async_easy_model import EasyModel, init_db, db_config, ModelVisualizer\n\n# Initialize your models and database\nawait init_db()\n\n# Create a visualizer\nvisualizer = ModelVisualizer()\n\n# Generate a Mermaid ER diagram\ner_diagram = visualizer.mermaid()\nprint(er_diagram)\n\n# Generate a shareable link to view the diagram online\ner_link = visualizer.mermaid_link()\nprint(er_link)\n\n# Customize the diagram title\nvisualizer.set_title(\"My Project Database Schema\")\ncustom_diagram = visualizer.mermaid()\n```\n\n### Example Mermaid ER Diagram Output\n\n```mermaid\n---\ntitle: EasyModel Table Schemas\nconfig:\n layout: elk\n---\nerDiagram\n author {\n number id PK\n string name \"required\"\n string email\n }\n book {\n number id PK\n string title \"required\"\n number author_id FK\n string isbn\n number published_year\n author author \"virtual\"\n tag[] tags \"virtual\"\n }\n tag {\n number id PK\n string name \"required\"\n book[] books \"virtual\"\n }\n book_tag {\n number id PK\n number book_id FK \"required\"\n number tag_id FK \"required\"\n book book \"virtual\"\n tag tag \"virtual\"\n }\n review {\n number id PK\n number book_id FK \"required\"\n number rating \"required\"\n string comment\n string reviewer_name \"required\"\n book book \"virtual\"\n }\n book ||--o{ author : \"author_id\"\n book_tag ||--o{ book : \"book_id\"\n book_tag ||--o{ tag : \"tag_id\"\n book }o--o{ tag : \"many-to-many\"\n review ||--o{ book : \"book_id\"\n```\n\nThe diagram automatically:\n- Shows all tables with their fields and data types\n- Identifies primary keys (PK) and foreign keys (FK)\n- Shows required fields and virtual relationships\n- Visualizes relationships between tables with proper cardinality\n- Properly handles many-to-many relationships\n\n## Convenient Query Methods\n\nasync-easy-model provides simplified methods for common query patterns:\n\n```python\n# Get all records with relationships loaded (default)\nusers = await User.all()\n\n# Get all records ordered by a field (ascending)\nusers = await User.all(order_by=\"username\")\n\n# Get all records ordered by a field (descending)\nnewest_users = await User.all(order_by=\"-created_at\")\n\n# Get all records ordered by multiple fields\nsorted_users = await User.all(order_by=[\"last_name\", \"first_name\"])\n\n# Get all records ordered by relationship fields\nbooks = await Book.all(order_by=\"author.name\")\n\n# Get the first record \nuser = await User.first()\n\n# Get the most recently created user\nnewest_user = await User.first(order_by=\"-created_at\")\n\n# Get a limited number of records\nrecent_users = await User.limit(10)\n\n# Get a limited number of records with ordering\ntop_products = await Product.limit(5, order_by=\"-sales\")\n```\n\n## SQLAlchemy/SQLModel Compatibility Layer\n\n**New in v0.4.3**: async-easy-model now includes a compatibility layer that allows you to use familiar SQLAlchemy query patterns alongside EasyModel's simplified API. This enables smooth migration from existing SQLAlchemy/SQLModel code and provides a familiar interface for developers coming from SQLAlchemy backgrounds.\n\n### Query Builder Pattern\n\nThe compatibility layer provides a `query()` method that returns an AsyncQuery builder, allowing you to chain filter operations just like SQLAlchemy:\n\n```python\nfrom async_easy_model import EasyModel, Field\nfrom async_easy_model.compat import AsyncQuery # For IDE type hints (optional)\n\nclass User(EasyModel, table=True):\n username: str = Field(index=True)\n email: str\n age: int\n is_active: bool = Field(default=True)\n\n# Use the query builder pattern\nusers = await User.query().filter(User.age > 25).all()\nuser = await User.query().filter_by(username=\"john\").first()\n\n# Chain multiple operations\nactive_adults = await (\n User.query()\n .filter(User.age >= 18)\n .filter(User.is_active == True)\n .order_by(User.username)\n .limit(10)\n .all()\n)\n\n# IDE Support Tip (Optional):\n# For full IDE autocomplete, add a type annotation:\nquery: AsyncQuery[User] = User.query() # Optional - only for IDE support\nfiltered = query.filter(User.age > 25) # IDE will show all available methods\n```\n\n### Available Methods\n\nThe compatibility layer provides all common SQLAlchemy patterns:\n\n#### Query Building Methods\n- `filter()` - Filter using SQLAlchemy expressions\n- `filter_by()` - Filter using keyword arguments\n- `order_by()` - Sort results\n- `limit()` - Limit number of results\n- `offset()` - Skip results\n- `join()` - Join related tables\n\n#### Terminal Methods (async)\n- `all()` - Get all matching records\n- `first()` - Get first matching record\n- `one()` - Get exactly one record (raises if not found)\n- `one_or_none()` - Get one record or None\n- `count()` - Count matching records\n- `exists()` - Check if any records match\n\n#### Instance Methods\n```python\nuser = await User.find(1)\nawait user.save() # Save changes to database\nawait user.refresh() # Refresh from database\nawait user.delete_instance() # Delete this record\n```\n\n#### Class Methods\n```python\n# Create new record\nuser = await User.create(username=\"jane\", email=\"jane@example.com\")\n\n# Find records\nuser = await User.find(1) # By ID\nuser = await User.find_by(username=\"john\") # By field\n\n# Bulk operations\nusers = await User.bulk_create([\n {\"username\": \"user1\", \"email\": \"user1@example.com\"},\n {\"username\": \"user2\", \"email\": \"user2@example.com\"}\n])\n\n# Check existence\nexists = await User.exists(username=\"john\")\n\n# Count records\ncount = await User.count()\n```\n\n### SQLAlchemy Statement Builders\n\nFor advanced use cases, you can access SQLAlchemy statement builders:\n\n```python\n# Get SQLAlchemy select statement\nstmt = User.select_stmt().where(User.age > 25)\n\n# Use with session\nasync with User.session() as session:\n result = await session.execute(stmt)\n users = result.scalars().all()\n```\n\n### Mixed Usage Example\n\nYou can freely mix EasyModel's simplified API with SQLAlchemy patterns:\n\n```python\n# EasyModel style\nusers = await User.select({\"age\": {\"$gt\": 25}})\n\n# SQLAlchemy compatibility style\nusers = await User.query().filter(User.age > 25).all()\n\n# Both work seamlessly together!\n```\n\n## Enhanced Relationship Handling\n\nUsing the models defined earlier, here's how to work with relationships:\n\n```python\n# Load all relationships automatically\npost = await Post.select({\"id\": 1})\nprint(post.user.username) # Access related objects directly\n\n# Load specific relationships\npost = await Post.get_with_related(1, [\"user\", \"comments\"])\n\n# Load relationships after fetching\npost = await Post.select({\"id\": 1}, include_relationships=False)\nawait post.load_related([\"user\", \"comments\"])\n\n# Insert with nested relationships\nnew_post = await Post.insert({\n \"title\": \"My Post\",\n \"content\": \"Content here\",\n \"user\": {\"username\": \"jane_doe\"}, # Will automatically link to existing user\n \"comments\": [ # Create multiple comments in a single transaction\n {\"text\": \"Great post!\", \"user\": {\"username\": \"reader1\"}},\n {\"text\": \"Thanks for sharing\", \"user\": {\"username\": \"reader2\"}}\n ]\n})\n# Access nested data without requerying\nprint(f\"Post by {new_post.user.username} with {len(new_post.comments)} comments\")\n\n# Convert to dictionary with nested relationships\npost_dict = post.to_dict(include_relationships=True, max_depth=2)\n```\n\n## Automatic Relationship Detection\n\nThe package can automatically detect and set up bidirectional relationships between models:\n\n```python\nclass User(EasyModel, table=True):\n username: str\n\nclass Post(EasyModel, table=True):\n title: str\n user_id: int = Field(foreign_key=\"user.id\")\n\n# After init_db():\n# - post.user relationship is automatically available\n# - user.posts relationship is automatically available\n```\n\n## Database Configuration\n\n```python\n# SQLite Configuration\ndb_config.configure_sqlite(\"database.db\")\ndb_config.configure_sqlite(\":memory:\") # In-memory database\n\n# PostgreSQL Configuration\ndb_config.configure_postgres(\n user=\"your_user\",\n password=\"your_password\",\n host=\"localhost\",\n port=\"5432\",\n database=\"your_database\"\n)\n\n# MySQL Configuration\ndb_config.configure_mysql(\n user=\"your_user\",\n password=\"your_password\",\n host=\"localhost\",\n port=\"3306\",\n database=\"your_database\"\n)\n\n# Custom Connection URL\ndb_config.set_connection_url(\"postgresql+asyncpg://user:password@localhost:5432/database\")\n```\n\n### Configurable Default for Relationship Loading\n\n**New in v0.3.9**: You can now set a global default for `include_relationships` behavior across all query methods:\n\n```python\n# Configure with default_include_relationships=False for better performance\ndb_config.configure_sqlite(\"database.db\", default_include_relationships=False)\n\n# Or for PostgreSQL\ndb_config.configure_postgres(\n user=\"your_user\",\n password=\"your_password\",\n host=\"localhost\",\n port=\"5432\",\n database=\"your_database\",\n default_include_relationships=False # Set global default\n)\n\n# Or for MySQL\ndb_config.configure_mysql(\n user=\"your_user\",\n password=\"your_password\",\n host=\"localhost\",\n port=\"3306\",\n database=\"your_database\",\n default_include_relationships=False # Set global default\n)\n```\n\n**Benefits:**\n- **Performance**: Set `default_include_relationships=False` to avoid loading relationships by default, improving query performance\n- **Flexibility**: Still override per method call with explicit `True` or `False` values\n- **Backward Compatible**: Defaults to `True` if not specified, maintaining existing behavior\n\n**Usage Examples:**\n```python\n# With default_include_relationships=False configured:\nusers = await User.all() # No relationships loaded (faster)\nusers_with_rels = await User.all(include_relationships=True) # Override to load relationships\n\n# With default_include_relationships=True configured (default behavior):\nusers = await User.all() # Relationships loaded\nusers_no_rels = await User.all(include_relationships=False) # Override to skip relationships\n```\n\n### Database Initialization Options\n\n**New in v0.4.1**: The `init_db()` function now supports configurable auto-relationships handling:\n\n```python\n# Default behavior (auto-detect auto-relationships availability)\nawait init_db()\n\n# Force enable auto-relationships (will warn if not available)\nawait init_db(has_auto_relationships=True)\n\n# Force disable auto-relationships\nawait init_db(has_auto_relationships=False)\n\n# Combined with other parameters\nawait init_db(\n migrate=True,\n model_classes=[User, Post, Comment],\n has_auto_relationships=False\n)\n```\n\n**Benefits:**\n- **Control**: Explicitly enable or disable auto-relationships functionality\n- **Reliability**: Auto-relationships errors now issue warnings instead of stopping database initialization\n- **Flexibility**: Can be combined with migration control and specific model classes\n- **Robust**: Database initialization continues even if auto-relationships fail\n\n**Parameter Priority:**\n1. Explicit `has_auto_relationships` parameter (if provided)\n2. Auto-detection of auto-relationships availability (default)\n3. Fallback to available functionality\n\n## Documentation\n\nFor more detailed documentation, please visit the [GitHub repository](https://github.com/puntorigen/easy-model) or refer to the [DOCS.md](https://github.com/puntorigen/easy-model/blob/master/DOCS.md) file.\n\n## License\n\nThis project is licensed under the MIT License - see the LICENSE file for details.\n",
"bugtrack_url": null,
"license": null,
"summary": "A simplified SQLModel-based ORM for async database operations",
"version": "0.4.3",
"project_urls": {
"Homepage": "https://github.com/puntorigen/easy-model"
},
"split_keywords": [
"orm",
" sqlmodel",
" database",
" async",
" postgresql",
" sqlite",
" mysql"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "5aca9f06bec3f1e1065c957d3b921f8d4f14495d1fdecb55267adaecd2f70bed",
"md5": "9080c5aec2b862ecfa3aee6ad50c5876",
"sha256": "a32c7e168e224979691fbe770b9ca2db5c9b16ec3a8bd2d362d4db0e2d0f6e04"
},
"downloads": -1,
"filename": "async_easy_model-0.4.3-py3-none-any.whl",
"has_sig": false,
"md5_digest": "9080c5aec2b862ecfa3aee6ad50c5876",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7",
"size": 49100,
"upload_time": "2025-08-07T02:31:19",
"upload_time_iso_8601": "2025-08-07T02:31:19.086418Z",
"url": "https://files.pythonhosted.org/packages/5a/ca/9f06bec3f1e1065c957d3b921f8d4f14495d1fdecb55267adaecd2f70bed/async_easy_model-0.4.3-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "e78ba6e3b027d7013fb1e408983e94794a098f0cca919e61d3cbddab2c5255a6",
"md5": "e79a30bfc0d9f671225e50c0ebfeedd1",
"sha256": "60cf015afc05410e11de21db8e8972e3b77c4395cd55183e8a780bee245dc3c4"
},
"downloads": -1,
"filename": "async_easy_model-0.4.3.tar.gz",
"has_sig": false,
"md5_digest": "e79a30bfc0d9f671225e50c0ebfeedd1",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.7",
"size": 51952,
"upload_time": "2025-08-07T02:31:20",
"upload_time_iso_8601": "2025-08-07T02:31:20.670609Z",
"url": "https://files.pythonhosted.org/packages/e7/8b/a6e3b027d7013fb1e408983e94794a098f0cca919e61d3cbddab2c5255a6/async_easy_model-0.4.3.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-08-07 02:31:20",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "puntorigen",
"github_project": "easy-model",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "async-easy-model"
}