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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgrid_orm
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Shotgrid SQLAlchemy ORM Generator - creates Python classes from a schema.
5
5
  Author-email: John Em Tran <johnemtran@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shotgrid_orm"
3
- version = "0.3.1"
3
+ version = "0.3.3"
4
4
  authors = [
5
5
  { name="John Em Tran", email="johnemtran@gmail.com" },
6
6
  ]
@@ -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
- # Single valid type: FK to target table; _type column is redundant.
262
- t_namespace[f"{field_code}_id"] = mapped_column(
263
- sa.BigInteger, sa.ForeignKey(f"{valid_types[0]}.id")
264
- )
265
- self.info(f" -> FK to {valid_types[0]}.id, no _type column")
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)},
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgrid_orm
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Shotgrid SQLAlchemy ORM Generator - creates Python classes from a schema.
5
5
  Author-email: John Em Tran <johnemtran@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -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