async-easy-model 0.2.2__tar.gz → 0.2.4__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.2
3
+ Version: 0.2.4
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
@@ -154,12 +154,36 @@ users = await User.insert([
154
154
  {"username": "user2", "email": "user2@example.com"}
155
155
  ])
156
156
 
157
- # Insert with relationships using nested dictionaries
158
- post = await Post.insert({
159
- "title": "My First Post",
160
- "content": "Hello world!",
161
- "user": {"username": "john_doe"} # Will automatically link to existing user
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
+ ]
162
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]}")
163
187
  ```
164
188
 
165
189
  ### Read (Retrieve)
@@ -265,22 +289,28 @@ Using the models defined earlier, here's how to work with relationships:
265
289
 
266
290
  ```python
267
291
  # Load all relationships automatically
268
- post = await Post.select({"id": 1}, include_relationships=True)
292
+ post = await Post.select({"id": 1})
269
293
  print(post.user.username) # Access related objects directly
270
294
 
271
295
  # Load specific relationships
272
296
  post = await Post.get_with_related(1, ["user", "comments"])
273
297
 
274
298
  # Load relationships after fetching
275
- post = await Post.select({"id": 1})
299
+ post = await Post.select({"id": 1}, include_relationships=False)
276
300
  await post.load_related(["user", "comments"])
277
301
 
278
- # Insert with related objects in a single transaction
279
- new_post = await Post.insert_with_related({
302
+ # Insert with nested relationships
303
+ new_post = await Post.insert({
280
304
  "title": "My Post",
281
305
  "content": "Content here",
282
- "user": {"username": "john_doe"}
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
+ ]
283
311
  })
312
+ # Access nested data without requerying
313
+ print(f"Post by {new_post.user.username} with {len(new_post.comments)} comments")
284
314
 
285
315
  # Convert to dictionary with nested relationships
286
316
  post_dict = post.to_dict(include_relationships=True, max_depth=2)
@@ -325,7 +355,7 @@ db_config.set_connection_url("postgresql+asyncpg://user:password@localhost:5432/
325
355
 
326
356
  ## Documentation
327
357
 
328
- 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/main/DOCS.md) file.
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.
329
359
 
330
360
  ## License
331
361
 
@@ -116,12 +116,36 @@ users = await User.insert([
116
116
  {"username": "user2", "email": "user2@example.com"}
117
117
  ])
118
118
 
119
- # Insert with relationships using nested dictionaries
120
- post = await Post.insert({
121
- "title": "My First Post",
122
- "content": "Hello world!",
123
- "user": {"username": "john_doe"} # Will automatically link to existing user
119
+ # Insert with nested relationships
120
+ new_post = await Post.insert({
121
+ "title": "My Post",
122
+ "content": "Content here",
123
+ "user": {"username": "jane_doe"}, # Will automatically link to existing user
124
+ "comments": [ # Create multiple comments in a single transaction
125
+ {"text": "Great post!", "user": {"username": "reader1"}},
126
+ {"text": "Thanks for sharing", "user": {"username": "reader2"}}
127
+ ]
128
+ })
129
+ # Access nested data without requerying
130
+ print(f"Post by {new_post.user.username} with {len(new_post.comments)} comments")
131
+
132
+ # Insert with nested one-to-many relationships
133
+ publisher = await Publisher.insert({
134
+ "name": "Example Publisher",
135
+ "books": [ # List of nested objects
136
+ {
137
+ "title": "Python Mastery",
138
+ "genres": [
139
+ {"name": "Programming"},
140
+ {"name": "Education"}
141
+ ]
142
+ },
143
+ {"title": "Data Science Handbook"}
144
+ ]
124
145
  })
146
+ # Access nested relationships immediately
147
+ print(f"Publisher: {publisher.name} with {len(publisher.books)} books")
148
+ print(f"First book genres: {[g.name for g in publisher.books[0].genres]}")
125
149
  ```
126
150
 
127
151
  ### Read (Retrieve)
@@ -227,22 +251,28 @@ Using the models defined earlier, here's how to work with relationships:
227
251
 
228
252
  ```python
229
253
  # Load all relationships automatically
230
- post = await Post.select({"id": 1}, include_relationships=True)
254
+ post = await Post.select({"id": 1})
231
255
  print(post.user.username) # Access related objects directly
232
256
 
233
257
  # Load specific relationships
234
258
  post = await Post.get_with_related(1, ["user", "comments"])
235
259
 
236
260
  # Load relationships after fetching
237
- post = await Post.select({"id": 1})
261
+ post = await Post.select({"id": 1}, include_relationships=False)
238
262
  await post.load_related(["user", "comments"])
239
263
 
240
- # Insert with related objects in a single transaction
241
- new_post = await Post.insert_with_related({
264
+ # Insert with nested relationships
265
+ new_post = await Post.insert({
242
266
  "title": "My Post",
243
267
  "content": "Content here",
244
- "user": {"username": "john_doe"}
268
+ "user": {"username": "jane_doe"}, # Will automatically link to existing user
269
+ "comments": [ # Create multiple comments in a single transaction
270
+ {"text": "Great post!", "user": {"username": "reader1"}},
271
+ {"text": "Thanks for sharing", "user": {"username": "reader2"}}
272
+ ]
245
273
  })
274
+ # Access nested data without requerying
275
+ print(f"Post by {new_post.user.username} with {len(new_post.comments)} comments")
246
276
 
247
277
  # Convert to dictionary with nested relationships
248
278
  post_dict = post.to_dict(include_relationships=True, max_depth=2)
@@ -287,7 +317,7 @@ db_config.set_connection_url("postgresql+asyncpg://user:password@localhost:5432/
287
317
 
288
318
  ## Documentation
289
319
 
290
- 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/main/DOCS.md) file.
320
+ 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.
291
321
 
292
322
  ## License
293
323
 
@@ -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.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
- # 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__}")