async-easy-model 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- async_easy_model/__init__.py +1 -1
- async_easy_model/model.py +54 -54
- async_easy_model-0.2.2.dist-info/METADATA +332 -0
- async_easy_model-0.2.2.dist-info/RECORD +10 -0
- async_easy_model-0.2.1.dist-info/METADATA +0 -343
- async_easy_model-0.2.1.dist-info/RECORD +0 -10
- {async_easy_model-0.2.1.dist-info → async_easy_model-0.2.2.dist-info}/LICENSE +0 -0
- {async_easy_model-0.2.1.dist-info → async_easy_model-0.2.2.dist-info}/WHEEL +0 -0
- {async_easy_model-0.2.1.dist-info → async_easy_model-0.2.2.dist-info}/top_level.txt +0 -0
async_easy_model/__init__.py
CHANGED
@@ -6,7 +6,7 @@ from typing import Optional, Any
|
|
6
6
|
from .model import EasyModel, init_db, db_config
|
7
7
|
from sqlmodel import Field, Relationship as SQLModelRelationship
|
8
8
|
|
9
|
-
__version__ = "0.
|
9
|
+
__version__ = "0.2.2"
|
10
10
|
__all__ = ["EasyModel", "init_db", "db_config", "Field", "Relationship", "Relation", "enable_auto_relationships", "disable_auto_relationships", "process_auto_relationships", "MigrationManager", "check_and_migrate_models"]
|
11
11
|
|
12
12
|
# Create a more user-friendly Relationship function
|
async_easy_model/model.py
CHANGED
@@ -511,6 +511,60 @@ class EasyModel(SQLModel):
|
|
511
511
|
await session.rollback()
|
512
512
|
raise
|
513
513
|
|
514
|
+
@classmethod
|
515
|
+
async def insert_with_related(
|
516
|
+
cls: Type[T],
|
517
|
+
data: Dict[str, Any],
|
518
|
+
related_data: Dict[str, List[Dict[str, Any]]] = None
|
519
|
+
) -> T:
|
520
|
+
"""
|
521
|
+
Create a model instance with related objects in a single transaction.
|
522
|
+
|
523
|
+
Args:
|
524
|
+
data: Dictionary of field values for the main model
|
525
|
+
related_data: Dictionary mapping relationship names to lists of data dictionaries
|
526
|
+
for creating related objects
|
527
|
+
|
528
|
+
Returns:
|
529
|
+
The created model instance with relationships loaded
|
530
|
+
"""
|
531
|
+
if related_data is None:
|
532
|
+
related_data = {}
|
533
|
+
|
534
|
+
async with cls.get_session() as session:
|
535
|
+
# Create the main object
|
536
|
+
obj = cls(**data)
|
537
|
+
session.add(obj)
|
538
|
+
await session.flush() # Flush to get the ID
|
539
|
+
|
540
|
+
# Create related objects
|
541
|
+
for rel_name, items_data in related_data.items():
|
542
|
+
if not hasattr(cls, rel_name):
|
543
|
+
continue
|
544
|
+
|
545
|
+
rel_attr = getattr(cls, rel_name)
|
546
|
+
if not hasattr(rel_attr, "property"):
|
547
|
+
continue
|
548
|
+
|
549
|
+
# Get the related model class and the back reference attribute
|
550
|
+
related_model = rel_attr.property.mapper.class_
|
551
|
+
back_populates = getattr(rel_attr.property, "back_populates", None)
|
552
|
+
|
553
|
+
# Create each related object
|
554
|
+
for item_data in items_data:
|
555
|
+
# Set the back reference if it exists
|
556
|
+
if back_populates:
|
557
|
+
item_data[back_populates] = obj
|
558
|
+
|
559
|
+
related_obj = related_model(**item_data)
|
560
|
+
session.add(related_obj)
|
561
|
+
|
562
|
+
await session.commit()
|
563
|
+
|
564
|
+
# Refresh with relationships
|
565
|
+
await session.refresh(obj, attribute_names=list(related_data.keys()))
|
566
|
+
return obj
|
567
|
+
|
514
568
|
@classmethod
|
515
569
|
def _get_unique_fields(cls) -> List[str]:
|
516
570
|
"""
|
@@ -861,60 +915,6 @@ class EasyModel(SQLModel):
|
|
861
915
|
await session.rollback()
|
862
916
|
raise
|
863
917
|
|
864
|
-
@classmethod
|
865
|
-
async def create_with_related(
|
866
|
-
cls: Type[T],
|
867
|
-
data: Dict[str, Any],
|
868
|
-
related_data: Dict[str, List[Dict[str, Any]]] = None
|
869
|
-
) -> T:
|
870
|
-
"""
|
871
|
-
Create a model instance with related objects in a single transaction.
|
872
|
-
|
873
|
-
Args:
|
874
|
-
data: Dictionary of field values for the main model
|
875
|
-
related_data: Dictionary mapping relationship names to lists of data dictionaries
|
876
|
-
for creating related objects
|
877
|
-
|
878
|
-
Returns:
|
879
|
-
The created model instance with relationships loaded
|
880
|
-
"""
|
881
|
-
if related_data is None:
|
882
|
-
related_data = {}
|
883
|
-
|
884
|
-
async with cls.get_session() as session:
|
885
|
-
# Create the main object
|
886
|
-
obj = cls(**data)
|
887
|
-
session.add(obj)
|
888
|
-
await session.flush() # Flush to get the ID
|
889
|
-
|
890
|
-
# Create related objects
|
891
|
-
for rel_name, items_data in related_data.items():
|
892
|
-
if not hasattr(cls, rel_name):
|
893
|
-
continue
|
894
|
-
|
895
|
-
rel_attr = getattr(cls, rel_name)
|
896
|
-
if not hasattr(rel_attr, "property"):
|
897
|
-
continue
|
898
|
-
|
899
|
-
# Get the related model class and the back reference attribute
|
900
|
-
related_model = rel_attr.property.mapper.class_
|
901
|
-
back_populates = getattr(rel_attr.property, "back_populates", None)
|
902
|
-
|
903
|
-
# Create each related object
|
904
|
-
for item_data in items_data:
|
905
|
-
# Set the back reference if it exists
|
906
|
-
if back_populates:
|
907
|
-
item_data[back_populates] = obj
|
908
|
-
|
909
|
-
related_obj = related_model(**item_data)
|
910
|
-
session.add(related_obj)
|
911
|
-
|
912
|
-
await session.commit()
|
913
|
-
|
914
|
-
# Refresh with relationships
|
915
|
-
await session.refresh(obj, attribute_names=list(related_data.keys()))
|
916
|
-
return obj
|
917
|
-
|
918
918
|
def to_dict(self, include_relationships: bool = True, max_depth: int = 4) -> Dict[str, Any]:
|
919
919
|
"""
|
920
920
|
Convert the model instance to a dictionary.
|
@@ -0,0 +1,332 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: async-easy-model
|
3
|
+
Version: 0.2.2
|
4
|
+
Summary: A simplified SQLModel-based ORM for async database operations
|
5
|
+
Home-page: https://github.com/puntorigen/easy-model
|
6
|
+
Author: Pablo Schaffner
|
7
|
+
Author-email: pablo@puntorigen.com
|
8
|
+
Keywords: orm,sqlmodel,database,async,postgresql,sqlite
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
10
|
+
Classifier: Intended Audience :: Developers
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
19
|
+
Requires-Python: >=3.7
|
20
|
+
Description-Content-Type: text/markdown
|
21
|
+
License-File: LICENSE
|
22
|
+
Requires-Dist: sqlmodel>=0.0.8
|
23
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
24
|
+
Requires-Dist: asyncpg>=0.25.0
|
25
|
+
Requires-Dist: aiosqlite>=0.19.0
|
26
|
+
Requires-Dist: greenlet>=3.1.1
|
27
|
+
Requires-Dist: inflection>=0.5.1
|
28
|
+
Dynamic: author
|
29
|
+
Dynamic: author-email
|
30
|
+
Dynamic: classifier
|
31
|
+
Dynamic: description
|
32
|
+
Dynamic: description-content-type
|
33
|
+
Dynamic: home-page
|
34
|
+
Dynamic: keywords
|
35
|
+
Dynamic: requires-dist
|
36
|
+
Dynamic: requires-python
|
37
|
+
Dynamic: summary
|
38
|
+
|
39
|
+
# async-easy-model
|
40
|
+
|
41
|
+
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.
|
42
|
+
|
43
|
+
<p align="center">
|
44
|
+
<img src="https://img.shields.io/pypi/v/async-easy-model" alt="PyPI Version">
|
45
|
+
<img src="https://img.shields.io/pypi/pyversions/async-easy-model" alt="Python Versions">
|
46
|
+
<img src="https://img.shields.io/github/license/puntorigen/easy_model" alt="License">
|
47
|
+
</p>
|
48
|
+
|
49
|
+
## Features
|
50
|
+
|
51
|
+
- 🚀 Easy-to-use async database operations with standardized methods
|
52
|
+
- 🔄 Intuitive APIs with sensible defaults for rapid development
|
53
|
+
- 📊 Dictionary-based CRUD operations (select, insert, update, delete)
|
54
|
+
- 🔗 Enhanced relationship handling with eager loading and nested operations
|
55
|
+
- 🔍 Powerful query methods with flexible ordering support
|
56
|
+
- ⚙️ Automatic relationship detection and bidirectional setup
|
57
|
+
- 📱 Support for both PostgreSQL and SQLite databases
|
58
|
+
- 🛠️ Built on top of SQLModel and SQLAlchemy for robust performance
|
59
|
+
- 📝 Type hints for better IDE support
|
60
|
+
- 🕒 Automatic `id`, `created_at` and `updated_at` fields provided by default
|
61
|
+
- 🔄 Automatic schema migrations for evolving database models
|
62
|
+
|
63
|
+
## Installation
|
64
|
+
|
65
|
+
```bash
|
66
|
+
pip install async-easy-model
|
67
|
+
```
|
68
|
+
|
69
|
+
## Basic Usage
|
70
|
+
|
71
|
+
```python
|
72
|
+
from async_easy_model import EasyModel, init_db, db_config, Field
|
73
|
+
from typing import Optional
|
74
|
+
from datetime import datetime
|
75
|
+
|
76
|
+
# Configure your database
|
77
|
+
db_config.configure_sqlite("database.db")
|
78
|
+
|
79
|
+
# Define your model
|
80
|
+
class User(EasyModel, table=True):
|
81
|
+
#no need to specify id, created_at or updated_at since EasyModel provides them by default
|
82
|
+
username: str = Field(unique=True)
|
83
|
+
email: str
|
84
|
+
|
85
|
+
# Initialize your database
|
86
|
+
async def setup():
|
87
|
+
await init_db()
|
88
|
+
|
89
|
+
# Use it in your async code
|
90
|
+
async def main():
|
91
|
+
await setup()
|
92
|
+
# Create a new user
|
93
|
+
user = await User.insert({
|
94
|
+
"username": "john_doe",
|
95
|
+
"email": "john@example.com"
|
96
|
+
})
|
97
|
+
|
98
|
+
# Get user ID
|
99
|
+
print(f"New user id: {user.id}")
|
100
|
+
```
|
101
|
+
|
102
|
+
## CRUD Operations
|
103
|
+
|
104
|
+
First, let's define some models that we'll use throughout the examples:
|
105
|
+
|
106
|
+
```python
|
107
|
+
from async_easy_model import EasyModel, Field
|
108
|
+
from typing import Optional, List
|
109
|
+
from datetime import datetime
|
110
|
+
|
111
|
+
class User(EasyModel, table=True):
|
112
|
+
username: str = Field(unique=True)
|
113
|
+
email: str
|
114
|
+
is_active: bool = Field(default=True)
|
115
|
+
|
116
|
+
class Post(EasyModel, table=True):
|
117
|
+
title: str
|
118
|
+
content: str
|
119
|
+
user_id: Optional[int] = Field(default=None, foreign_key="user.id")
|
120
|
+
|
121
|
+
class Comment(EasyModel, table=True):
|
122
|
+
text: str
|
123
|
+
post_id: Optional[int] = Field(default=None, foreign_key="post.id")
|
124
|
+
user_id: Optional[int] = Field(default=None, foreign_key="user.id")
|
125
|
+
|
126
|
+
class Department(EasyModel, table=True):
|
127
|
+
name: str = Field(unique=True)
|
128
|
+
|
129
|
+
class Product(EasyModel, table=True):
|
130
|
+
name: str
|
131
|
+
price: float
|
132
|
+
sales: int = Field(default=0)
|
133
|
+
|
134
|
+
class Book(EasyModel, table=True):
|
135
|
+
title: str
|
136
|
+
author_id: Optional[int] = Field(default=None, foreign_key="author.id")
|
137
|
+
|
138
|
+
class Author(EasyModel, table=True):
|
139
|
+
name: str
|
140
|
+
```
|
141
|
+
|
142
|
+
### Create (Insert)
|
143
|
+
|
144
|
+
```python
|
145
|
+
# Insert a single record
|
146
|
+
user = await User.insert({
|
147
|
+
"username": "john_doe",
|
148
|
+
"email": "john@example.com"
|
149
|
+
})
|
150
|
+
|
151
|
+
# Insert multiple records
|
152
|
+
users = await User.insert([
|
153
|
+
{"username": "user1", "email": "user1@example.com"},
|
154
|
+
{"username": "user2", "email": "user2@example.com"}
|
155
|
+
])
|
156
|
+
|
157
|
+
# Insert with relationships using nested dictionaries
|
158
|
+
post = await Post.insert({
|
159
|
+
"title": "My First Post",
|
160
|
+
"content": "Hello world!",
|
161
|
+
"user": {"username": "john_doe"} # Will automatically link to existing user
|
162
|
+
})
|
163
|
+
```
|
164
|
+
|
165
|
+
### Read (Retrieve)
|
166
|
+
|
167
|
+
```python
|
168
|
+
# Select by ID
|
169
|
+
user = await User.select({"id": 1})
|
170
|
+
|
171
|
+
# Select with criteria
|
172
|
+
users = await User.select({"is_active": True}, all=True)
|
173
|
+
|
174
|
+
# Select first matching record
|
175
|
+
first_user = await User.select({"is_active": True}, first=True)
|
176
|
+
|
177
|
+
# Select all records
|
178
|
+
all_users = await User.select({}, all=True)
|
179
|
+
|
180
|
+
# Select with wildcard pattern matching
|
181
|
+
gmail_users = await User.select({"email": "*@gmail.com"}, all=True)
|
182
|
+
|
183
|
+
# Select with ordering
|
184
|
+
recent_users = await User.select({}, order_by="-created_at", all=True)
|
185
|
+
|
186
|
+
# Select with limit
|
187
|
+
latest_posts = await Post.select({}, order_by="-created_at", limit=5)
|
188
|
+
# Note: limit > 1 automatically sets all=True
|
189
|
+
|
190
|
+
# Select with multiple ordering fields
|
191
|
+
sorted_users = await User.select({}, order_by=["last_name", "first_name"], all=True)
|
192
|
+
|
193
|
+
# Select with relationship ordering
|
194
|
+
posts_by_author = await Post.select({}, order_by="user.username", all=True)
|
195
|
+
```
|
196
|
+
|
197
|
+
### Update
|
198
|
+
|
199
|
+
```python
|
200
|
+
# Update by ID
|
201
|
+
user = await User.update({"is_active": False}, 1)
|
202
|
+
|
203
|
+
# Update by criteria
|
204
|
+
count = await User.update(
|
205
|
+
{"is_active": False},
|
206
|
+
{"last_login": None} # Set all users without login to inactive
|
207
|
+
)
|
208
|
+
|
209
|
+
# Update with relationships
|
210
|
+
await User.update(
|
211
|
+
{"department": {"name": "Sales"}}, # Update department relationship
|
212
|
+
{"username": "john_doe"}
|
213
|
+
)
|
214
|
+
```
|
215
|
+
|
216
|
+
### Delete
|
217
|
+
|
218
|
+
```python
|
219
|
+
# Delete by ID
|
220
|
+
success = await User.delete(1)
|
221
|
+
|
222
|
+
# Delete by criteria
|
223
|
+
deleted_count = await User.delete({"is_active": False})
|
224
|
+
|
225
|
+
# Delete with compound criteria
|
226
|
+
await Post.delete({"user": {"username": "john_doe"}, "is_published": False})
|
227
|
+
```
|
228
|
+
|
229
|
+
## Convenient Query Methods
|
230
|
+
|
231
|
+
async-easy-model provides simplified methods for common query patterns:
|
232
|
+
|
233
|
+
```python
|
234
|
+
# Get all records with relationships loaded (default)
|
235
|
+
users = await User.all()
|
236
|
+
|
237
|
+
# Get all records ordered by a field (ascending)
|
238
|
+
users = await User.all(order_by="username")
|
239
|
+
|
240
|
+
# Get all records ordered by a field (descending)
|
241
|
+
newest_users = await User.all(order_by="-created_at")
|
242
|
+
|
243
|
+
# Get all records ordered by multiple fields
|
244
|
+
sorted_users = await User.all(order_by=["last_name", "first_name"])
|
245
|
+
|
246
|
+
# Get all records ordered by relationship fields
|
247
|
+
books = await Book.all(order_by="author.name")
|
248
|
+
|
249
|
+
# Get the first record
|
250
|
+
user = await User.first()
|
251
|
+
|
252
|
+
# Get the most recently created user
|
253
|
+
newest_user = await User.first(order_by="-created_at")
|
254
|
+
|
255
|
+
# Get a limited number of records
|
256
|
+
recent_users = await User.limit(10)
|
257
|
+
|
258
|
+
# Get a limited number of records with ordering
|
259
|
+
top_products = await Product.limit(5, order_by="-sales")
|
260
|
+
```
|
261
|
+
|
262
|
+
## Enhanced Relationship Handling
|
263
|
+
|
264
|
+
Using the models defined earlier, here's how to work with relationships:
|
265
|
+
|
266
|
+
```python
|
267
|
+
# Load all relationships automatically
|
268
|
+
post = await Post.select({"id": 1}, include_relationships=True)
|
269
|
+
print(post.user.username) # Access related objects directly
|
270
|
+
|
271
|
+
# Load specific relationships
|
272
|
+
post = await Post.get_with_related(1, ["user", "comments"])
|
273
|
+
|
274
|
+
# Load relationships after fetching
|
275
|
+
post = await Post.select({"id": 1})
|
276
|
+
await post.load_related(["user", "comments"])
|
277
|
+
|
278
|
+
# Insert with related objects in a single transaction
|
279
|
+
new_post = await Post.insert_with_related({
|
280
|
+
"title": "My Post",
|
281
|
+
"content": "Content here",
|
282
|
+
"user": {"username": "john_doe"}
|
283
|
+
})
|
284
|
+
|
285
|
+
# Convert to dictionary with nested relationships
|
286
|
+
post_dict = post.to_dict(include_relationships=True, max_depth=2)
|
287
|
+
```
|
288
|
+
|
289
|
+
## Automatic Relationship Detection
|
290
|
+
|
291
|
+
The package can automatically detect and set up bidirectional relationships between models:
|
292
|
+
|
293
|
+
```python
|
294
|
+
class User(EasyModel, table=True):
|
295
|
+
username: str
|
296
|
+
|
297
|
+
class Post(EasyModel, table=True):
|
298
|
+
title: str
|
299
|
+
user_id: int = Field(foreign_key="user.id")
|
300
|
+
|
301
|
+
# After init_db():
|
302
|
+
# - post.user relationship is automatically available
|
303
|
+
# - user.posts relationship is automatically available
|
304
|
+
```
|
305
|
+
|
306
|
+
## Database Configuration
|
307
|
+
|
308
|
+
```python
|
309
|
+
# SQLite Configuration
|
310
|
+
db_config.configure_sqlite("database.db")
|
311
|
+
db_config.configure_sqlite(":memory:") # In-memory database
|
312
|
+
|
313
|
+
# PostgreSQL Configuration
|
314
|
+
db_config.configure_postgres(
|
315
|
+
user="your_user",
|
316
|
+
password="your_password",
|
317
|
+
host="localhost",
|
318
|
+
port="5432",
|
319
|
+
database="your_database"
|
320
|
+
)
|
321
|
+
|
322
|
+
# Custom Connection URL
|
323
|
+
db_config.set_connection_url("postgresql+asyncpg://user:password@localhost:5432/database")
|
324
|
+
```
|
325
|
+
|
326
|
+
## Documentation
|
327
|
+
|
328
|
+
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/main/DOCS.md) file.
|
329
|
+
|
330
|
+
## License
|
331
|
+
|
332
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
@@ -0,0 +1,10 @@
|
|
1
|
+
async_easy_model/__init__.py,sha256=A0U-LtrK9tjK-b94oDNJyc3XFmmnjQAHXP7Bs-FHx18,1642
|
2
|
+
async_easy_model/auto_relationships.py,sha256=VetxcrOdKGGSImTiysRFR8PSOSlo50RqnVG95CLe8Jg,22433
|
3
|
+
async_easy_model/migrations.py,sha256=rYDGCGlruSugAmPfdIF2-uhyG6UvC_2qbF3BXJ084qI,17803
|
4
|
+
async_easy_model/model.py,sha256=TUTTLP47YWCdlsrsyf-7HmMAhPdlvTNZG416pA7UfCo,52958
|
5
|
+
async_easy_model/relationships.py,sha256=vR5BsJpGaDcecCcNlg9-ouZfxFXFQv5kOyiXhKp_T7A,3286
|
6
|
+
async_easy_model-0.2.2.dist-info/LICENSE,sha256=uwDkl6oHbRltW7vYKNc4doJyhtwhyrSNFFlPpKATwLE,1072
|
7
|
+
async_easy_model-0.2.2.dist-info/METADATA,sha256=QcX4beoXLL0fbPJ8ZVMAL4Q5QlXGgJfb4jjSjPx3Yqw,9474
|
8
|
+
async_easy_model-0.2.2.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
9
|
+
async_easy_model-0.2.2.dist-info/top_level.txt,sha256=e5_47sGmJnyxz2msfwU6C316EqmrSd9RGIYwZyWx68E,17
|
10
|
+
async_easy_model-0.2.2.dist-info/RECORD,,
|
@@ -1,343 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.2
|
2
|
-
Name: async-easy-model
|
3
|
-
Version: 0.2.1
|
4
|
-
Summary: A simplified SQLModel-based ORM for async database operations
|
5
|
-
Home-page: https://github.com/puntorigen/easy-model
|
6
|
-
Author: Pablo Schaffner
|
7
|
-
Author-email: pablo@puntorigen.com
|
8
|
-
Keywords: orm,sqlmodel,database,async,postgresql,sqlite
|
9
|
-
Classifier: Development Status :: 3 - Alpha
|
10
|
-
Classifier: Intended Audience :: Developers
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
12
|
-
Classifier: Operating System :: OS Independent
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
14
|
-
Classifier: Programming Language :: Python :: 3.7
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
19
|
-
Requires-Python: >=3.7
|
20
|
-
Description-Content-Type: text/markdown
|
21
|
-
License-File: LICENSE
|
22
|
-
Requires-Dist: sqlmodel>=0.0.8
|
23
|
-
Requires-Dist: sqlalchemy>=2.0.0
|
24
|
-
Requires-Dist: asyncpg>=0.25.0
|
25
|
-
Requires-Dist: aiosqlite>=0.19.0
|
26
|
-
Requires-Dist: greenlet>=3.1.1
|
27
|
-
Requires-Dist: inflection>=0.5.1
|
28
|
-
Dynamic: author
|
29
|
-
Dynamic: author-email
|
30
|
-
Dynamic: classifier
|
31
|
-
Dynamic: description
|
32
|
-
Dynamic: description-content-type
|
33
|
-
Dynamic: home-page
|
34
|
-
Dynamic: keywords
|
35
|
-
Dynamic: requires-dist
|
36
|
-
Dynamic: requires-python
|
37
|
-
Dynamic: summary
|
38
|
-
|
39
|
-
# EasyModel
|
40
|
-
|
41
|
-
A simplified SQLModel-based ORM for async database operations in Python. EasyModel provides a clean and intuitive interface for common database operations while leveraging the power of SQLModel and SQLAlchemy.
|
42
|
-
|
43
|
-
## Features
|
44
|
-
|
45
|
-
- Easy-to-use async database operations with standardized methods
|
46
|
-
- Intuitive APIs with sensible defaults (relationships loaded by default)
|
47
|
-
- Dictionary-based CRUD operations (select, insert, update, delete)
|
48
|
-
- Built on top of SQLModel and SQLAlchemy
|
49
|
-
- Support for both PostgreSQL and SQLite databases
|
50
|
-
- Type hints for better IDE support
|
51
|
-
- Automatic `id`, `created_at` and `updated_at` fields provided by default
|
52
|
-
- Enhanced relationship handling with eager loading and nested operations
|
53
|
-
- Flexible ordering of query results with support for relationship fields
|
54
|
-
- Automatic relationship detection
|
55
|
-
- Automatic schema migrations for evolving database models
|
56
|
-
|
57
|
-
## Installation
|
58
|
-
|
59
|
-
```bash
|
60
|
-
pip install async-easy-model
|
61
|
-
```
|
62
|
-
|
63
|
-
## Quick Start with Standardized API
|
64
|
-
|
65
|
-
This section demonstrates the preferred usage of EasyModel with its standardized API methods.
|
66
|
-
|
67
|
-
```python
|
68
|
-
from async_easy_model import EasyModel, init_db, db_config, Field
|
69
|
-
from typing import Optional
|
70
|
-
from datetime import datetime
|
71
|
-
|
72
|
-
# Configure your database
|
73
|
-
db_config.configure_sqlite("database.db")
|
74
|
-
|
75
|
-
# Define your model
|
76
|
-
class User(EasyModel, table=True):
|
77
|
-
# id field is automatically created (primary key)
|
78
|
-
username: str = Field(unique=True)
|
79
|
-
email: str
|
80
|
-
is_active: bool = Field(default=True)
|
81
|
-
# created_at and updated_at fields are automatically included
|
82
|
-
|
83
|
-
class Post(EasyModel, table=True):
|
84
|
-
# id field is automatically created (primary key)
|
85
|
-
title: str
|
86
|
-
content: str
|
87
|
-
user_id: Optional[int] = Field(default=None, foreign_key="user.id")
|
88
|
-
# created_at and updated_at fields are automatically included
|
89
|
-
|
90
|
-
# Initialize your database
|
91
|
-
async def setup():
|
92
|
-
await init_db()
|
93
|
-
|
94
|
-
# Use the standardized methods in your code
|
95
|
-
async def main():
|
96
|
-
# Insert a new user
|
97
|
-
user = await User.insert({
|
98
|
-
"username": "john_doe",
|
99
|
-
"email": "john@example.com"
|
100
|
-
})
|
101
|
-
|
102
|
-
# Insert with relationships
|
103
|
-
post = await Post.insert({
|
104
|
-
"title": "My First Post",
|
105
|
-
"content": "Hello world!",
|
106
|
-
"user": {"username": "john_doe"} # Will find the user by username
|
107
|
-
})
|
108
|
-
|
109
|
-
# Select with criteria
|
110
|
-
active_users = await User.select({"is_active": True}, all=True)
|
111
|
-
|
112
|
-
# Select with wildcard search
|
113
|
-
gmail_users = await User.select({"email": "*@gmail.com"}, all=True)
|
114
|
-
|
115
|
-
# Select with ordering and limit
|
116
|
-
recent_posts = await Post.select({}, order_by="-id", limit=5)
|
117
|
-
# Note: limit > 1 automatically sets all=True
|
118
|
-
|
119
|
-
# Update by criteria
|
120
|
-
await User.update(
|
121
|
-
{"is_active": False},
|
122
|
-
{"last_login": None} # Update users with no login
|
123
|
-
)
|
124
|
-
|
125
|
-
# Delete with criteria
|
126
|
-
await Post.delete({"user": {"username": "john_doe"}})
|
127
|
-
```
|
128
|
-
|
129
|
-
## Standardized API Methods
|
130
|
-
|
131
|
-
EasyModel provides a set of standardized methods that make it easy and intuitive to perform common database operations.
|
132
|
-
|
133
|
-
### Select Method
|
134
|
-
|
135
|
-
The `select()` method is a powerful and flexible way to query data:
|
136
|
-
|
137
|
-
```python
|
138
|
-
# Get active users
|
139
|
-
active_users = await User.select({"is_active": True}, all=True)
|
140
|
-
|
141
|
-
# Get single user by username (returns first match)
|
142
|
-
user = await User.select({"username": "john_doe"})
|
143
|
-
|
144
|
-
# Explicitly get first result
|
145
|
-
first_admin = await User.select({"role": "admin"}, first=True)
|
146
|
-
|
147
|
-
# With wildcard pattern matching
|
148
|
-
gmail_users = await User.select({"email": "*@gmail.com"}, all=True)
|
149
|
-
|
150
|
-
# With ordering and limit (automatically sets all=True)
|
151
|
-
newest_users = await User.select({}, order_by="-created_at", limit=5)
|
152
|
-
|
153
|
-
# With ordering by multiple fields
|
154
|
-
sorted_users = await User.select({}, order_by=["last_name", "first_name"], all=True)
|
155
|
-
|
156
|
-
# With ordering by nested relationship fields using dot notation
|
157
|
-
books_by_author = await Book.select({}, order_by="author.name", all=True)
|
158
|
-
posts_by_popularity = await Post.select({}, order_by=["-comments.count", "title"], all=True)
|
159
|
-
```
|
160
|
-
|
161
|
-
### Insert Method
|
162
|
-
|
163
|
-
The `insert()` method supports both single and multiple records with relationship handling and returns the newly created records with assigned IDs and auto-generated fields (`created_at`, `updated_at`):
|
164
|
-
|
165
|
-
```python
|
166
|
-
# Insert single record
|
167
|
-
user = await User.insert({
|
168
|
-
"username": "john_doe",
|
169
|
-
"email": "john@example.com"
|
170
|
-
})
|
171
|
-
print(user.id) # Newly assigned ID is available
|
172
|
-
print(user.created_at) # Auto-generated timestamp is available
|
173
|
-
|
174
|
-
# Insert with relationship
|
175
|
-
comment = await Comment.insert({
|
176
|
-
"text": "Great post!",
|
177
|
-
"post": {"id": 1}, # Link by ID
|
178
|
-
"author": {"username": "jane_doe"} # Link by attribute lookup
|
179
|
-
})
|
180
|
-
|
181
|
-
# Insert multiple records
|
182
|
-
products = await Product.insert([
|
183
|
-
{"name": "Product 1", "price": 10.99},
|
184
|
-
{"name": "Product 2", "price": 24.99}
|
185
|
-
])
|
186
|
-
```
|
187
|
-
|
188
|
-
### Update Method
|
189
|
-
|
190
|
-
The `update()` method allows updates based on ID or criteria:
|
191
|
-
|
192
|
-
```python
|
193
|
-
# Update by ID
|
194
|
-
user = await User.update({"email": "new@example.com"}, 1)
|
195
|
-
|
196
|
-
# Update by criteria
|
197
|
-
count = await User.update(
|
198
|
-
{"is_active": False},
|
199
|
-
{"last_login": None} # Set all users without login to inactive
|
200
|
-
)
|
201
|
-
|
202
|
-
# Update with relationships
|
203
|
-
await User.update(
|
204
|
-
{"department": {"name": "Sales"}}, # Update department relationship
|
205
|
-
{"username": "john_doe"}
|
206
|
-
)
|
207
|
-
```
|
208
|
-
|
209
|
-
### Delete Method
|
210
|
-
|
211
|
-
The `delete()` method provides a consistent way to delete records:
|
212
|
-
|
213
|
-
```python
|
214
|
-
# Delete by ID
|
215
|
-
success = await User.delete(1)
|
216
|
-
|
217
|
-
# Delete by criteria
|
218
|
-
deleted_count = await User.delete({"is_active": False})
|
219
|
-
|
220
|
-
# Delete with compound criteria
|
221
|
-
await Post.delete({"author": {"username": "john_doe"}, "is_published": False})
|
222
|
-
```
|
223
|
-
|
224
|
-
## Convenience Query Methods
|
225
|
-
|
226
|
-
EasyModel also provides convenient shorthand methods for common queries:
|
227
|
-
|
228
|
-
```python
|
229
|
-
# Get all records with relationships loaded (default)
|
230
|
-
users = await User.all()
|
231
|
-
|
232
|
-
# Get all records ordered by a field
|
233
|
-
users = await User.all(order_by="username")
|
234
|
-
|
235
|
-
# Get the first record
|
236
|
-
user = await User.first()
|
237
|
-
|
238
|
-
# Get the most recently created user
|
239
|
-
newest_user = await User.first(order_by="-created_at")
|
240
|
-
|
241
|
-
# Get limited records
|
242
|
-
recent_users = await User.limit(10, order_by="-created_at")
|
243
|
-
```
|
244
|
-
|
245
|
-
## Automatic Relationship Detection
|
246
|
-
|
247
|
-
EasyModel supports automatic relationship detection based on foreign key fields:
|
248
|
-
|
249
|
-
```python
|
250
|
-
from async_easy_model import enable_auto_relationships, EasyModel, init_db, Field
|
251
|
-
from typing import Optional
|
252
|
-
|
253
|
-
# Enable automatic relationship detection
|
254
|
-
enable_auto_relationships()
|
255
|
-
|
256
|
-
# Define models with foreign keys but without explicit relationships
|
257
|
-
class Author(EasyModel, table=True):
|
258
|
-
# id field is automatically created (primary key)
|
259
|
-
name: str
|
260
|
-
|
261
|
-
class Book(EasyModel, table=True):
|
262
|
-
title: str
|
263
|
-
author_id: Optional[int] = Field(default=None, foreign_key="author.id")
|
264
|
-
# No need to define Relationship attributes - they're detected automatically!
|
265
|
-
|
266
|
-
# Use the automatically detected relationships
|
267
|
-
async def main():
|
268
|
-
await init_db()
|
269
|
-
author = await Author.insert({"name": "Jane Author"})
|
270
|
-
|
271
|
-
book = await Book.insert({
|
272
|
-
"title": "Auto-detected Relationships",
|
273
|
-
"author_id": author.id
|
274
|
-
})
|
275
|
-
|
276
|
-
# Show the book with its author
|
277
|
-
print(f"Book: {book.title}, Author: {book.author.name}")
|
278
|
-
```
|
279
|
-
|
280
|
-
### Another Example
|
281
|
-
```python
|
282
|
-
# Using the standard insert with nested dictionaries (recommended)
|
283
|
-
new_book = await Book.insert({
|
284
|
-
"title": "New Book",
|
285
|
-
"author": {"name": "Jane Author"} # Will create or find the author
|
286
|
-
})
|
287
|
-
```
|
288
|
-
|
289
|
-
## Automatic Schema Migrations
|
290
|
-
|
291
|
-
EasyModel includes automatic database migration capabilities, similar to alembic:
|
292
|
-
|
293
|
-
```python
|
294
|
-
from async_easy_model import MigrationManager
|
295
|
-
|
296
|
-
async def apply_migrations():
|
297
|
-
migration_manager = MigrationManager()
|
298
|
-
results = await migration_manager.migrate_models([User, Post])
|
299
|
-
|
300
|
-
if results:
|
301
|
-
print("Migrations applied:")
|
302
|
-
for model_name, changes in results.items():
|
303
|
-
print(f" {model_name}:")
|
304
|
-
for change in changes:
|
305
|
-
print(f" - {change}")
|
306
|
-
```
|
307
|
-
|
308
|
-
## Legacy API Methods
|
309
|
-
|
310
|
-
The following methods are still supported but the standardized methods above are recommended for new code:
|
311
|
-
|
312
|
-
### Traditional CRUD Operations
|
313
|
-
|
314
|
-
```python
|
315
|
-
# Create a record (consider using insert() instead)
|
316
|
-
user = User(username="john_doe", email="john@example.com")
|
317
|
-
await user.save()
|
318
|
-
|
319
|
-
# Get by ID (consider using select() instead)
|
320
|
-
user = await User.get_by_id(1)
|
321
|
-
|
322
|
-
# Get by attribute (consider using select() instead)
|
323
|
-
users = await User.get_by_attribute(is_active=True, all=True)
|
324
|
-
|
325
|
-
# Update by ID (consider using update() instead)
|
326
|
-
updated_user = await User.update_by_id(1, {"email": "new_email@example.com"})
|
327
|
-
|
328
|
-
# Update by attribute (consider using update() instead)
|
329
|
-
await User.update_by_attribute(
|
330
|
-
{"is_active": False}, # Update data
|
331
|
-
is_active=True, role="guest" # Filter criteria
|
332
|
-
)
|
333
|
-
|
334
|
-
# Delete by ID (consider using delete() instead)
|
335
|
-
success = await User.delete_by_id(1)
|
336
|
-
|
337
|
-
# Delete by attribute (consider using delete() instead)
|
338
|
-
deleted_count = await User.delete_by_attribute(is_active=False)
|
339
|
-
```
|
340
|
-
|
341
|
-
## Complete Documentation
|
342
|
-
|
343
|
-
For complete documentation, including advanced features, please see the [full documentation](DOCS.md).
|
@@ -1,10 +0,0 @@
|
|
1
|
-
async_easy_model/__init__.py,sha256=JcNQU_W8DUFnGVoOkbwfLDz5akT6jjMNVYLzWy_HA6w,1643
|
2
|
-
async_easy_model/auto_relationships.py,sha256=VetxcrOdKGGSImTiysRFR8PSOSlo50RqnVG95CLe8Jg,22433
|
3
|
-
async_easy_model/migrations.py,sha256=rYDGCGlruSugAmPfdIF2-uhyG6UvC_2qbF3BXJ084qI,17803
|
4
|
-
async_easy_model/model.py,sha256=D8nXgiYGu1WdNGciVSESyH4hmYmEx2_Rs6pBQQpyEA8,52958
|
5
|
-
async_easy_model/relationships.py,sha256=vR5BsJpGaDcecCcNlg9-ouZfxFXFQv5kOyiXhKp_T7A,3286
|
6
|
-
async_easy_model-0.2.1.dist-info/LICENSE,sha256=uwDkl6oHbRltW7vYKNc4doJyhtwhyrSNFFlPpKATwLE,1072
|
7
|
-
async_easy_model-0.2.1.dist-info/METADATA,sha256=RrlZpVzUvAO8RWPMH7g9w2DUKSyjsXqxfKsPzaOs6QM,10420
|
8
|
-
async_easy_model-0.2.1.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
9
|
-
async_easy_model-0.2.1.dist-info/top_level.txt,sha256=e5_47sGmJnyxz2msfwU6C316EqmrSd9RGIYwZyWx68E,17
|
10
|
-
async_easy_model-0.2.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|