async-easy-model 0.2.3__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: async-easy-model
3
- Version: 0.2.3
3
+ Version: 0.2.5
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
@@ -59,6 +59,7 @@ A simplified SQLModel-based ORM for async database operations in Python. async-e
59
59
  - 📝 Type hints for better IDE support
60
60
  - 🕒 Automatic `id`, `created_at` and `updated_at` fields provided by default
61
61
  - 🔄 Automatic schema migrations for evolving database models
62
+ - 📊 Visualization of database schema using Mermaid ER diagrams
62
63
 
63
64
  ## Installation
64
65
 
@@ -250,6 +251,89 @@ deleted_count = await User.delete({"is_active": False})
250
251
  await Post.delete({"user": {"username": "john_doe"}, "is_published": False})
251
252
  ```
252
253
 
254
+ ## Database Schema Visualization
255
+
256
+ The package includes a `ModelVisualizer` class that makes it easy to generate Entity-Relationship (ER) diagrams for your database models using Mermaid syntax.
257
+
258
+ ```python
259
+ from async_easy_model import EasyModel, init_db, db_config, ModelVisualizer
260
+
261
+ # Initialize your models and database
262
+ await init_db()
263
+
264
+ # Create a visualizer
265
+ visualizer = ModelVisualizer()
266
+
267
+ # Generate a Mermaid ER diagram
268
+ er_diagram = visualizer.mermaid()
269
+ print(er_diagram)
270
+
271
+ # Generate a shareable link to view the diagram online
272
+ er_link = visualizer.mermaid_link()
273
+ print(er_link)
274
+
275
+ # Customize the diagram title
276
+ visualizer.set_title("My Project Database Schema")
277
+ custom_diagram = visualizer.mermaid()
278
+ ```
279
+
280
+ ### Example Mermaid ER Diagram Output
281
+
282
+ ```mermaid
283
+ ---
284
+ title: EasyModel Table Schemas
285
+ config:
286
+ layout: elk
287
+ ---
288
+ erDiagram
289
+ author {
290
+ number id PK
291
+ string name "required"
292
+ string email
293
+ }
294
+ book {
295
+ number id PK
296
+ string title "required"
297
+ number author_id FK
298
+ string isbn
299
+ number published_year
300
+ author author "virtual"
301
+ tag[] tags "virtual"
302
+ }
303
+ tag {
304
+ number id PK
305
+ string name "required"
306
+ book[] books "virtual"
307
+ }
308
+ book_tag {
309
+ number id PK
310
+ number book_id FK "required"
311
+ number tag_id FK "required"
312
+ book book "virtual"
313
+ tag tag "virtual"
314
+ }
315
+ review {
316
+ number id PK
317
+ number book_id FK "required"
318
+ number rating "required"
319
+ string comment
320
+ string reviewer_name "required"
321
+ book book "virtual"
322
+ }
323
+ book ||--o{ author : "author_id"
324
+ book_tag ||--o{ book : "book_id"
325
+ book_tag ||--o{ tag : "tag_id"
326
+ book }o--o{ tag : "many-to-many"
327
+ review ||--o{ book : "book_id"
328
+ ```
329
+
330
+ The diagram automatically:
331
+ - Shows all tables with their fields and data types
332
+ - Identifies primary keys (PK) and foreign keys (FK)
333
+ - Shows required fields and virtual relationships
334
+ - Visualizes relationships between tables with proper cardinality
335
+ - Properly handles many-to-many relationships
336
+
253
337
  ## Convenient Query Methods
254
338
 
255
339
  async-easy-model provides simplified methods for common query patterns:
@@ -21,6 +21,7 @@ A simplified SQLModel-based ORM for async database operations in Python. async-e
21
21
  - 📝 Type hints for better IDE support
22
22
  - 🕒 Automatic `id`, `created_at` and `updated_at` fields provided by default
23
23
  - 🔄 Automatic schema migrations for evolving database models
24
+ - 📊 Visualization of database schema using Mermaid ER diagrams
24
25
 
25
26
  ## Installation
26
27
 
@@ -212,6 +213,89 @@ deleted_count = await User.delete({"is_active": False})
212
213
  await Post.delete({"user": {"username": "john_doe"}, "is_published": False})
213
214
  ```
214
215
 
216
+ ## Database Schema Visualization
217
+
218
+ The package includes a `ModelVisualizer` class that makes it easy to generate Entity-Relationship (ER) diagrams for your database models using Mermaid syntax.
219
+
220
+ ```python
221
+ from async_easy_model import EasyModel, init_db, db_config, ModelVisualizer
222
+
223
+ # Initialize your models and database
224
+ await init_db()
225
+
226
+ # Create a visualizer
227
+ visualizer = ModelVisualizer()
228
+
229
+ # Generate a Mermaid ER diagram
230
+ er_diagram = visualizer.mermaid()
231
+ print(er_diagram)
232
+
233
+ # Generate a shareable link to view the diagram online
234
+ er_link = visualizer.mermaid_link()
235
+ print(er_link)
236
+
237
+ # Customize the diagram title
238
+ visualizer.set_title("My Project Database Schema")
239
+ custom_diagram = visualizer.mermaid()
240
+ ```
241
+
242
+ ### Example Mermaid ER Diagram Output
243
+
244
+ ```mermaid
245
+ ---
246
+ title: EasyModel Table Schemas
247
+ config:
248
+ layout: elk
249
+ ---
250
+ erDiagram
251
+ author {
252
+ number id PK
253
+ string name "required"
254
+ string email
255
+ }
256
+ book {
257
+ number id PK
258
+ string title "required"
259
+ number author_id FK
260
+ string isbn
261
+ number published_year
262
+ author author "virtual"
263
+ tag[] tags "virtual"
264
+ }
265
+ tag {
266
+ number id PK
267
+ string name "required"
268
+ book[] books "virtual"
269
+ }
270
+ book_tag {
271
+ number id PK
272
+ number book_id FK "required"
273
+ number tag_id FK "required"
274
+ book book "virtual"
275
+ tag tag "virtual"
276
+ }
277
+ review {
278
+ number id PK
279
+ number book_id FK "required"
280
+ number rating "required"
281
+ string comment
282
+ string reviewer_name "required"
283
+ book book "virtual"
284
+ }
285
+ book ||--o{ author : "author_id"
286
+ book_tag ||--o{ book : "book_id"
287
+ book_tag ||--o{ tag : "tag_id"
288
+ book }o--o{ tag : "many-to-many"
289
+ review ||--o{ book : "book_id"
290
+ ```
291
+
292
+ The diagram automatically:
293
+ - Shows all tables with their fields and data types
294
+ - Identifies primary keys (PK) and foreign keys (FK)
295
+ - Shows required fields and virtual relationships
296
+ - Visualizes relationships between tables with proper cardinality
297
+ - Properly handles many-to-many relationships
298
+
215
299
  ## Convenient Query Methods
216
300
 
217
301
  async-easy-model provides simplified methods for common query patterns:
@@ -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__}")