async-easy-model 0.2.2__py3-none-any.whl → 0.2.4__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/auto_relationships.py +132 -2
- async_easy_model/model.py +683 -436
- {async_easy_model-0.2.2.dist-info → async_easy_model-0.2.4.dist-info}/METADATA +42 -12
- async_easy_model-0.2.4.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.4.dist-info}/LICENSE +0 -0
- {async_easy_model-0.2.2.dist-info → async_easy_model-0.2.4.dist-info}/WHEEL +0 -0
- {async_easy_model-0.2.2.dist-info → async_easy_model-0.2.4.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.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__}")
|