async-easy-model 0.2.3__py3-none-any.whl → 0.2.5__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.
@@ -6,8 +6,8 @@ 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.3"
10
- __all__ = ["EasyModel", "init_db", "db_config", "Field", "Relationship", "Relation", "enable_auto_relationships", "disable_auto_relationships", "process_auto_relationships", "MigrationManager", "check_and_migrate_models"]
9
+ __version__ = "0.2.5"
10
+ __all__ = ["EasyModel", "init_db", "db_config", "Field", "Relationship", "Relation", "enable_auto_relationships", "disable_auto_relationships", "process_auto_relationships", "MigrationManager", "check_and_migrate_models", "ModelVisualizer"]
11
11
 
12
12
  # Create a more user-friendly Relationship function
13
13
  def Relationship(
@@ -43,4 +43,7 @@ from .relationships import Relation
43
43
  from .auto_relationships import enable_auto_relationships, disable_auto_relationships, process_auto_relationships
44
44
 
45
45
  # Add to __init__.py
46
- from .migrations import MigrationManager, check_and_migrate_models
46
+ from .migrations import MigrationManager, check_and_migrate_models
47
+
48
+ # Import the visualization helper
49
+ from .visualization import ModelVisualizer
@@ -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
- # Next, set up relationships using those foreign keys
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__}")