sqliter-py 0.12.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/constants.py +4 -3
- sqliter/exceptions.py +29 -0
- sqliter/helpers.py +27 -0
- sqliter/model/model.py +21 -4
- sqliter/orm/__init__.py +17 -0
- sqliter/orm/fields.py +412 -0
- sqliter/orm/foreign_key.py +8 -0
- sqliter/orm/m2m.py +784 -0
- sqliter/orm/model.py +308 -0
- sqliter/orm/query.py +221 -0
- sqliter/orm/registry.py +440 -0
- sqliter/query/query.py +573 -51
- sqliter/sqliter.py +182 -47
- sqliter/tui/__init__.py +62 -0
- sqliter/tui/__main__.py +6 -0
- sqliter/tui/app.py +179 -0
- sqliter/tui/demos/__init__.py +96 -0
- sqliter/tui/demos/base.py +114 -0
- sqliter/tui/demos/caching.py +283 -0
- sqliter/tui/demos/connection.py +150 -0
- sqliter/tui/demos/constraints.py +211 -0
- sqliter/tui/demos/crud.py +154 -0
- sqliter/tui/demos/errors.py +231 -0
- sqliter/tui/demos/field_selection.py +150 -0
- sqliter/tui/demos/filters.py +389 -0
- sqliter/tui/demos/models.py +248 -0
- sqliter/tui/demos/ordering.py +156 -0
- sqliter/tui/demos/orm.py +537 -0
- sqliter/tui/demos/results.py +241 -0
- sqliter/tui/demos/string_filters.py +210 -0
- sqliter/tui/demos/timestamps.py +126 -0
- sqliter/tui/demos/transactions.py +177 -0
- sqliter/tui/runner.py +116 -0
- sqliter/tui/styles/app.tcss +130 -0
- sqliter/tui/widgets/__init__.py +7 -0
- sqliter/tui/widgets/code_display.py +81 -0
- sqliter/tui/widgets/demo_list.py +65 -0
- sqliter/tui/widgets/output_display.py +92 -0
- {sqliter_py-0.12.0.dist-info → sqliter_py-0.17.0.dist-info}/METADATA +28 -14
- sqliter_py-0.17.0.dist-info/RECORD +48 -0
- {sqliter_py-0.12.0.dist-info → sqliter_py-0.17.0.dist-info}/WHEEL +2 -2
- sqliter_py-0.17.0.dist-info/entry_points.txt +3 -0
- sqliter_py-0.12.0.dist-info/RECORD +0 -15
sqliter/orm/registry.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""Model registry for ORM functionality.
|
|
2
|
+
|
|
3
|
+
Central registry for:
|
|
4
|
+
- Model classes by table name
|
|
5
|
+
- Foreign key relationships
|
|
6
|
+
- Pending reverse relationships
|
|
7
|
+
- Many-to-many relationships
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
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]]]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ModelRegistry:
|
|
34
|
+
"""Registry for ORM models, FK, and M2M relationships.
|
|
35
|
+
|
|
36
|
+
Uses automatic setup via descriptor __set_name__ hook - no manual setup
|
|
37
|
+
required.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_models: ClassVar[dict[str, type]] = {}
|
|
41
|
+
_models_by_name: ClassVar[dict[str, type[Any]]] = {}
|
|
42
|
+
_foreign_keys: ClassVar[dict[str, list[dict[str, Any]]]] = {}
|
|
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"]
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def register_model(cls, model_class: type[Any]) -> None:
|
|
93
|
+
"""Register a model class in the global registry.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
model_class: The model class to register
|
|
97
|
+
"""
|
|
98
|
+
table_name = model_class.get_table_name()
|
|
99
|
+
cls._models[table_name] = model_class
|
|
100
|
+
cls._models_by_name[model_class.__name__] = model_class
|
|
101
|
+
|
|
102
|
+
# Process any pending reverse relationships for this model
|
|
103
|
+
if table_name in cls._pending_reverses:
|
|
104
|
+
for pending in cls._pending_reverses[table_name]:
|
|
105
|
+
cls._add_reverse_relationship_now(**pending)
|
|
106
|
+
del cls._pending_reverses[table_name]
|
|
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
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def register_foreign_key(
|
|
142
|
+
cls,
|
|
143
|
+
from_model: type[Any],
|
|
144
|
+
to_model: type[Any],
|
|
145
|
+
fk_field: str,
|
|
146
|
+
on_delete: str,
|
|
147
|
+
related_name: Optional[str] = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Register a FK relationship.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
from_model: The model with the FK field
|
|
153
|
+
to_model: The model being referenced
|
|
154
|
+
fk_field: Name of the FK field
|
|
155
|
+
on_delete: Action when related object is deleted
|
|
156
|
+
related_name: Name for reverse relationship
|
|
157
|
+
"""
|
|
158
|
+
from_table = from_model.get_table_name()
|
|
159
|
+
|
|
160
|
+
if from_table not in cls._foreign_keys:
|
|
161
|
+
cls._foreign_keys[from_table] = []
|
|
162
|
+
|
|
163
|
+
cls._foreign_keys[from_table].append(
|
|
164
|
+
{
|
|
165
|
+
"to_model": to_model,
|
|
166
|
+
"fk_field": fk_field,
|
|
167
|
+
"on_delete": on_delete,
|
|
168
|
+
"related_name": related_name,
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def get_model(cls, table_name: str) -> Optional[type[Any]]:
|
|
174
|
+
"""Get model by table name.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
table_name: The table name to look up
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The model class or None if not found
|
|
181
|
+
"""
|
|
182
|
+
return cls._models.get(table_name)
|
|
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
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def get_foreign_keys(cls, table_name: str) -> list[dict[str, Any]]:
|
|
191
|
+
"""Get FK relationships for a model.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
table_name: The table name to look up
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
List of FK relationship dictionaries
|
|
198
|
+
"""
|
|
199
|
+
return cls._foreign_keys.get(table_name, [])
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def add_reverse_relationship(
|
|
203
|
+
cls,
|
|
204
|
+
from_model: type[Any],
|
|
205
|
+
to_model: type[Any],
|
|
206
|
+
fk_field: str,
|
|
207
|
+
related_name: str,
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Automatically add reverse relationship descriptor during class.
|
|
210
|
+
|
|
211
|
+
Called by ForeignKeyDescriptor.__set_name__ during class creation.
|
|
212
|
+
If to_model doesn't exist yet, stores as pending and adds when
|
|
213
|
+
to_model is registered.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
from_model: The model with the FK field (e.g., Book)
|
|
217
|
+
to_model: The model being referenced (e.g., Author)
|
|
218
|
+
fk_field: Name of the FK field (e.g., "author")
|
|
219
|
+
related_name: Name for reverse relationship (e.g., "books")
|
|
220
|
+
"""
|
|
221
|
+
to_table = to_model.get_table_name()
|
|
222
|
+
|
|
223
|
+
# Check if to_model has been registered yet
|
|
224
|
+
if to_table in cls._models:
|
|
225
|
+
# Model exists, add reverse relationship now
|
|
226
|
+
cls._add_reverse_relationship_now(
|
|
227
|
+
from_model, to_model, fk_field, related_name
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
# Model doesn't exist yet, store as pending
|
|
231
|
+
if to_table not in cls._pending_reverses:
|
|
232
|
+
cls._pending_reverses[to_table] = []
|
|
233
|
+
cls._pending_reverses[to_table].append(
|
|
234
|
+
{
|
|
235
|
+
"from_model": from_model,
|
|
236
|
+
"to_model": to_model,
|
|
237
|
+
"fk_field": fk_field,
|
|
238
|
+
"related_name": related_name,
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
@classmethod
|
|
243
|
+
def _add_reverse_relationship_now(
|
|
244
|
+
cls,
|
|
245
|
+
from_model: type[Any],
|
|
246
|
+
to_model: type[Any],
|
|
247
|
+
fk_field: str,
|
|
248
|
+
related_name: str,
|
|
249
|
+
) -> None:
|
|
250
|
+
"""Add reverse relationship descriptor to model.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
from_model: The model with the FK field (e.g., Book)
|
|
254
|
+
to_model: The model being referenced (e.g., Author)
|
|
255
|
+
fk_field: Name of the FK field (e.g., "author")
|
|
256
|
+
related_name: Name for reverse relationship (e.g., "books")
|
|
257
|
+
"""
|
|
258
|
+
from sqliter.orm.query import ReverseRelationship # noqa: PLC0415
|
|
259
|
+
|
|
260
|
+
# Guard against overwriting existing attributes
|
|
261
|
+
if hasattr(to_model, related_name):
|
|
262
|
+
msg = (
|
|
263
|
+
f"Reverse relationship '{related_name}' already exists on "
|
|
264
|
+
f"{to_model.__name__}"
|
|
265
|
+
)
|
|
266
|
+
raise AttributeError(msg)
|
|
267
|
+
|
|
268
|
+
# Add reverse relationship descriptor to to_model
|
|
269
|
+
setattr(
|
|
270
|
+
to_model,
|
|
271
|
+
related_name,
|
|
272
|
+
ReverseRelationship(from_model, fk_field, related_name),
|
|
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, [])
|