shotgrid-orm 0.3.1__tar.gz → 0.3.3__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.
- {shotgrid_orm-0.3.1/src/shotgrid_orm.egg-info → shotgrid_orm-0.3.3}/PKG-INFO +1 -1
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/pyproject.toml +1 -1
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/src/shotgrid_orm/classes.py +10 -5
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/src/shotgrid_orm/sgtypes.py +2 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3/src/shotgrid_orm.egg-info}/PKG-INFO +1 -1
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/tests/test_sgorm.py +119 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/LICENSE +0 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/README.md +0 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/setup.cfg +0 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/src/shotgrid_orm/__init__.py +0 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/src/shotgrid_orm.egg-info/SOURCES.txt +0 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/src/shotgrid_orm.egg-info/dependency_links.txt +0 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/src/shotgrid_orm.egg-info/requires.txt +0 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/src/shotgrid_orm.egg-info/top_level.txt +0 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/tests/test_createdb.py +0 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/tests/test_createscript.py +0 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/tests/test_index.py +0 -0
- {shotgrid_orm-0.3.1 → shotgrid_orm-0.3.3}/tests/test_model.py +0 -0
|
@@ -258,11 +258,16 @@ class SGORM:
|
|
|
258
258
|
if field_type_value == "entity":
|
|
259
259
|
t_annotations[f"{field_code}_id"] = Mapped[Optional[int]]
|
|
260
260
|
if len(valid_types) == 1:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
261
|
+
v_table = valid_types[0]
|
|
262
|
+
if v_table in self.sg_schema and v_table not in self.ignored_tables:
|
|
263
|
+
# Single valid type: FK to target table; _type column is redundant.
|
|
264
|
+
t_namespace[f"{field_code}_id"] = mapped_column(
|
|
265
|
+
sa.BigInteger, sa.ForeignKey(f"{v_table}.id")
|
|
266
|
+
)
|
|
267
|
+
self.info(f" -> FK to {v_table}.id, no _type column")
|
|
268
|
+
else:
|
|
269
|
+
t_namespace[f"{field_code}_id"] = mapped_column(sa.BigInteger)
|
|
270
|
+
self.info(f" -> {v_table} not in schema, plain BigInteger (no FK)")
|
|
266
271
|
else:
|
|
267
272
|
# Zero or multiple valid types: keep _type for runtime disambiguation.
|
|
268
273
|
# NOTE: For ORM navigation of polymorphic refs, consider
|
|
@@ -36,6 +36,7 @@ sg_types = {
|
|
|
36
36
|
"currency": {"hint": Mapped[float], "type": mapped_column(Float)},
|
|
37
37
|
"date": {"hint": Mapped[str], "type": mapped_column(String)},
|
|
38
38
|
"date_time": {"hint": Mapped[datetime], "type": mapped_column(DateTime)},
|
|
39
|
+
"duration": {"hint": Mapped[int], "type": mapped_column(Integer)},
|
|
39
40
|
# NOTE: Entity types are stored as integer IDs. Relationship mappings are handled
|
|
40
41
|
# in classes.py by creating _id and _type fields for polymorphic associations.
|
|
41
42
|
# Full SQLAlchemy relationship() support could be added in future versions.
|
|
@@ -71,6 +72,7 @@ sg_types_optional = {
|
|
|
71
72
|
"currency": {"hint": Mapped[Optional[float]], "type": mapped_column(Float)},
|
|
72
73
|
"date": {"hint": Mapped[Optional[str]], "type": mapped_column(String)},
|
|
73
74
|
"date_time": {"hint": Mapped[Optional[datetime]], "type": mapped_column(DateTime)},
|
|
75
|
+
"duration": {"hint": Mapped[Optional[int]], "type": mapped_column(Integer)},
|
|
74
76
|
# NOTE: Entity types are stored as integer IDs. Relationship mappings are handled
|
|
75
77
|
# in classes.py by creating _id and _type fields for polymorphic associations.
|
|
76
78
|
"entity": {"hint": Mapped[Optional[int]], "type": mapped_column(Integer)},
|
|
@@ -197,6 +197,66 @@ def test_multi_entity_field_multi_valid_type_keeps_type_col(sg_orm):
|
|
|
197
197
|
assert hasattr(Asset, "task_assignees_type")
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
def test_dangling_fk_not_generated(tmp_path):
|
|
201
|
+
"""entity field whose single valid_type is absent from the schema must not emit a FK.
|
|
202
|
+
|
|
203
|
+
Reproduces: 'Foreign key associated with column ... could not find table ...'
|
|
204
|
+
which previously crashed metadata.create_all() for internal SG entities like
|
|
205
|
+
AppWelcome that are referenced by other tables but not present in the schema.
|
|
206
|
+
"""
|
|
207
|
+
import json
|
|
208
|
+
|
|
209
|
+
schema = {
|
|
210
|
+
"Child": {
|
|
211
|
+
"name": {"visible": True},
|
|
212
|
+
"fields": {
|
|
213
|
+
"id": {
|
|
214
|
+
"data_type": {"value": "number"},
|
|
215
|
+
"editable": False,
|
|
216
|
+
"entity_type": {"value": "Child"},
|
|
217
|
+
"mandatory": {"value": False},
|
|
218
|
+
"name": {"value": "id"},
|
|
219
|
+
"properties": {"default_value": {"value": None}},
|
|
220
|
+
"unique": False,
|
|
221
|
+
"visible": {"value": True},
|
|
222
|
+
},
|
|
223
|
+
"ghost_link": {
|
|
224
|
+
# Single valid type that is NOT in this schema
|
|
225
|
+
"data_type": {"value": "entity"},
|
|
226
|
+
"editable": True,
|
|
227
|
+
"entity_type": {"value": "Child"},
|
|
228
|
+
"mandatory": {"value": False},
|
|
229
|
+
"name": {"value": "ghost_link"},
|
|
230
|
+
"properties": {
|
|
231
|
+
"default_value": {"value": None},
|
|
232
|
+
"valid_types": {"value": ["GhostEntity"]},
|
|
233
|
+
},
|
|
234
|
+
"unique": False,
|
|
235
|
+
"visible": {"value": True},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
schema_path = tmp_path / "schema.json"
|
|
242
|
+
schema_path.write_text(json.dumps(schema))
|
|
243
|
+
|
|
244
|
+
orm = SGORM(sg_schema_type=SchemaType.JSON_FILE, sg_schema_source=str(schema_path), echo=False)
|
|
245
|
+
|
|
246
|
+
# Must not raise when creating tables — no dangling FK
|
|
247
|
+
engine = create_engine("sqlite+pysqlite:///:memory:", echo=False)
|
|
248
|
+
orm.Base.metadata.create_all(engine) # Would raise before the fix
|
|
249
|
+
|
|
250
|
+
# The column must exist but without a FK constraint
|
|
251
|
+
inspector = inspect(engine)
|
|
252
|
+
fks = inspector.get_foreign_keys("Child")
|
|
253
|
+
ghost_fks = [fk for fk in fks if "ghost_link_id" in fk["constrained_columns"]]
|
|
254
|
+
assert len(ghost_fks) == 0, "No FK should be generated for a target table absent from the schema"
|
|
255
|
+
|
|
256
|
+
Child = orm["Child"]
|
|
257
|
+
assert hasattr(Child, "ghost_link_id"), "ghost_link_id column must still be present"
|
|
258
|
+
|
|
259
|
+
|
|
200
260
|
def test_entity_field_fk_enforced_in_db(sg_orm, test_db_path):
|
|
201
261
|
"""FK constraints appear on single-type entity fields but not on multi-type fields."""
|
|
202
262
|
from sqlalchemy import create_engine
|
|
@@ -216,3 +276,62 @@ def test_entity_field_fk_enforced_in_db(sg_orm, test_db_path):
|
|
|
216
276
|
asset_fks = inspector.get_foreign_keys("Asset")
|
|
217
277
|
entity_source_fks = [fk for fk in asset_fks if "entity_source_id" in fk["constrained_columns"]]
|
|
218
278
|
assert len(entity_source_fks) == 0, "Asset.entity_source_id must NOT have a FK (polymorphic field)"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_duration_field_type(tmp_path):
|
|
282
|
+
"""duration fields are numeric (integer minutes) and persist as Integer columns."""
|
|
283
|
+
import json
|
|
284
|
+
|
|
285
|
+
from sqlalchemy.types import Integer
|
|
286
|
+
|
|
287
|
+
schema = {
|
|
288
|
+
"Task": {
|
|
289
|
+
"name": {"visible": True},
|
|
290
|
+
"fields": {
|
|
291
|
+
"id": {
|
|
292
|
+
"data_type": {"value": "number"},
|
|
293
|
+
"editable": False,
|
|
294
|
+
"entity_type": {"value": "Task"},
|
|
295
|
+
"mandatory": {"value": False},
|
|
296
|
+
"name": {"value": "id"},
|
|
297
|
+
"properties": {"default_value": {"value": None}},
|
|
298
|
+
"unique": False,
|
|
299
|
+
"visible": {"value": True},
|
|
300
|
+
},
|
|
301
|
+
"duration": {
|
|
302
|
+
"data_type": {"value": "duration"},
|
|
303
|
+
"editable": True,
|
|
304
|
+
"entity_type": {"value": "Task"},
|
|
305
|
+
"mandatory": {"value": False},
|
|
306
|
+
"name": {"value": "duration"},
|
|
307
|
+
"properties": {"default_value": {"value": None}},
|
|
308
|
+
"unique": False,
|
|
309
|
+
"visible": {"value": True},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
schema_path = tmp_path / "schema.json"
|
|
316
|
+
schema_path.write_text(json.dumps(schema))
|
|
317
|
+
|
|
318
|
+
orm = SGORM(sg_schema_type=SchemaType.JSON_FILE, sg_schema_source=str(schema_path), echo=False)
|
|
319
|
+
Task = orm["Task"]
|
|
320
|
+
assert Task is not None
|
|
321
|
+
assert hasattr(Task, "duration")
|
|
322
|
+
|
|
323
|
+
engine = create_engine("sqlite+pysqlite:///:memory:", echo=False)
|
|
324
|
+
orm.Base.metadata.create_all(engine)
|
|
325
|
+
|
|
326
|
+
# Duration column must map to an integer type.
|
|
327
|
+
duration_col = next(c for c in Task.__table__.columns if c.name == "duration")
|
|
328
|
+
assert isinstance(duration_col.type, Integer)
|
|
329
|
+
|
|
330
|
+
# Round-trip a value to ensure persistence works.
|
|
331
|
+
session = Session(engine)
|
|
332
|
+
task = Task(id=1, duration=120)
|
|
333
|
+
session.add(task)
|
|
334
|
+
session.commit()
|
|
335
|
+
retrieved = session.get(Task, 1)
|
|
336
|
+
assert retrieved.duration == 120
|
|
337
|
+
session.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|