async-easy-model 0.2.2__tar.gz → 0.2.4__tar.gz
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-0.2.2 → async_easy_model-0.2.4}/PKG-INFO +42 -12
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/README.md +41 -11
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/async_easy_model/__init__.py +1 -1
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/async_easy_model/auto_relationships.py +132 -2
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/async_easy_model/model.py +683 -436
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/async_easy_model.egg-info/PKG-INFO +42 -12
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/setup.py +1 -1
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/LICENSE +0 -0
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/async_easy_model/migrations.py +0 -0
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/async_easy_model/relationships.py +0 -0
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/async_easy_model.egg-info/SOURCES.txt +0 -0
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/async_easy_model.egg-info/dependency_links.txt +0 -0
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/async_easy_model.egg-info/requires.txt +0 -0
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/async_easy_model.egg-info/top_level.txt +0 -0
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/setup.cfg +0 -0
- {async_easy_model-0.2.2 → async_easy_model-0.2.4}/tests/test_easy_model.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: async-easy-model
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.4
|
4
4
|
Summary: A simplified SQLModel-based ORM for async database operations
|
5
5
|
Home-page: https://github.com/puntorigen/easy-model
|
6
6
|
Author: Pablo Schaffner
|
@@ -154,12 +154,36 @@ users = await User.insert([
|
|
154
154
|
{"username": "user2", "email": "user2@example.com"}
|
155
155
|
])
|
156
156
|
|
157
|
-
# Insert with
|
158
|
-
|
159
|
-
"title": "My
|
160
|
-
"content": "
|
161
|
-
"user": {"username": "
|
157
|
+
# Insert with nested relationships
|
158
|
+
new_post = await Post.insert({
|
159
|
+
"title": "My Post",
|
160
|
+
"content": "Content here",
|
161
|
+
"user": {"username": "jane_doe"}, # Will automatically link to existing user
|
162
|
+
"comments": [ # Create multiple comments in a single transaction
|
163
|
+
{"text": "Great post!", "user": {"username": "reader1"}},
|
164
|
+
{"text": "Thanks for sharing", "user": {"username": "reader2"}}
|
165
|
+
]
|
166
|
+
})
|
167
|
+
# Access nested data without requerying
|
168
|
+
print(f"Post by {new_post.user.username} with {len(new_post.comments)} comments")
|
169
|
+
|
170
|
+
# Insert with nested one-to-many relationships
|
171
|
+
publisher = await Publisher.insert({
|
172
|
+
"name": "Example Publisher",
|
173
|
+
"books": [ # List of nested objects
|
174
|
+
{
|
175
|
+
"title": "Python Mastery",
|
176
|
+
"genres": [
|
177
|
+
{"name": "Programming"},
|
178
|
+
{"name": "Education"}
|
179
|
+
]
|
180
|
+
},
|
181
|
+
{"title": "Data Science Handbook"}
|
182
|
+
]
|
162
183
|
})
|
184
|
+
# Access nested relationships immediately
|
185
|
+
print(f"Publisher: {publisher.name} with {len(publisher.books)} books")
|
186
|
+
print(f"First book genres: {[g.name for g in publisher.books[0].genres]}")
|
163
187
|
```
|
164
188
|
|
165
189
|
### Read (Retrieve)
|
@@ -265,22 +289,28 @@ Using the models defined earlier, here's how to work with relationships:
|
|
265
289
|
|
266
290
|
```python
|
267
291
|
# Load all relationships automatically
|
268
|
-
post = await Post.select({"id": 1}
|
292
|
+
post = await Post.select({"id": 1})
|
269
293
|
print(post.user.username) # Access related objects directly
|
270
294
|
|
271
295
|
# Load specific relationships
|
272
296
|
post = await Post.get_with_related(1, ["user", "comments"])
|
273
297
|
|
274
298
|
# Load relationships after fetching
|
275
|
-
post = await Post.select({"id": 1})
|
299
|
+
post = await Post.select({"id": 1}, include_relationships=False)
|
276
300
|
await post.load_related(["user", "comments"])
|
277
301
|
|
278
|
-
# Insert with
|
279
|
-
new_post = await Post.
|
302
|
+
# Insert with nested relationships
|
303
|
+
new_post = await Post.insert({
|
280
304
|
"title": "My Post",
|
281
305
|
"content": "Content here",
|
282
|
-
"user": {"username": "
|
306
|
+
"user": {"username": "jane_doe"}, # Will automatically link to existing user
|
307
|
+
"comments": [ # Create multiple comments in a single transaction
|
308
|
+
{"text": "Great post!", "user": {"username": "reader1"}},
|
309
|
+
{"text": "Thanks for sharing", "user": {"username": "reader2"}}
|
310
|
+
]
|
283
311
|
})
|
312
|
+
# Access nested data without requerying
|
313
|
+
print(f"Post by {new_post.user.username} with {len(new_post.comments)} comments")
|
284
314
|
|
285
315
|
# Convert to dictionary with nested relationships
|
286
316
|
post_dict = post.to_dict(include_relationships=True, max_depth=2)
|
@@ -325,7 +355,7 @@ db_config.set_connection_url("postgresql+asyncpg://user:password@localhost:5432/
|
|
325
355
|
|
326
356
|
## Documentation
|
327
357
|
|
328
|
-
For more detailed documentation, please visit the [GitHub repository](https://github.com/puntorigen/
|
358
|
+
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.
|
329
359
|
|
330
360
|
## License
|
331
361
|
|
@@ -116,12 +116,36 @@ users = await User.insert([
|
|
116
116
|
{"username": "user2", "email": "user2@example.com"}
|
117
117
|
])
|
118
118
|
|
119
|
-
# Insert with
|
120
|
-
|
121
|
-
"title": "My
|
122
|
-
"content": "
|
123
|
-
"user": {"username": "
|
119
|
+
# Insert with nested relationships
|
120
|
+
new_post = await Post.insert({
|
121
|
+
"title": "My Post",
|
122
|
+
"content": "Content here",
|
123
|
+
"user": {"username": "jane_doe"}, # Will automatically link to existing user
|
124
|
+
"comments": [ # Create multiple comments in a single transaction
|
125
|
+
{"text": "Great post!", "user": {"username": "reader1"}},
|
126
|
+
{"text": "Thanks for sharing", "user": {"username": "reader2"}}
|
127
|
+
]
|
128
|
+
})
|
129
|
+
# Access nested data without requerying
|
130
|
+
print(f"Post by {new_post.user.username} with {len(new_post.comments)} comments")
|
131
|
+
|
132
|
+
# Insert with nested one-to-many relationships
|
133
|
+
publisher = await Publisher.insert({
|
134
|
+
"name": "Example Publisher",
|
135
|
+
"books": [ # List of nested objects
|
136
|
+
{
|
137
|
+
"title": "Python Mastery",
|
138
|
+
"genres": [
|
139
|
+
{"name": "Programming"},
|
140
|
+
{"name": "Education"}
|
141
|
+
]
|
142
|
+
},
|
143
|
+
{"title": "Data Science Handbook"}
|
144
|
+
]
|
124
145
|
})
|
146
|
+
# Access nested relationships immediately
|
147
|
+
print(f"Publisher: {publisher.name} with {len(publisher.books)} books")
|
148
|
+
print(f"First book genres: {[g.name for g in publisher.books[0].genres]}")
|
125
149
|
```
|
126
150
|
|
127
151
|
### Read (Retrieve)
|
@@ -227,22 +251,28 @@ Using the models defined earlier, here's how to work with relationships:
|
|
227
251
|
|
228
252
|
```python
|
229
253
|
# Load all relationships automatically
|
230
|
-
post = await Post.select({"id": 1}
|
254
|
+
post = await Post.select({"id": 1})
|
231
255
|
print(post.user.username) # Access related objects directly
|
232
256
|
|
233
257
|
# Load specific relationships
|
234
258
|
post = await Post.get_with_related(1, ["user", "comments"])
|
235
259
|
|
236
260
|
# Load relationships after fetching
|
237
|
-
post = await Post.select({"id": 1})
|
261
|
+
post = await Post.select({"id": 1}, include_relationships=False)
|
238
262
|
await post.load_related(["user", "comments"])
|
239
263
|
|
240
|
-
# Insert with
|
241
|
-
new_post = await Post.
|
264
|
+
# Insert with nested relationships
|
265
|
+
new_post = await Post.insert({
|
242
266
|
"title": "My Post",
|
243
267
|
"content": "Content here",
|
244
|
-
"user": {"username": "
|
268
|
+
"user": {"username": "jane_doe"}, # Will automatically link to existing user
|
269
|
+
"comments": [ # Create multiple comments in a single transaction
|
270
|
+
{"text": "Great post!", "user": {"username": "reader1"}},
|
271
|
+
{"text": "Thanks for sharing", "user": {"username": "reader2"}}
|
272
|
+
]
|
245
273
|
})
|
274
|
+
# Access nested data without requerying
|
275
|
+
print(f"Post by {new_post.user.username} with {len(new_post.comments)} comments")
|
246
276
|
|
247
277
|
# Convert to dictionary with nested relationships
|
248
278
|
post_dict = post.to_dict(include_relationships=True, max_depth=2)
|
@@ -287,7 +317,7 @@ db_config.set_connection_url("postgresql+asyncpg://user:password@localhost:5432/
|
|
287
317
|
|
288
318
|
## Documentation
|
289
319
|
|
290
|
-
For more detailed documentation, please visit the [GitHub repository](https://github.com/puntorigen/
|
320
|
+
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.
|
291
321
|
|
292
322
|
## License
|
293
323
|
|
@@ -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.2.
|
9
|
+
__version__ = "0.2.4"
|
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
|
@@ -178,7 +178,8 @@ def setup_relationship_on_class(
|
|
178
178
|
relationship_name: str,
|
179
179
|
target_cls: Type[SQLModel],
|
180
180
|
back_populates: str,
|
181
|
-
is_list: bool = False
|
181
|
+
is_list: bool = False,
|
182
|
+
through_model: Optional[Type[SQLModel]] = None
|
182
183
|
) -> None:
|
183
184
|
"""
|
184
185
|
Properly set up a relationship on a SQLModel class that will be recognized by SQLModel.
|
@@ -198,6 +199,9 @@ def setup_relationship_on_class(
|
|
198
199
|
'back_populates': back_populates
|
199
200
|
}
|
200
201
|
|
202
|
+
if through_model:
|
203
|
+
rel_args['secondary'] = through_model.__tablename__
|
204
|
+
|
201
205
|
# Create the SQLAlchemy relationship
|
202
206
|
rel_prop = sa_relationship(
|
203
207
|
target_cls.__name__,
|
@@ -339,18 +343,26 @@ def process_all_models_for_relationships():
|
|
339
343
|
This function:
|
340
344
|
1. Looks for foreign keys in all registered models
|
341
345
|
2. Sets up relationships automatically based on those foreign keys
|
346
|
+
3. Detects junction tables and sets up many-to-many relationships
|
342
347
|
"""
|
343
348
|
logger.info(f"Processing relationships for all registered models: {list(_model_registry.keys())}")
|
344
349
|
|
345
350
|
# First, gather all foreign keys for all models
|
346
351
|
foreign_keys_map = {}
|
352
|
+
junction_models = []
|
353
|
+
|
347
354
|
for model_name, model_cls in _model_registry.items():
|
348
355
|
logger.info(f"Processing model: {model_name}")
|
349
356
|
foreign_keys = get_foreign_keys_from_model(model_cls)
|
350
357
|
if foreign_keys:
|
351
358
|
foreign_keys_map[model_name] = (model_cls, foreign_keys)
|
352
359
|
|
353
|
-
|
360
|
+
# Check if this model represents a junction table
|
361
|
+
if is_junction_table(model_cls):
|
362
|
+
logger.info(f"Detected junction table: {model_name}")
|
363
|
+
junction_models.append(model_cls)
|
364
|
+
|
365
|
+
# Next, set up direct relationships using those foreign keys
|
354
366
|
for model_name, (model_cls, foreign_keys) in foreign_keys_map.items():
|
355
367
|
for field_name, target_fk in foreign_keys.items():
|
356
368
|
# Parse target table and field
|
@@ -366,6 +378,11 @@ def process_all_models_for_relationships():
|
|
366
378
|
else:
|
367
379
|
logger.warning(f"Target model not found for {target_table}")
|
368
380
|
|
381
|
+
# Finally, set up many-to-many relationships
|
382
|
+
for junction_model in junction_models:
|
383
|
+
logger.info(f"Processing junction table for many-to-many relationships: {junction_model.__name__}")
|
384
|
+
setup_many_to_many_relationships(junction_model)
|
385
|
+
|
369
386
|
logger.info("Finished processing relationships")
|
370
387
|
|
371
388
|
# Alias for backwards compatibility
|
@@ -542,3 +559,116 @@ def setup_auto_relationships_for_model(model_cls):
|
|
542
559
|
logger.error(f"Error setting up relationship: {e}")
|
543
560
|
else:
|
544
561
|
logger.warning(f"Target model not found for {target_table}")
|
562
|
+
|
563
|
+
def is_junction_table(model_cls: Type[SQLModel]) -> bool:
|
564
|
+
"""
|
565
|
+
Determine if a model represents a junction table (many-to-many relationship).
|
566
|
+
|
567
|
+
A junction table typically has:
|
568
|
+
- Only foreign key fields (plus perhaps id, created_at, etc.)
|
569
|
+
- Exactly two foreign keys pointing to different tables
|
570
|
+
|
571
|
+
Args:
|
572
|
+
model_cls: The model class to check
|
573
|
+
|
574
|
+
Returns:
|
575
|
+
True if the model appears to be a junction table, False otherwise
|
576
|
+
"""
|
577
|
+
foreign_keys = get_foreign_keys_from_model(model_cls)
|
578
|
+
|
579
|
+
# A junction table should have at least two foreign keys
|
580
|
+
if len(foreign_keys) < 2:
|
581
|
+
return False
|
582
|
+
|
583
|
+
# Get the tables referenced by the foreign keys
|
584
|
+
referenced_tables = set()
|
585
|
+
for field_name, target_fk in foreign_keys.items():
|
586
|
+
target_table = get_related_model_name_from_foreign_key(target_fk)
|
587
|
+
referenced_tables.add(target_table)
|
588
|
+
|
589
|
+
# A true junction table should reference at least two different tables
|
590
|
+
# (some junction tables might have multiple FKs to the same table)
|
591
|
+
if len(referenced_tables) < 2:
|
592
|
+
return False
|
593
|
+
|
594
|
+
# Check if all non-standard fields are foreign keys
|
595
|
+
standard_fields = {'id', 'created_at', 'updated_at'}
|
596
|
+
model_fields = set(getattr(model_cls, '__fields__', {}).keys())
|
597
|
+
non_standard_fields = model_fields - standard_fields
|
598
|
+
foreign_key_fields = set(foreign_keys.keys())
|
599
|
+
|
600
|
+
# Allow for a few extra fields beyond the foreign keys
|
601
|
+
# Some junction tables might have additional metadata
|
602
|
+
non_fk_fields = non_standard_fields - foreign_key_fields
|
603
|
+
return len(non_fk_fields) <= 2 # Allow for up to 2 additional fields
|
604
|
+
|
605
|
+
def setup_many_to_many_relationships(junction_model: Type[SQLModel]) -> None:
|
606
|
+
"""
|
607
|
+
Set up many-to-many relationships using a junction table.
|
608
|
+
|
609
|
+
This creates relationships between the two entities connected by the junction table.
|
610
|
+
For example, if we have Book, Tag, and BookTag, this would set up:
|
611
|
+
- Book.tags -> List[Tag]
|
612
|
+
- Tag.books -> List[Book]
|
613
|
+
|
614
|
+
Args:
|
615
|
+
junction_model: The junction model class (e.g., BookTag)
|
616
|
+
"""
|
617
|
+
logger.info(f"Setting up many-to-many relationships for junction table: {junction_model.__name__}")
|
618
|
+
|
619
|
+
# Get the foreign keys from the junction model
|
620
|
+
foreign_keys = get_foreign_keys_from_model(junction_model)
|
621
|
+
if len(foreign_keys) < 2:
|
622
|
+
logger.warning(f"Junction table {junction_model.__name__} has fewer than 2 foreign keys")
|
623
|
+
return
|
624
|
+
|
625
|
+
# Get the models referenced by the foreign keys
|
626
|
+
referenced_models = []
|
627
|
+
for field_name, target_fk in foreign_keys.items():
|
628
|
+
target_table = get_related_model_name_from_foreign_key(target_fk)
|
629
|
+
target_model = get_model_by_table_name(target_table)
|
630
|
+
if target_model:
|
631
|
+
referenced_models.append((field_name, target_model))
|
632
|
+
else:
|
633
|
+
logger.warning(f"Could not find model for table {target_table}")
|
634
|
+
|
635
|
+
# We need at least two different models for a many-to-many relationship
|
636
|
+
if len(referenced_models) < 2:
|
637
|
+
logger.warning(f"Junction table {junction_model.__name__} references fewer than 2 valid models")
|
638
|
+
return
|
639
|
+
|
640
|
+
# For each pair of models, set up the many-to-many relationship
|
641
|
+
# For simplicity, we'll just use the first two models found
|
642
|
+
model_a_field, model_a = referenced_models[0]
|
643
|
+
model_b_field, model_b = referenced_models[1]
|
644
|
+
|
645
|
+
# Determine relationship names
|
646
|
+
# For model_a -> model_b relationship (e.g., Book.tags)
|
647
|
+
model_a_to_b_name = pluralize_name(model_b.__tablename__)
|
648
|
+
|
649
|
+
# For model_b -> model_a relationship (e.g., Tag.books)
|
650
|
+
model_b_to_a_name = pluralize_name(model_a.__tablename__)
|
651
|
+
|
652
|
+
logger.info(f"Setting up many-to-many: {model_a.__name__}.{model_a_to_b_name} <-> {model_b.__name__}.{model_b_to_a_name}")
|
653
|
+
|
654
|
+
# Set up relationship from model_a to model_b (e.g., Book.tags)
|
655
|
+
setup_relationship_on_class(
|
656
|
+
model_cls=model_a,
|
657
|
+
relationship_name=model_a_to_b_name,
|
658
|
+
target_cls=model_b,
|
659
|
+
back_populates=model_b_to_a_name,
|
660
|
+
is_list=True,
|
661
|
+
through_model=junction_model
|
662
|
+
)
|
663
|
+
|
664
|
+
# Set up relationship from model_b to model_a (e.g., Tag.books)
|
665
|
+
setup_relationship_on_class(
|
666
|
+
model_cls=model_b,
|
667
|
+
relationship_name=model_b_to_a_name,
|
668
|
+
target_cls=model_a,
|
669
|
+
back_populates=model_a_to_b_name,
|
670
|
+
is_list=True,
|
671
|
+
through_model=junction_model
|
672
|
+
)
|
673
|
+
|
674
|
+
logger.info(f"Successfully set up many-to-many relationships for {junction_model.__name__}")
|