async-easy-model 0.2.2__py3-none-any.whl → 0.2.3__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 +246 -148
- {async_easy_model-0.2.2.dist-info → async_easy_model-0.2.3.dist-info}/METADATA +42 -12
- async_easy_model-0.2.3.dist-info/RECORD +10 -0
- async_easy_model-0.2.2.dist-info/RECORD +0 -10
- {async_easy_model-0.2.2.dist-info → async_easy_model-0.2.3.dist-info}/LICENSE +0 -0
- {async_easy_model-0.2.2.dist-info → async_easy_model-0.2.3.dist-info}/WHEEL +0 -0
- {async_easy_model-0.2.2.dist-info → async_easy_model-0.2.3.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.2.
|
9
|
+
__version__ = "0.2.3"
|
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
@@ -411,6 +411,13 @@ class EasyModel(SQLModel):
|
|
411
411
|
# Process relationships first
|
412
412
|
processed_item = await cls._process_relationships_for_insert(session, item)
|
413
413
|
|
414
|
+
# Extract special _related_* fields for post-processing
|
415
|
+
related_fields = {}
|
416
|
+
for key in list(processed_item.keys()):
|
417
|
+
if key.startswith("_related_"):
|
418
|
+
rel_name = key[9:] # Remove "_related_" prefix
|
419
|
+
related_fields[rel_name] = processed_item.pop(key)
|
420
|
+
|
414
421
|
# Check if a record with unique constraints already exists
|
415
422
|
unique_fields = cls._get_unique_fields()
|
416
423
|
existing_obj = None
|
@@ -433,12 +440,33 @@ class EasyModel(SQLModel):
|
|
433
440
|
for key, value in processed_item.items():
|
434
441
|
if key != 'id': # Don't update ID
|
435
442
|
setattr(existing_obj, key, value)
|
436
|
-
|
443
|
+
obj = existing_obj
|
437
444
|
else:
|
438
445
|
# Create new object
|
439
446
|
obj = cls(**processed_item)
|
440
447
|
session.add(obj)
|
441
|
-
|
448
|
+
|
449
|
+
# Flush to get the ID for this object
|
450
|
+
await session.flush()
|
451
|
+
|
452
|
+
# Now handle any one-to-many relationships
|
453
|
+
for rel_name, related_objects in related_fields.items():
|
454
|
+
# Check if the relationship attribute exists in the class (not the instance)
|
455
|
+
if hasattr(cls, rel_name):
|
456
|
+
# Get the relationship attribute from the class
|
457
|
+
rel_attr = getattr(cls, rel_name)
|
458
|
+
|
459
|
+
# Check if it's a SQLAlchemy relationship
|
460
|
+
if hasattr(rel_attr, 'property') and hasattr(rel_attr.property, 'back_populates'):
|
461
|
+
back_attr = rel_attr.property.back_populates
|
462
|
+
|
463
|
+
# For each related object, set the back reference to this object
|
464
|
+
for related_obj in related_objects:
|
465
|
+
setattr(related_obj, back_attr, obj)
|
466
|
+
# Make sure the related object is in the session
|
467
|
+
session.add(related_obj)
|
468
|
+
|
469
|
+
objects.append(obj)
|
442
470
|
except Exception as e:
|
443
471
|
logging.error(f"Error inserting record: {e}")
|
444
472
|
await session.rollback()
|
@@ -465,6 +493,13 @@ class EasyModel(SQLModel):
|
|
465
493
|
# Process relationships first
|
466
494
|
processed_data = await cls._process_relationships_for_insert(session, data)
|
467
495
|
|
496
|
+
# Extract special _related_* fields for post-processing
|
497
|
+
related_fields = {}
|
498
|
+
for key in list(processed_data.keys()):
|
499
|
+
if key.startswith("_related_"):
|
500
|
+
rel_name = key[9:] # Remove "_related_" prefix
|
501
|
+
related_fields[rel_name] = processed_data.pop(key)
|
502
|
+
|
468
503
|
# Check if a record with unique constraints already exists
|
469
504
|
unique_fields = cls._get_unique_fields()
|
470
505
|
existing_obj = None
|
@@ -494,6 +529,24 @@ class EasyModel(SQLModel):
|
|
494
529
|
session.add(obj)
|
495
530
|
|
496
531
|
await session.flush() # Flush to get the ID
|
532
|
+
|
533
|
+
# Now handle any one-to-many relationships
|
534
|
+
for rel_name, related_objects in related_fields.items():
|
535
|
+
# Check if the relationship attribute exists in the class (not the instance)
|
536
|
+
if hasattr(cls, rel_name):
|
537
|
+
# Get the relationship attribute from the class
|
538
|
+
rel_attr = getattr(cls, rel_name)
|
539
|
+
|
540
|
+
# Check if it's a SQLAlchemy relationship
|
541
|
+
if hasattr(rel_attr, 'property') and hasattr(rel_attr.property, 'back_populates'):
|
542
|
+
back_attr = rel_attr.property.back_populates
|
543
|
+
|
544
|
+
# For each related object, set the back reference to this object
|
545
|
+
for related_obj in related_objects:
|
546
|
+
setattr(related_obj, back_attr, obj)
|
547
|
+
# Make sure the related object is in the session
|
548
|
+
session.add(related_obj)
|
549
|
+
|
497
550
|
await session.commit()
|
498
551
|
|
499
552
|
if include_relationships:
|
@@ -510,60 +563,6 @@ class EasyModel(SQLModel):
|
|
510
563
|
logging.error(f"Error inserting record: {e}")
|
511
564
|
await session.rollback()
|
512
565
|
raise
|
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
566
|
|
568
567
|
@classmethod
|
569
568
|
def _get_unique_fields(cls) -> List[str]:
|
@@ -591,6 +590,15 @@ class EasyModel(SQLModel):
|
|
591
590
|
"quantity": 2
|
592
591
|
})
|
593
592
|
|
593
|
+
It also handles lists of related objects for one-to-many relationships:
|
594
|
+
publisher = await Publisher.insert({
|
595
|
+
"name": "Example Publisher",
|
596
|
+
"authors": [
|
597
|
+
{"name": "Author 1", "email": "author1@example.com"},
|
598
|
+
{"name": "Author 2", "email": "author2@example.com"}
|
599
|
+
]
|
600
|
+
})
|
601
|
+
|
594
602
|
For each nested object:
|
595
603
|
1. Find the target model class
|
596
604
|
2. Check if an object with the same unique fields already exists
|
@@ -620,8 +628,8 @@ class EasyModel(SQLModel):
|
|
620
628
|
|
621
629
|
# Handle nested relationship objects
|
622
630
|
for key, value in data.items():
|
623
|
-
# Skip if
|
624
|
-
if
|
631
|
+
# Skip if None
|
632
|
+
if value is None:
|
625
633
|
continue
|
626
634
|
|
627
635
|
# Check if this is a relationship field (either by name or derived from foreign key)
|
@@ -675,105 +683,164 @@ class EasyModel(SQLModel):
|
|
675
683
|
logging.warning(f"Could not find related model for {key} in {cls.__name__}")
|
676
684
|
continue
|
677
685
|
|
678
|
-
#
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
field_info.field_info.extra.get('unique', False)):
|
683
|
-
unique_fields.append(field_name)
|
684
|
-
|
685
|
-
# Create a search dictionary using unique fields
|
686
|
-
search_dict = {}
|
687
|
-
for field in unique_fields:
|
688
|
-
if field in value and value[field] is not None:
|
689
|
-
search_dict[field] = value[field]
|
690
|
-
|
691
|
-
# If no unique fields found but ID is provided, use it
|
692
|
-
if not search_dict and 'id' in value and value['id']:
|
693
|
-
search_dict = {'id': value['id']}
|
694
|
-
|
695
|
-
# Special case for products without uniqueness constraints
|
696
|
-
if not search_dict and related_model.__tablename__ == 'products' and 'name' in value:
|
697
|
-
search_dict = {'name': value['name']}
|
698
|
-
|
699
|
-
# Try to find an existing record
|
700
|
-
related_obj = None
|
701
|
-
if search_dict:
|
702
|
-
logging.info(f"Searching for existing {related_model.__name__} with {search_dict}")
|
686
|
+
# Check if the value is a list (one-to-many) or dict (one-to-one)
|
687
|
+
if isinstance(value, list):
|
688
|
+
# Handle one-to-many relationship (list of dictionaries)
|
689
|
+
related_objects = []
|
703
690
|
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
for field, field_value in search_dict.items():
|
708
|
-
existing_stmt = existing_stmt.where(getattr(related_model, field) == field_value)
|
709
|
-
|
710
|
-
existing_result = await session.execute(existing_stmt)
|
711
|
-
related_obj = existing_result.scalars().first()
|
712
|
-
|
713
|
-
if related_obj:
|
714
|
-
logging.info(f"Found existing {related_model.__name__} with ID: {related_obj.id}")
|
715
|
-
except Exception as e:
|
716
|
-
logging.error(f"Error finding existing record: {e}")
|
717
|
-
|
718
|
-
if related_obj:
|
719
|
-
# Update the existing record with any non-unique field values
|
720
|
-
for attr, attr_val in value.items():
|
721
|
-
# Skip ID field
|
722
|
-
if attr == 'id':
|
691
|
+
for item in value:
|
692
|
+
if not isinstance(item, dict):
|
693
|
+
logging.warning(f"Skipping non-dict item in list for {key}")
|
723
694
|
continue
|
724
695
|
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
current_val = getattr(related_obj, attr, None)
|
731
|
-
if current_val != attr_val:
|
732
|
-
setattr(related_obj, attr, attr_val)
|
696
|
+
related_obj = await cls._process_single_relationship_item(
|
697
|
+
session, related_model, item
|
698
|
+
)
|
699
|
+
if related_obj:
|
700
|
+
related_objects.append(related_obj)
|
733
701
|
|
734
|
-
#
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
#
|
739
|
-
|
740
|
-
|
741
|
-
session.add(related_obj)
|
702
|
+
# For one-to-many, we need to keep a list of related objects to be attached later
|
703
|
+
# We'll store them in a special field that will be removed before creating the model
|
704
|
+
result[f"_related_{key}"] = related_objects
|
705
|
+
|
706
|
+
# Remove the original field from the result
|
707
|
+
if key in result:
|
708
|
+
del result[key]
|
742
709
|
|
743
|
-
|
744
|
-
|
745
|
-
await
|
746
|
-
|
747
|
-
|
710
|
+
elif isinstance(value, dict):
|
711
|
+
# Handle one-to-one relationship (single dictionary)
|
712
|
+
related_obj = await cls._process_single_relationship_item(
|
713
|
+
session, related_model, value
|
714
|
+
)
|
748
715
|
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
# Try to find by any field provided in the search_dict
|
754
|
-
existing_stmt = select(related_model)
|
755
|
-
for field, field_value in search_dict.items():
|
756
|
-
existing_stmt = existing_stmt.where(getattr(related_model, field) == field_value)
|
757
|
-
|
758
|
-
# Execute the search query
|
759
|
-
existing_result = await session.execute(existing_stmt)
|
760
|
-
related_obj = existing_result.scalars().first()
|
716
|
+
if related_obj:
|
717
|
+
# Update the result with the foreign key ID
|
718
|
+
foreign_key_name = f"{key}_id"
|
719
|
+
result[foreign_key_name] = related_obj.id
|
761
720
|
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
721
|
+
# Remove the relationship dictionary from the result
|
722
|
+
if key in result:
|
723
|
+
del result[key]
|
724
|
+
|
725
|
+
return result
|
726
|
+
|
727
|
+
@classmethod
|
728
|
+
async def _process_single_relationship_item(cls, session: AsyncSession, related_model: Type, item_data: Dict[str, Any]) -> Optional[Any]:
|
729
|
+
"""
|
730
|
+
Process a single relationship item (dictionary).
|
731
|
+
|
732
|
+
This helper method is used by _process_relationships_for_insert to handle
|
733
|
+
both singular relationship objects and items within lists of relationships.
|
734
|
+
|
735
|
+
Args:
|
736
|
+
session: The database session to use
|
737
|
+
related_model: The related model class
|
738
|
+
item_data: Dictionary with field values for the related object
|
739
|
+
|
740
|
+
Returns:
|
741
|
+
The created or found related object, or None if processing failed
|
742
|
+
"""
|
743
|
+
# Look for unique fields in the related model to use for searching
|
744
|
+
unique_fields = []
|
745
|
+
for field_name, field_info in related_model.__fields__.items():
|
746
|
+
if (hasattr(field_info, "field_info") and
|
747
|
+
field_info.field_info.extra.get('unique', False)):
|
748
|
+
unique_fields.append(field_name)
|
749
|
+
|
750
|
+
# Create a search dictionary using unique fields
|
751
|
+
search_dict = {}
|
752
|
+
for field in unique_fields:
|
753
|
+
if field in item_data and item_data[field] is not None:
|
754
|
+
search_dict[field] = item_data[field]
|
755
|
+
|
756
|
+
# If no unique fields found but ID is provided, use it
|
757
|
+
if not search_dict and 'id' in item_data and item_data['id']:
|
758
|
+
search_dict = {'id': item_data['id']}
|
759
|
+
|
760
|
+
# Special case for products without uniqueness constraints
|
761
|
+
if not search_dict and related_model.__tablename__ == 'products' and 'name' in item_data:
|
762
|
+
search_dict = {'name': item_data['name']}
|
763
|
+
|
764
|
+
# Try to find an existing record
|
765
|
+
related_obj = None
|
766
|
+
if search_dict:
|
767
|
+
logging.info(f"Searching for existing {related_model.__name__} with {search_dict}")
|
768
|
+
|
769
|
+
try:
|
770
|
+
# Create a more appropriate search query based on unique fields
|
771
|
+
existing_stmt = select(related_model)
|
772
|
+
for field, field_value in search_dict.items():
|
773
|
+
existing_stmt = existing_stmt.where(getattr(related_model, field) == field_value)
|
767
774
|
|
768
|
-
|
769
|
-
|
770
|
-
result[foreign_key_name] = related_obj.id
|
775
|
+
existing_result = await session.execute(existing_stmt)
|
776
|
+
related_obj = existing_result.scalars().first()
|
771
777
|
|
772
|
-
|
773
|
-
|
774
|
-
|
778
|
+
if related_obj:
|
779
|
+
logging.info(f"Found existing {related_model.__name__} with ID: {related_obj.id}")
|
780
|
+
except Exception as e:
|
781
|
+
logging.error(f"Error finding existing record: {e}")
|
775
782
|
|
776
|
-
|
783
|
+
if related_obj:
|
784
|
+
# Update the existing record with any non-unique field values
|
785
|
+
for attr, attr_val in item_data.items():
|
786
|
+
# Skip ID field
|
787
|
+
if attr == 'id':
|
788
|
+
continue
|
789
|
+
|
790
|
+
# Skip unique fields with different values to avoid constraint violations
|
791
|
+
if attr in unique_fields and getattr(related_obj, attr) != attr_val:
|
792
|
+
continue
|
793
|
+
|
794
|
+
# Update non-unique fields
|
795
|
+
current_val = getattr(related_obj, attr, None)
|
796
|
+
if current_val != attr_val:
|
797
|
+
setattr(related_obj, attr, attr_val)
|
798
|
+
|
799
|
+
# Add the updated object to the session
|
800
|
+
session.add(related_obj)
|
801
|
+
logging.info(f"Reusing existing {related_model.__name__} with ID: {related_obj.id}")
|
802
|
+
else:
|
803
|
+
# Create a new record
|
804
|
+
logging.info(f"Creating new {related_model.__name__}")
|
805
|
+
|
806
|
+
# Process nested relationships in this item first
|
807
|
+
if hasattr(related_model, '_process_relationships_for_insert'):
|
808
|
+
# This is a recursive call to process nested relationships
|
809
|
+
processed_item_data = await related_model._process_relationships_for_insert(
|
810
|
+
session, item_data
|
811
|
+
)
|
812
|
+
else:
|
813
|
+
processed_item_data = item_data
|
814
|
+
|
815
|
+
related_obj = related_model(**processed_item_data)
|
816
|
+
session.add(related_obj)
|
817
|
+
|
818
|
+
# Ensure the object has an ID by flushing
|
819
|
+
try:
|
820
|
+
await session.flush()
|
821
|
+
except Exception as e:
|
822
|
+
logging.error(f"Error flushing session for {related_model.__name__}: {e}")
|
823
|
+
|
824
|
+
# If there was a uniqueness error, try again to find the existing record
|
825
|
+
if "UNIQUE constraint failed" in str(e):
|
826
|
+
logging.info(f"UNIQUE constraint failed, trying to find existing record again")
|
827
|
+
|
828
|
+
# Try to find by any field provided in the search_dict
|
829
|
+
existing_stmt = select(related_model)
|
830
|
+
for field, field_value in search_dict.items():
|
831
|
+
existing_stmt = existing_stmt.where(getattr(related_model, field) == field_value)
|
832
|
+
|
833
|
+
# Execute the search query
|
834
|
+
existing_result = await session.execute(existing_stmt)
|
835
|
+
related_obj = existing_result.scalars().first()
|
836
|
+
|
837
|
+
if not related_obj:
|
838
|
+
# We couldn't find an existing record, re-raise the exception
|
839
|
+
raise
|
840
|
+
|
841
|
+
logging.info(f"Found existing {related_model.__name__} with ID: {related_obj.id} after constraint error")
|
842
|
+
|
843
|
+
return related_obj
|
777
844
|
|
778
845
|
@classmethod
|
779
846
|
async def update(cls: Type[T], data: Dict[str, Any], criteria: Dict[str, Any], include_relationships: bool = True) -> Optional[T]:
|
@@ -997,7 +1064,7 @@ class EasyModel(SQLModel):
|
|
997
1064
|
Retrieve record(s) by matching attribute values.
|
998
1065
|
|
999
1066
|
Args:
|
1000
|
-
criteria: Dictionary of
|
1067
|
+
criteria: Dictionary of search criteria
|
1001
1068
|
all: If True, return all matching records, otherwise return only the first one
|
1002
1069
|
first: If True, return only the first record (equivalent to all=False)
|
1003
1070
|
include_relationships: If True, eagerly load all relationships
|
@@ -1110,6 +1177,37 @@ class EasyModel(SQLModel):
|
|
1110
1177
|
new_record = await cls.insert(data)
|
1111
1178
|
return new_record, True
|
1112
1179
|
|
1180
|
+
@classmethod
|
1181
|
+
async def insert_with_related(
|
1182
|
+
cls: Type[T],
|
1183
|
+
data: Dict[str, Any],
|
1184
|
+
related_data: Dict[str, List[Dict[str, Any]]] = None
|
1185
|
+
) -> T:
|
1186
|
+
"""
|
1187
|
+
Create a model instance with related objects in a single transaction.
|
1188
|
+
|
1189
|
+
Args:
|
1190
|
+
data: Dictionary of field values for the main model
|
1191
|
+
related_data: Dictionary mapping relationship names to lists of data dictionaries
|
1192
|
+
for creating related objects
|
1193
|
+
|
1194
|
+
Returns:
|
1195
|
+
The created model instance with relationships loaded
|
1196
|
+
"""
|
1197
|
+
if related_data is None:
|
1198
|
+
related_data = {}
|
1199
|
+
|
1200
|
+
# Create a copy of data for modification
|
1201
|
+
insert_data = data.copy()
|
1202
|
+
|
1203
|
+
# Add relationship fields to the data
|
1204
|
+
for rel_name, items_data in related_data.items():
|
1205
|
+
if items_data:
|
1206
|
+
insert_data[rel_name] = items_data
|
1207
|
+
|
1208
|
+
# Use the enhanced insert method to handle all relationships
|
1209
|
+
return await cls.insert(insert_data, include_relationships=True)
|
1210
|
+
|
1113
1211
|
# Register an event listener to update 'updated_at' on instance modifications.
|
1114
1212
|
@event.listens_for(Session, "before_flush")
|
1115
1213
|
def _update_updated_at(session, flush_context, instances):
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: async-easy-model
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.3
|
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
|
|
@@ -0,0 +1,10 @@
|
|
1
|
+
async_easy_model/__init__.py,sha256=u1yerriPm93E_uXcK9r-y7wcMOV1F4t-KjKEooCaiIA,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=G-0htjy78Nae5HdNE2dMGV8Z8s-rUNRTUANcKArxrDM,57852
|
5
|
+
async_easy_model/relationships.py,sha256=vR5BsJpGaDcecCcNlg9-ouZfxFXFQv5kOyiXhKp_T7A,3286
|
6
|
+
async_easy_model-0.2.3.dist-info/LICENSE,sha256=uwDkl6oHbRltW7vYKNc4doJyhtwhyrSNFFlPpKATwLE,1072
|
7
|
+
async_easy_model-0.2.3.dist-info/METADATA,sha256=SZJy5Ozs7Jn-J3qVTiVB637IGza-5C6hdet_dPocM3I,10720
|
8
|
+
async_easy_model-0.2.3.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
9
|
+
async_easy_model-0.2.3.dist-info/top_level.txt,sha256=e5_47sGmJnyxz2msfwU6C316EqmrSd9RGIYwZyWx68E,17
|
10
|
+
async_easy_model-0.2.3.dist-info/RECORD,,
|
@@ -1,10 +0,0 @@
|
|
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,,
|
File without changes
|
File without changes
|
File without changes
|