sqliter-py 0.16.0__py3-none-any.whl → 0.17.0__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.
sqliter/orm/model.py CHANGED
@@ -1,4 +1,4 @@
1
- """ORM model with lazy loading and reverse relationships."""
1
+ """ORM model with lazy loading, reverse relationships, and M2M."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -8,6 +8,7 @@ from pydantic import Field
8
8
 
9
9
  from sqliter.model.model import BaseDBModel as _BaseDBModel
10
10
  from sqliter.orm.fields import ForeignKey, HasPK, LazyLoader
11
+ from sqliter.orm.m2m import ManyToMany
11
12
  from sqliter.orm.registry import ModelRegistry
12
13
 
13
14
  __all__ = ["BaseDBModel"]
@@ -19,12 +20,16 @@ class BaseDBModel(_BaseDBModel):
19
20
  Adds:
20
21
  - Lazy loading of foreign key relationships
21
22
  - Automatic reverse relationship setup
23
+ - Many-to-many relationships
22
24
  - db_context for query execution
23
25
  """
24
26
 
25
27
  # Store FK descriptors per class (not inherited)
26
28
  fk_descriptors: ClassVar[dict[str, ForeignKey[Any]]] = {}
27
29
 
30
+ # Store M2M descriptors per class (not inherited)
31
+ m2m_descriptors: ClassVar[dict[str, ManyToMany[Any]]] = {}
32
+
28
33
  # Database context for lazy loading and reverse queries
29
34
  # Using Any since SqliterDB would cause circular import issues with Pydantic
30
35
  db_context: Optional[Any] = Field(default=None, exclude=True)
@@ -51,15 +56,19 @@ class BaseDBModel(_BaseDBModel):
51
56
  super().__init__(**kwargs)
52
57
 
53
58
  def model_dump(self, **kwargs: Any) -> dict[str, Any]: # noqa: ANN401
54
- """Dump model, excluding FK descriptor fields.
59
+ """Dump model, excluding FK and M2M descriptor fields.
55
60
 
56
- FK descriptor fields (like 'author') are excluded from serialization.
57
- Only the _id fields (like 'author_id') are included.
61
+ FK descriptor fields (like 'author') are excluded from
62
+ serialization. Only the _id fields (like 'author_id') are
63
+ included. M2M descriptor fields are also excluded.
58
64
  """
59
65
  data = super().model_dump(**kwargs)
60
66
  # Remove FK descriptor fields from the dump
61
67
  for fk_field in self.fk_descriptors:
62
68
  data.pop(fk_field, None)
69
+ # Remove M2M descriptor fields from the dump
70
+ for m2m_field in self.m2m_descriptors:
71
+ data.pop(m2m_field, None)
63
72
  return data
64
73
 
65
74
  def __getattribute__(self, name: str) -> object:
@@ -98,8 +107,48 @@ class BaseDBModel(_BaseDBModel):
98
107
  # For non-FK fields, use normal attribute access
99
108
  return object.__getattribute__(self, name)
100
109
 
110
+ def _handle_reverse_m2m_set(self, name: str, value: object) -> bool:
111
+ """Check and handle reverse M2M descriptor assignment.
112
+
113
+ Args:
114
+ name: Attribute name being set.
115
+ value: Value being assigned.
116
+
117
+ Returns:
118
+ True if handled (caller should return), False otherwise.
119
+ """
120
+ cls_attr = type(self).__dict__.get(name)
121
+ if cls_attr is None:
122
+ for klass in type(self).__mro__:
123
+ if name in klass.__dict__:
124
+ cls_attr = klass.__dict__[name]
125
+ break
126
+ if cls_attr is not None:
127
+ from sqliter.orm.m2m import ( # noqa: PLC0415
128
+ ReverseManyToMany,
129
+ )
130
+
131
+ if isinstance(cls_attr, ReverseManyToMany):
132
+ cls_attr.__set__(self, value)
133
+ return True
134
+ return False
135
+
101
136
  def __setattr__(self, name: str, value: object) -> None:
102
- """Intercept FK field assignment to convert to _id field."""
137
+ """Intercept FK, M2M, and reverse M2M field assignment."""
138
+ # Guard against M2M field assignment
139
+ m2m_descs = getattr(self, "m2m_descriptors", {})
140
+ if name in m2m_descs:
141
+ msg = (
142
+ f"Cannot assign to ManyToMany field '{name}'. "
143
+ f"Use .add(), .remove(), .clear(), or .set() "
144
+ f"instead."
145
+ )
146
+ raise AttributeError(msg)
147
+
148
+ # Guard against reverse M2M assignment (dynamic descriptors)
149
+ if self._handle_reverse_m2m_set(name, value):
150
+ return
151
+
103
152
  # Check if this is a FK field assignment
104
153
  fk_descs = getattr(self, "fk_descriptors", {})
105
154
  if name in fk_descs:
@@ -143,6 +192,10 @@ class BaseDBModel(_BaseDBModel):
143
192
  if "fk_descriptors" not in cls.__dict__:
144
193
  cls.fk_descriptors = {}
145
194
 
195
+ # Collect M2M descriptors from class dict
196
+ if "m2m_descriptors" not in cls.__dict__:
197
+ cls.m2m_descriptors = {}
198
+
146
199
  # Find all ForeignKeys in the class and add _id field annotations
147
200
  # Make a copy of items to avoid modifying dict during iteration
148
201
  class_items = list(cls.__dict__.items())
@@ -165,6 +218,13 @@ class BaseDBModel(_BaseDBModel):
165
218
  if name in cls.__annotations__:
166
219
  del cls.__annotations__[name]
167
220
 
221
+ elif isinstance(value, ManyToMany):
222
+ cls.m2m_descriptors[name] = value
223
+ # Remove M2M annotation so Pydantic doesn't create a
224
+ # DB column for it
225
+ if name in cls.__annotations__:
226
+ del cls.__annotations__[name]
227
+
168
228
  @classmethod
169
229
  def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: # noqa: ANN401
170
230
  """Set up ORM FK metadata after Pydantic has created model_fields.
@@ -241,3 +301,8 @@ class BaseDBModel(_BaseDBModel):
241
301
  # validate it
242
302
  if field_name in cls.model_fields:
243
303
  del cls.model_fields[field_name]
304
+
305
+ # Remove M2M descriptor fields from model_fields
306
+ for m2m_name in cls.m2m_descriptors:
307
+ if m2m_name in cls.model_fields:
308
+ del cls.model_fields[m2m_name]
sqliter/orm/registry.py CHANGED
@@ -4,23 +4,89 @@ Central registry for:
4
4
  - Model classes by table name
5
5
  - Foreign key relationships
6
6
  - Pending reverse relationships
7
+ - Many-to-many relationships
7
8
  """
8
9
 
9
10
  from __future__ import annotations
10
11
 
11
- from typing import Any, ClassVar, Optional
12
+ from copy import deepcopy
13
+ from typing import Any, ClassVar, Optional, Protocol, TypedDict
14
+
15
+
16
+ class _M2MForwardRef(Protocol):
17
+ def resolve_forward_ref(self, model_class: type[Any]) -> None: ...
18
+
19
+ @property
20
+ def junction_table(self) -> Optional[str]: ...
21
+
22
+
23
+ class _RegistryState(TypedDict):
24
+ models: dict[str, type[Any]]
25
+ models_by_name: dict[str, type[Any]]
26
+ foreign_keys: dict[str, list[dict[str, Any]]]
27
+ pending_reverses: dict[str, list[dict[str, Any]]]
28
+ m2m_relationships: dict[str, list[dict[str, Any]]]
29
+ pending_m2m_reverses: dict[str, list[dict[str, Any]]]
30
+ pending_m2m_targets: dict[str, list[dict[str, Any]]]
12
31
 
13
32
 
14
33
  class ModelRegistry:
15
- """Registry for ORM models and FK relationships.
34
+ """Registry for ORM models, FK, and M2M relationships.
16
35
 
17
36
  Uses automatic setup via descriptor __set_name__ hook - no manual setup
18
37
  required.
19
38
  """
20
39
 
21
40
  _models: ClassVar[dict[str, type]] = {}
41
+ _models_by_name: ClassVar[dict[str, type[Any]]] = {}
22
42
  _foreign_keys: ClassVar[dict[str, list[dict[str, Any]]]] = {}
23
43
  _pending_reverses: ClassVar[dict[str, list[dict[str, Any]]]] = {}
44
+ _m2m_relationships: ClassVar[dict[str, list[dict[str, Any]]]] = {}
45
+ _pending_m2m_reverses: ClassVar[dict[str, list[dict[str, Any]]]] = {}
46
+ _pending_m2m_targets: ClassVar[dict[str, list[dict[str, Any]]]] = {}
47
+
48
+ @classmethod
49
+ def reset(cls) -> None:
50
+ """Clear all registry state.
51
+
52
+ Intended for tests that need isolation between model definitions.
53
+ """
54
+ cls._models.clear()
55
+ cls._models_by_name.clear()
56
+ cls._foreign_keys.clear()
57
+ cls._pending_reverses.clear()
58
+ cls._m2m_relationships.clear()
59
+ cls._pending_m2m_reverses.clear()
60
+ cls._pending_m2m_targets.clear()
61
+
62
+ @classmethod
63
+ def snapshot(cls) -> _RegistryState:
64
+ """Return a deep copy of the registry state.
65
+
66
+ Useful for temporarily isolating tests and restoring state afterward.
67
+ """
68
+ # Model class references are immutable, so shallow copies are enough
69
+ # for model maps; relationship dicts are deep-copied for isolation.
70
+ return {
71
+ "models": cls._models.copy(),
72
+ "models_by_name": cls._models_by_name.copy(),
73
+ "foreign_keys": deepcopy(cls._foreign_keys),
74
+ "pending_reverses": deepcopy(cls._pending_reverses),
75
+ "m2m_relationships": deepcopy(cls._m2m_relationships),
76
+ "pending_m2m_reverses": deepcopy(cls._pending_m2m_reverses),
77
+ "pending_m2m_targets": deepcopy(cls._pending_m2m_targets),
78
+ }
79
+
80
+ @classmethod
81
+ def restore(cls, state: _RegistryState) -> None:
82
+ """Restore registry state from a snapshot."""
83
+ cls._models = state["models"]
84
+ cls._models_by_name = state["models_by_name"]
85
+ cls._foreign_keys = state["foreign_keys"]
86
+ cls._pending_reverses = state["pending_reverses"]
87
+ cls._m2m_relationships = state["m2m_relationships"]
88
+ cls._pending_m2m_reverses = state["pending_m2m_reverses"]
89
+ cls._pending_m2m_targets = state["pending_m2m_targets"]
24
90
 
25
91
  @classmethod
26
92
  def register_model(cls, model_class: type[Any]) -> None:
@@ -31,6 +97,7 @@ class ModelRegistry:
31
97
  """
32
98
  table_name = model_class.get_table_name()
33
99
  cls._models[table_name] = model_class
100
+ cls._models_by_name[model_class.__name__] = model_class
34
101
 
35
102
  # Process any pending reverse relationships for this model
36
103
  if table_name in cls._pending_reverses:
@@ -38,6 +105,38 @@ class ModelRegistry:
38
105
  cls._add_reverse_relationship_now(**pending)
39
106
  del cls._pending_reverses[table_name]
40
107
 
108
+ # Process any pending M2M reverse relationships
109
+ if table_name in cls._pending_m2m_reverses:
110
+ for pending in cls._pending_m2m_reverses[table_name]:
111
+ cls._add_m2m_reverse_now(
112
+ from_model=pending["from_model"],
113
+ to_model=pending["to_model"],
114
+ m2m_field=pending["m2m_field"],
115
+ junction_table=pending["junction_table"],
116
+ related_name=pending["related_name"],
117
+ symmetrical=pending["symmetrical"],
118
+ )
119
+ del cls._pending_m2m_reverses[table_name]
120
+
121
+ class_name = model_class.__name__
122
+ if class_name in cls._pending_m2m_targets:
123
+ for pending in cls._pending_m2m_targets[class_name]:
124
+ descriptor = pending["descriptor"]
125
+ descriptor.resolve_forward_ref(model_class)
126
+ junction_table = descriptor.junction_table
127
+ if junction_table is None:
128
+ msg = "ManyToMany junction table could not be resolved."
129
+ raise ValueError(msg)
130
+ cls.add_m2m_relationship(
131
+ from_model=pending["from_model"],
132
+ to_model=model_class,
133
+ m2m_field=pending["m2m_field"],
134
+ junction_table=junction_table,
135
+ related_name=pending["related_name"],
136
+ symmetrical=pending["symmetrical"],
137
+ )
138
+ del cls._pending_m2m_targets[class_name]
139
+
41
140
  @classmethod
42
141
  def register_foreign_key(
43
142
  cls,
@@ -82,6 +181,11 @@ class ModelRegistry:
82
181
  """
83
182
  return cls._models.get(table_name)
84
183
 
184
+ @classmethod
185
+ def get_model_by_name(cls, class_name: str) -> Optional[type[Any]]:
186
+ """Get model by class name."""
187
+ return cls._models_by_name.get(class_name)
188
+
85
189
  @classmethod
86
190
  def get_foreign_keys(cls, table_name: str) -> list[dict[str, Any]]:
87
191
  """Get FK relationships for a model.
@@ -167,3 +271,170 @@ class ModelRegistry:
167
271
  related_name,
168
272
  ReverseRelationship(from_model, fk_field, related_name),
169
273
  )
274
+
275
+ @classmethod
276
+ def add_m2m_relationship(
277
+ cls,
278
+ from_model: type[Any],
279
+ to_model: type[Any],
280
+ m2m_field: str,
281
+ junction_table: str,
282
+ related_name: Optional[str],
283
+ *,
284
+ symmetrical: bool = False,
285
+ ) -> None:
286
+ """Register a M2M relationship and set up reverse accessor.
287
+
288
+ Called by ManyToMany.__set_name__ during class creation. If the
289
+ target model hasn't been registered yet, the reverse accessor
290
+ is stored as pending.
291
+
292
+ Args:
293
+ from_model: The model defining the ManyToMany field.
294
+ to_model: The target model class.
295
+ m2m_field: Name of the M2M field.
296
+ junction_table: Name of the junction table.
297
+ related_name: Name for the reverse accessor.
298
+ symmetrical: Whether self-referential relationships are symmetric.
299
+ """
300
+ from_table = from_model.get_table_name()
301
+
302
+ if from_table not in cls._m2m_relationships:
303
+ cls._m2m_relationships[from_table] = []
304
+
305
+ cls._m2m_relationships[from_table].append(
306
+ {
307
+ "to_model": to_model,
308
+ "m2m_field": m2m_field,
309
+ "junction_table": junction_table,
310
+ "related_name": related_name,
311
+ "symmetrical": symmetrical,
312
+ }
313
+ )
314
+
315
+ if related_name is None:
316
+ return
317
+
318
+ if from_model is to_model and symmetrical:
319
+ return
320
+
321
+ to_table = to_model.get_table_name()
322
+ pending_info = {
323
+ "from_model": from_model,
324
+ "to_model": to_model,
325
+ "m2m_field": m2m_field,
326
+ "junction_table": junction_table,
327
+ "related_name": related_name,
328
+ "symmetrical": symmetrical,
329
+ }
330
+
331
+ if to_table in cls._models:
332
+ cls._add_m2m_reverse_now(
333
+ from_model=from_model,
334
+ to_model=to_model,
335
+ m2m_field=m2m_field,
336
+ junction_table=junction_table,
337
+ related_name=related_name,
338
+ symmetrical=symmetrical,
339
+ )
340
+ else:
341
+ if to_table not in cls._pending_m2m_reverses:
342
+ cls._pending_m2m_reverses[to_table] = []
343
+ cls._pending_m2m_reverses[to_table].append(pending_info)
344
+
345
+ @classmethod
346
+ def add_pending_m2m_relationship(
347
+ cls,
348
+ *,
349
+ from_model: type[Any],
350
+ to_model_name: str,
351
+ m2m_field: str,
352
+ related_name: Optional[str],
353
+ symmetrical: bool,
354
+ descriptor: _M2MForwardRef,
355
+ ) -> None:
356
+ """Store a pending M2M relationship targeting a string forward ref."""
357
+ existing = cls.get_model_by_name(to_model_name)
358
+ if existing is not None:
359
+ descriptor.resolve_forward_ref(existing)
360
+ junction_table = descriptor.junction_table
361
+ if junction_table is None:
362
+ msg = "ManyToMany junction table could not be resolved."
363
+ raise ValueError(msg)
364
+ cls.add_m2m_relationship(
365
+ from_model=from_model,
366
+ to_model=existing,
367
+ m2m_field=m2m_field,
368
+ junction_table=junction_table,
369
+ related_name=related_name,
370
+ symmetrical=symmetrical,
371
+ )
372
+ return
373
+
374
+ if to_model_name not in cls._pending_m2m_targets:
375
+ cls._pending_m2m_targets[to_model_name] = []
376
+ cls._pending_m2m_targets[to_model_name].append(
377
+ {
378
+ "from_model": from_model,
379
+ "m2m_field": m2m_field,
380
+ "related_name": related_name,
381
+ "symmetrical": symmetrical,
382
+ "descriptor": descriptor,
383
+ }
384
+ )
385
+
386
+ @classmethod
387
+ def _add_m2m_reverse_now(
388
+ cls,
389
+ from_model: type[Any],
390
+ to_model: type[Any],
391
+ m2m_field: str,
392
+ junction_table: str,
393
+ related_name: str,
394
+ *,
395
+ symmetrical: bool = False,
396
+ ) -> None:
397
+ """Add reverse M2M descriptor to the target model.
398
+
399
+ Args:
400
+ from_model: The model defining the ManyToMany field.
401
+ to_model: The target model (receives the descriptor).
402
+ m2m_field: Name of the M2M field.
403
+ junction_table: Name of the junction table.
404
+ related_name: Name for the reverse accessor.
405
+ symmetrical: Whether self-referential relationships are symmetric.
406
+ """
407
+ from sqliter.orm.m2m import ReverseManyToMany # noqa: PLC0415
408
+
409
+ _ = m2m_field # kept for signature consistency / future use
410
+
411
+ if hasattr(to_model, related_name):
412
+ msg = (
413
+ f"Reverse M2M accessor '{related_name}' already "
414
+ f"exists on {to_model.__name__}"
415
+ )
416
+ raise AttributeError(msg)
417
+
418
+ setattr(
419
+ to_model,
420
+ related_name,
421
+ ReverseManyToMany(
422
+ from_model=from_model,
423
+ to_model=to_model,
424
+ junction_table=junction_table,
425
+ related_name=related_name,
426
+ symmetrical=symmetrical,
427
+ ),
428
+ )
429
+
430
+ @classmethod
431
+ def get_m2m_relationships(cls, table_name: str) -> list[dict[str, Any]]:
432
+ """Get M2M relationships for a model.
433
+
434
+ Args:
435
+ table_name: The table name to look up.
436
+
437
+ Returns:
438
+ List of M2M relationship dictionaries.
439
+ """
440
+ return cls._m2m_relationships.get(table_name, [])
sqliter/sqliter.py CHANGED
@@ -678,6 +678,47 @@ class SqliterDB:
678
678
  model_class, model_class.Meta.unique_indexes, unique=True
679
679
  )
680
680
 
681
+ # Create junction tables for M2M relationships
682
+ self._create_m2m_junction_tables(model_class)
683
+
684
+ def _create_m2m_junction_tables(
685
+ self, model_class: type[BaseDBModel]
686
+ ) -> None:
687
+ """Create junction tables for M2M relationships on a model.
688
+
689
+ Queries the ModelRegistry for M2M relationships registered for
690
+ this model and creates the corresponding junction tables.
691
+
692
+ Args:
693
+ model_class: The model class to check for M2M relationships.
694
+ """
695
+ try:
696
+ from sqliter.orm.m2m import ( # noqa: PLC0415
697
+ create_junction_table,
698
+ )
699
+ from sqliter.orm.registry import ( # noqa: PLC0415
700
+ ModelRegistry,
701
+ )
702
+ except ImportError:
703
+ return
704
+
705
+ table_name = model_class.get_table_name()
706
+ m2m_rels = ModelRegistry.get_m2m_relationships(table_name)
707
+
708
+ for rel in m2m_rels:
709
+ junction_table = rel["junction_table"]
710
+ to_model = rel["to_model"]
711
+ to_table = to_model.get_table_name()
712
+
713
+ # Sort alphabetically for consistent column naming
714
+ sorted_tables = sorted([table_name, to_table])
715
+ create_junction_table(
716
+ self,
717
+ junction_table,
718
+ sorted_tables[0],
719
+ sorted_tables[1],
720
+ )
721
+
681
722
  def _create_indexes(
682
723
  self,
683
724
  model_class: type[BaseDBModel],
sqliter/tui/demos/orm.py CHANGED
@@ -3,10 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import io
6
- from typing import Optional
6
+ from typing import Any, Optional, cast
7
7
 
8
8
  from sqliter import SqliterDB
9
- from sqliter.orm import BaseDBModel, ForeignKey
9
+ from sqliter.orm import BaseDBModel, ForeignKey, ManyToMany
10
10
  from sqliter.tui.demos.base import Demo, DemoCategory, extract_demo_code
11
11
 
12
12
 
@@ -203,6 +203,67 @@ def _run_reverse_relationships() -> str:
203
203
  return output.getvalue()
204
204
 
205
205
 
206
+ def _run_many_to_many_basic() -> str:
207
+ """Show basic many-to-many usage with reverse access."""
208
+ output = io.StringIO()
209
+
210
+ class Tag(BaseDBModel):
211
+ name: str
212
+
213
+ class Article(BaseDBModel):
214
+ title: str
215
+ tags: ManyToMany[Tag] = ManyToMany(Tag, related_name="articles")
216
+
217
+ db = SqliterDB(memory=True)
218
+ db.create_table(Tag)
219
+ db.create_table(Article)
220
+
221
+ article = db.insert(Article(title="ORM Guide"))
222
+ python = db.insert(Tag(name="python"))
223
+ orm = db.insert(Tag(name="orm"))
224
+
225
+ article.tags.add(python, orm)
226
+ output.write("Article tags:\n")
227
+ for tag in article.tags.fetch_all():
228
+ output.write(f" - {tag.name}\n")
229
+
230
+ output.write("\nReverse access (tag.articles):\n")
231
+ entries = cast("Any", python.articles).fetch_all()
232
+ for entry in entries:
233
+ output.write(f" - {entry.title}\n")
234
+
235
+ db.close()
236
+ return output.getvalue()
237
+
238
+
239
+ def _run_many_to_many_symmetrical() -> str:
240
+ """Show symmetrical self-referential many-to-many."""
241
+ output = io.StringIO()
242
+
243
+ class User(BaseDBModel):
244
+ name: str
245
+ friends: ManyToMany[User] = ManyToMany("User", symmetrical=True)
246
+
247
+ db = SqliterDB(memory=True)
248
+ db.create_table(User)
249
+
250
+ alice = db.insert(User(name="Alice"))
251
+ bob = db.insert(User(name="Bob"))
252
+
253
+ alice.friends.add(bob)
254
+
255
+ output.write("Alice's friends:\n")
256
+ for friend in alice.friends.fetch_all():
257
+ output.write(f" - {friend.name}\n")
258
+
259
+ output.write("\nBob's friends (symmetrical):\n")
260
+ for friend in bob.friends.fetch_all():
261
+ output.write(f" - {friend.name}\n")
262
+
263
+ db.close()
264
+ return output.getvalue()
265
+
266
+
206
267
  def _run_select_related_basic() -> str:
207
268
  """Demonstrate eager loading with select_related().
208
269
 
@@ -424,6 +485,22 @@ def get_category() -> DemoCategory:
424
485
  code=extract_demo_code(_run_reverse_relationships),
425
486
  execute=_run_reverse_relationships,
426
487
  ),
488
+ Demo(
489
+ id="orm_m2m_basic",
490
+ title="Many-to-Many Basics",
491
+ description="Relate records with a junction table",
492
+ category="orm",
493
+ code=extract_demo_code(_run_many_to_many_basic),
494
+ execute=_run_many_to_many_basic,
495
+ ),
496
+ Demo(
497
+ id="orm_m2m_symmetrical",
498
+ title="Many-to-Many Symmetry",
499
+ description="Self-referential symmetrical relationships",
500
+ category="orm",
501
+ code=extract_demo_code(_run_many_to_many_symmetrical),
502
+ execute=_run_many_to_many_symmetrical,
503
+ ),
427
504
  Demo(
428
505
  id="orm_select_related",
429
506
  title="Eager Loading with select_related()",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqliter-py
3
- Version: 0.16.0
3
+ Version: 0.17.0
4
4
  Summary: Interact with SQLite databases using Python and Pydantic
5
5
  Author: Grant Ramsay
6
6
  Author-email: Grant Ramsay <grant@gnramsay.com>
@@ -50,8 +50,7 @@ is Pydantic itself.
50
50
 
51
51
  It does not aim to be a full-fledged ORM like SQLAlchemy, but rather a simple
52
52
  and easy-to-use library for basic database operations, especially for small
53
- projects. It is NOT asynchronous and does not support complex queries (at this
54
- time).
53
+ projects. It is NOT asynchronous (at this time, though that is planned).
55
54
 
56
55
  The ideal use case is more for Python CLI tools that need to store data in a
57
56
  database-like format without needing to learn SQL or use a full ORM.
@@ -60,11 +59,10 @@ Full documentation is available on the [Website](https://sqliter.grantramsay.dev
60
59
 
61
60
  > [!CAUTION]
62
61
  >
63
- > This project is still in the early stages of development and is lacking some
64
- > planned functionality. Please use with caution - Classes and methods may
65
- > change until a stable release is made. I'll try to keep this to an absolute
66
- > minimum and the releases and documentation will be very clear about any
67
- > breaking changes.
62
+ > This project is still in development and is lacking some planned
63
+ > functionality. Please use with caution - Classes and methods may change until
64
+ > a stable release is made. I'll try to keep this to an absolute minimum and the
65
+ > releases and documentation will be very clear about any breaking changes.
68
66
  >
69
67
  > See the [TODO](TODO.md) for planned features and improvements.
70
68