async-easy-model 0.2.1__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 +247 -149
- async_easy_model-0.2.3.dist-info/METADATA +362 -0
- async_easy_model-0.2.3.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.3.dist-info}/LICENSE +0 -0
- {async_easy_model-0.2.1.dist-info → async_easy_model-0.2.3.dist-info}/WHEEL +0 -0
- {async_easy_model-0.2.1.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.
|
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,7 +563,7 @@ class EasyModel(SQLModel):
|
|
510
563
|
logging.error(f"Error inserting record: {e}")
|
511
564
|
await session.rollback()
|
512
565
|
raise
|
513
|
-
|
566
|
+
|
514
567
|
@classmethod
|
515
568
|
def _get_unique_fields(cls) -> List[str]:
|
516
569
|
"""
|
@@ -537,6 +590,15 @@ class EasyModel(SQLModel):
|
|
537
590
|
"quantity": 2
|
538
591
|
})
|
539
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
|
+
|
540
602
|
For each nested object:
|
541
603
|
1. Find the target model class
|
542
604
|
2. Check if an object with the same unique fields already exists
|
@@ -566,8 +628,8 @@ class EasyModel(SQLModel):
|
|
566
628
|
|
567
629
|
# Handle nested relationship objects
|
568
630
|
for key, value in data.items():
|
569
|
-
# Skip if
|
570
|
-
if
|
631
|
+
# Skip if None
|
632
|
+
if value is None:
|
571
633
|
continue
|
572
634
|
|
573
635
|
# Check if this is a relationship field (either by name or derived from foreign key)
|
@@ -621,105 +683,164 @@ class EasyModel(SQLModel):
|
|
621
683
|
logging.warning(f"Could not find related model for {key} in {cls.__name__}")
|
622
684
|
continue
|
623
685
|
|
624
|
-
#
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
field_info.field_info.extra.get('unique', False)):
|
629
|
-
unique_fields.append(field_name)
|
630
|
-
|
631
|
-
# Create a search dictionary using unique fields
|
632
|
-
search_dict = {}
|
633
|
-
for field in unique_fields:
|
634
|
-
if field in value and value[field] is not None:
|
635
|
-
search_dict[field] = value[field]
|
636
|
-
|
637
|
-
# If no unique fields found but ID is provided, use it
|
638
|
-
if not search_dict and 'id' in value and value['id']:
|
639
|
-
search_dict = {'id': value['id']}
|
640
|
-
|
641
|
-
# Special case for products without uniqueness constraints
|
642
|
-
if not search_dict and related_model.__tablename__ == 'products' and 'name' in value:
|
643
|
-
search_dict = {'name': value['name']}
|
644
|
-
|
645
|
-
# Try to find an existing record
|
646
|
-
related_obj = None
|
647
|
-
if search_dict:
|
648
|
-
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 = []
|
649
690
|
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
for field, field_value in search_dict.items():
|
654
|
-
existing_stmt = existing_stmt.where(getattr(related_model, field) == field_value)
|
655
|
-
|
656
|
-
existing_result = await session.execute(existing_stmt)
|
657
|
-
related_obj = existing_result.scalars().first()
|
658
|
-
|
659
|
-
if related_obj:
|
660
|
-
logging.info(f"Found existing {related_model.__name__} with ID: {related_obj.id}")
|
661
|
-
except Exception as e:
|
662
|
-
logging.error(f"Error finding existing record: {e}")
|
663
|
-
|
664
|
-
if related_obj:
|
665
|
-
# Update the existing record with any non-unique field values
|
666
|
-
for attr, attr_val in value.items():
|
667
|
-
# Skip ID field
|
668
|
-
if attr == 'id':
|
669
|
-
continue
|
670
|
-
|
671
|
-
# Skip unique fields with different values to avoid constraint violations
|
672
|
-
if attr in unique_fields and getattr(related_obj, attr) != attr_val:
|
691
|
+
for item in value:
|
692
|
+
if not isinstance(item, dict):
|
693
|
+
logging.warning(f"Skipping non-dict item in list for {key}")
|
673
694
|
continue
|
674
695
|
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
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)
|
679
701
|
|
680
|
-
#
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
#
|
685
|
-
|
686
|
-
|
687
|
-
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]
|
688
709
|
|
689
|
-
|
690
|
-
|
691
|
-
await
|
692
|
-
|
693
|
-
|
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
|
+
)
|
694
715
|
|
695
|
-
|
696
|
-
|
697
|
-
|
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
|
698
720
|
|
699
|
-
#
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
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)
|
713
774
|
|
714
|
-
|
715
|
-
|
716
|
-
result[foreign_key_name] = related_obj.id
|
775
|
+
existing_result = await session.execute(existing_stmt)
|
776
|
+
related_obj = existing_result.scalars().first()
|
717
777
|
|
718
|
-
|
719
|
-
|
720
|
-
|
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}")
|
721
782
|
|
722
|
-
|
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
|
723
844
|
|
724
845
|
@classmethod
|
725
846
|
async def update(cls: Type[T], data: Dict[str, Any], criteria: Dict[str, Any], include_relationships: bool = True) -> Optional[T]:
|
@@ -861,60 +982,6 @@ class EasyModel(SQLModel):
|
|
861
982
|
await session.rollback()
|
862
983
|
raise
|
863
984
|
|
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
985
|
def to_dict(self, include_relationships: bool = True, max_depth: int = 4) -> Dict[str, Any]:
|
919
986
|
"""
|
920
987
|
Convert the model instance to a dictionary.
|
@@ -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):
|
@@ -0,0 +1,362 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: async-easy-model
|
3
|
+
Version: 0.2.3
|
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 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
|
+
]
|
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]}")
|
187
|
+
```
|
188
|
+
|
189
|
+
### Read (Retrieve)
|
190
|
+
|
191
|
+
```python
|
192
|
+
# Select by ID
|
193
|
+
user = await User.select({"id": 1})
|
194
|
+
|
195
|
+
# Select with criteria
|
196
|
+
users = await User.select({"is_active": True}, all=True)
|
197
|
+
|
198
|
+
# Select first matching record
|
199
|
+
first_user = await User.select({"is_active": True}, first=True)
|
200
|
+
|
201
|
+
# Select all records
|
202
|
+
all_users = await User.select({}, all=True)
|
203
|
+
|
204
|
+
# Select with wildcard pattern matching
|
205
|
+
gmail_users = await User.select({"email": "*@gmail.com"}, all=True)
|
206
|
+
|
207
|
+
# Select with ordering
|
208
|
+
recent_users = await User.select({}, order_by="-created_at", all=True)
|
209
|
+
|
210
|
+
# Select with limit
|
211
|
+
latest_posts = await Post.select({}, order_by="-created_at", limit=5)
|
212
|
+
# Note: limit > 1 automatically sets all=True
|
213
|
+
|
214
|
+
# Select with multiple ordering fields
|
215
|
+
sorted_users = await User.select({}, order_by=["last_name", "first_name"], all=True)
|
216
|
+
|
217
|
+
# Select with relationship ordering
|
218
|
+
posts_by_author = await Post.select({}, order_by="user.username", all=True)
|
219
|
+
```
|
220
|
+
|
221
|
+
### Update
|
222
|
+
|
223
|
+
```python
|
224
|
+
# Update by ID
|
225
|
+
user = await User.update({"is_active": False}, 1)
|
226
|
+
|
227
|
+
# Update by criteria
|
228
|
+
count = await User.update(
|
229
|
+
{"is_active": False},
|
230
|
+
{"last_login": None} # Set all users without login to inactive
|
231
|
+
)
|
232
|
+
|
233
|
+
# Update with relationships
|
234
|
+
await User.update(
|
235
|
+
{"department": {"name": "Sales"}}, # Update department relationship
|
236
|
+
{"username": "john_doe"}
|
237
|
+
)
|
238
|
+
```
|
239
|
+
|
240
|
+
### Delete
|
241
|
+
|
242
|
+
```python
|
243
|
+
# Delete by ID
|
244
|
+
success = await User.delete(1)
|
245
|
+
|
246
|
+
# Delete by criteria
|
247
|
+
deleted_count = await User.delete({"is_active": False})
|
248
|
+
|
249
|
+
# Delete with compound criteria
|
250
|
+
await Post.delete({"user": {"username": "john_doe"}, "is_published": False})
|
251
|
+
```
|
252
|
+
|
253
|
+
## Convenient Query Methods
|
254
|
+
|
255
|
+
async-easy-model provides simplified methods for common query patterns:
|
256
|
+
|
257
|
+
```python
|
258
|
+
# Get all records with relationships loaded (default)
|
259
|
+
users = await User.all()
|
260
|
+
|
261
|
+
# Get all records ordered by a field (ascending)
|
262
|
+
users = await User.all(order_by="username")
|
263
|
+
|
264
|
+
# Get all records ordered by a field (descending)
|
265
|
+
newest_users = await User.all(order_by="-created_at")
|
266
|
+
|
267
|
+
# Get all records ordered by multiple fields
|
268
|
+
sorted_users = await User.all(order_by=["last_name", "first_name"])
|
269
|
+
|
270
|
+
# Get all records ordered by relationship fields
|
271
|
+
books = await Book.all(order_by="author.name")
|
272
|
+
|
273
|
+
# Get the first record
|
274
|
+
user = await User.first()
|
275
|
+
|
276
|
+
# Get the most recently created user
|
277
|
+
newest_user = await User.first(order_by="-created_at")
|
278
|
+
|
279
|
+
# Get a limited number of records
|
280
|
+
recent_users = await User.limit(10)
|
281
|
+
|
282
|
+
# Get a limited number of records with ordering
|
283
|
+
top_products = await Product.limit(5, order_by="-sales")
|
284
|
+
```
|
285
|
+
|
286
|
+
## Enhanced Relationship Handling
|
287
|
+
|
288
|
+
Using the models defined earlier, here's how to work with relationships:
|
289
|
+
|
290
|
+
```python
|
291
|
+
# Load all relationships automatically
|
292
|
+
post = await Post.select({"id": 1})
|
293
|
+
print(post.user.username) # Access related objects directly
|
294
|
+
|
295
|
+
# Load specific relationships
|
296
|
+
post = await Post.get_with_related(1, ["user", "comments"])
|
297
|
+
|
298
|
+
# Load relationships after fetching
|
299
|
+
post = await Post.select({"id": 1}, include_relationships=False)
|
300
|
+
await post.load_related(["user", "comments"])
|
301
|
+
|
302
|
+
# Insert with nested relationships
|
303
|
+
new_post = await Post.insert({
|
304
|
+
"title": "My Post",
|
305
|
+
"content": "Content here",
|
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
|
+
]
|
311
|
+
})
|
312
|
+
# Access nested data without requerying
|
313
|
+
print(f"Post by {new_post.user.username} with {len(new_post.comments)} comments")
|
314
|
+
|
315
|
+
# Convert to dictionary with nested relationships
|
316
|
+
post_dict = post.to_dict(include_relationships=True, max_depth=2)
|
317
|
+
```
|
318
|
+
|
319
|
+
## Automatic Relationship Detection
|
320
|
+
|
321
|
+
The package can automatically detect and set up bidirectional relationships between models:
|
322
|
+
|
323
|
+
```python
|
324
|
+
class User(EasyModel, table=True):
|
325
|
+
username: str
|
326
|
+
|
327
|
+
class Post(EasyModel, table=True):
|
328
|
+
title: str
|
329
|
+
user_id: int = Field(foreign_key="user.id")
|
330
|
+
|
331
|
+
# After init_db():
|
332
|
+
# - post.user relationship is automatically available
|
333
|
+
# - user.posts relationship is automatically available
|
334
|
+
```
|
335
|
+
|
336
|
+
## Database Configuration
|
337
|
+
|
338
|
+
```python
|
339
|
+
# SQLite Configuration
|
340
|
+
db_config.configure_sqlite("database.db")
|
341
|
+
db_config.configure_sqlite(":memory:") # In-memory database
|
342
|
+
|
343
|
+
# PostgreSQL Configuration
|
344
|
+
db_config.configure_postgres(
|
345
|
+
user="your_user",
|
346
|
+
password="your_password",
|
347
|
+
host="localhost",
|
348
|
+
port="5432",
|
349
|
+
database="your_database"
|
350
|
+
)
|
351
|
+
|
352
|
+
# Custom Connection URL
|
353
|
+
db_config.set_connection_url("postgresql+asyncpg://user:password@localhost:5432/database")
|
354
|
+
```
|
355
|
+
|
356
|
+
## Documentation
|
357
|
+
|
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.
|
359
|
+
|
360
|
+
## License
|
361
|
+
|
362
|
+
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=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,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
|