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.
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/PKG-INFO +85 -1
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/README.md +84 -0
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/async_easy_model/__init__.py +6 -3
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/async_easy_model/auto_relationships.py +132 -2
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/async_easy_model/model.py +516 -367
- async_easy_model-0.2.5/async_easy_model/visualization.py +619 -0
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/async_easy_model.egg-info/PKG-INFO +85 -1
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/async_easy_model.egg-info/SOURCES.txt +1 -0
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/setup.py +1 -1
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/LICENSE +0 -0
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/async_easy_model/migrations.py +0 -0
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/async_easy_model/relationships.py +0 -0
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/async_easy_model.egg-info/dependency_links.txt +0 -0
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/async_easy_model.egg-info/requires.txt +0 -0
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/async_easy_model.egg-info/top_level.txt +0 -0
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/setup.cfg +0 -0
- {async_easy_model-0.2.3 → async_easy_model-0.2.5}/tests/test_easy_model.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: async-easy-model
|
3
|
-
Version: 0.2.
|
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.
|
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
|
-
|
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__}")
|