liminal-orm 1.0.6__py3-none-any.whl → 1.1.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.
Files changed (34) hide show
  1. liminal/base/base_operation.py +9 -11
  2. liminal/base/properties/base_field_properties.py +11 -0
  3. liminal/base/properties/base_schema_properties.py +6 -2
  4. liminal/cli/live_test_dropdown_migration.py +8 -9
  5. liminal/cli/live_test_entity_schema_migration.py +24 -27
  6. liminal/connection/benchling_connection.py +8 -2
  7. liminal/connection/benchling_service.py +11 -18
  8. liminal/dropdowns/operations.py +1 -1
  9. liminal/entity_schemas/compare.py +17 -7
  10. liminal/entity_schemas/entity_schema_models.py +6 -9
  11. liminal/entity_schemas/generate_files.py +7 -6
  12. liminal/entity_schemas/operations.py +77 -102
  13. liminal/entity_schemas/tag_schema_models.py +7 -2
  14. liminal/entity_schemas/utils.py +4 -2
  15. liminal/enums/benchling_api_field_type.py +3 -0
  16. liminal/enums/benchling_entity_type.py +1 -0
  17. liminal/enums/benchling_field_type.py +4 -0
  18. liminal/enums/benchling_folder_item_type.py +1 -0
  19. liminal/external/__init__.py +0 -2
  20. liminal/mappers.py +25 -1
  21. liminal/migrate/components.py +9 -4
  22. liminal/orm/base_model.py +8 -7
  23. liminal/orm/column.py +6 -3
  24. liminal/orm/mixins.py +9 -0
  25. liminal/orm/schema_properties.py +8 -2
  26. liminal/tests/conftest.py +52 -22
  27. liminal/tests/test_entity_schema_compare.py +11 -5
  28. liminal/utils.py +11 -4
  29. {liminal_orm-1.0.6.dist-info → liminal_orm-1.1.0.dist-info}/METADATA +3 -2
  30. liminal_orm-1.1.0.dist-info/RECORD +61 -0
  31. liminal_orm-1.0.6.dist-info/RECORD +0 -61
  32. {liminal_orm-1.0.6.dist-info → liminal_orm-1.1.0.dist-info}/LICENSE.md +0 -0
  33. {liminal_orm-1.0.6.dist-info → liminal_orm-1.1.0.dist-info}/WHEEL +0 -0
  34. {liminal_orm-1.0.6.dist-info → liminal_orm-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -23,6 +23,7 @@ from liminal.entity_schemas.utils import (
23
23
  )
24
24
  from liminal.enums.benchling_naming_strategy import BenchlingNamingStrategy
25
25
  from liminal.orm.schema_properties import SchemaProperties
26
+ from liminal.utils import to_snake_case
26
27
 
27
28
 
28
29
  class CreateEntitySchema(BaseOperation):
@@ -31,7 +32,7 @@ class CreateEntitySchema(BaseOperation):
31
32
  def __init__(
32
33
  self,
33
34
  schema_properties: BaseSchemaProperties,
34
- fields: dict[str, BaseFieldProperties],
35
+ fields: list[BaseFieldProperties],
35
36
  ) -> None:
36
37
  self.schema_properties = schema_properties
37
38
  self.fields = fields
@@ -64,11 +65,23 @@ class CreateEntitySchema(BaseOperation):
64
65
  )
65
66
 
66
67
  def describe_operation(self) -> str:
67
- return f"{self._validated_schema_properties.name}: Creating new entity schema with fields: {','.join(self.fields.keys())}."
68
+ return f"{self._validated_schema_properties.name}: Creating new entity schema with fields: {','.join([f.warehouse_name for f in self.fields if f.warehouse_name is not None])}."
68
69
 
69
70
  def describe(self) -> str:
70
71
  return f"{self._validated_schema_properties.name}: Schema is defined in code but not in Benchling."
71
72
 
73
+ def validate(self, benchling_service: BenchlingService) -> None:
74
+ if (
75
+ not benchling_service.connection.warehouse_access
76
+ and self._validated_schema_properties.warehouse_name
77
+ != to_snake_case(self._validated_schema_properties.name)
78
+ ):
79
+ raise ValueError(
80
+ f"Warehouse access is required to set a custom field warehouse name. \
81
+ Either set warehouse_access to True in BenchlingConnection or set the schema warehouse_name to the given Benchling warehouse name: {to_snake_case(self._validated_schema_properties.name)}. \
82
+ Reach out to Benchling support if you need help setting up warehouse access."
83
+ )
84
+
72
85
  def _validate_create(self, benchling_service: BenchlingService) -> None:
73
86
  all_schemas = TagSchemaModel.get_all_json(benchling_service)
74
87
  if self._validated_schema_properties.name in [
@@ -79,12 +92,6 @@ class CreateEntitySchema(BaseOperation):
79
92
  )
80
93
  if self._validated_schema_properties.warehouse_name in [
81
94
  schema["sqlIdentifier"] for schema in all_schemas
82
- ]:
83
- raise ValueError(
84
- f"Entity schema name {self._validated_schema_properties.name} already exists in Benchling."
85
- )
86
- if self._validated_schema_properties.warehouse_name in [
87
- schema["sqlIdentifier"] for schema in all_schemas
88
95
  ]:
89
96
  raise ValueError(
90
97
  f"Entity schema warehouse name {self._validated_schema_properties.warehouse_name} already exists in Benchling."
@@ -117,7 +124,7 @@ class CreateEntitySchema(BaseOperation):
117
124
  )
118
125
  if (
119
126
  self._validated_schema_properties != benchling_schema_props
120
- or self.fields != benchling_fields_props
127
+ or self.fields != [f for _, f in benchling_fields_props.items()]
121
128
  ):
122
129
  raise ValueError(
123
130
  f"Entity schema {self._validated_schema_properties.warehouse_name} is different in code versus Benchling. Cannot unarchive."
@@ -200,6 +207,17 @@ class UpdateEntitySchema(BaseOperation):
200
207
  def describe(self) -> str:
201
208
  return f"Schema properties for {self.wh_schema_name} are different in code versus Benchling: {str(self.update_props)}."
202
209
 
210
+ def validate(self, benchling_service: BenchlingService) -> None:
211
+ if (
212
+ benchling_service.connection.warehouse_access
213
+ and self.update_props.warehouse_name is not None
214
+ ):
215
+ raise ValueError(
216
+ "Warehouse access is required to change the schema warehouse name. \
217
+ Either set warehouse_access to True in BenchlingConnection or do not change the warehouse name. \
218
+ Reach out to Benchling support if you need help setting up warehouse access."
219
+ )
220
+
203
221
  def _validate(self, benchling_service: BenchlingService) -> TagSchemaModel:
204
222
  all_schemas = TagSchemaModel.get_all_json(benchling_service)
205
223
  tag_schema = TagSchemaModel.get_one(
@@ -235,69 +253,30 @@ class UpdateEntitySchema(BaseOperation):
235
253
  return tag_schema
236
254
 
237
255
 
238
- class UpdateEntitySchemaName(BaseOperation):
239
- order: ClassVar[int] = 100
240
-
241
- def __init__(
242
- self,
243
- old_wh_schema_name: str,
244
- new_wh_schema_name: str,
245
- ) -> None:
246
- self.old_wh_schema_name = old_wh_schema_name
247
- self.new_wh_schema_name = new_wh_schema_name
248
-
249
- def execute(self, benchling_service: BenchlingService) -> dict[str, Any]:
250
- tag_schema = self._validate(benchling_service)
251
- update = UpdateTagSchemaModel(sqlIdentifier=self.new_wh_schema_name)
252
- return update_tag_schema(
253
- benchling_service, tag_schema.id, update.model_dump(exclude_unset=True)
254
- )
255
-
256
- def _validate(self, benchling_service: BenchlingService) -> TagSchemaModel:
257
- all_schemas = TagSchemaModel.get_all(benchling_service)
258
- tag_schema = next(
259
- (s for s in all_schemas if s.sqlIdentifier == self.old_wh_schema_name), None
260
- )
261
- if tag_schema is None:
262
- raise ValueError(
263
- f"Entity schema {self.old_wh_schema_name} does not exist in Benchling."
264
- )
265
- new_tag_schema = next(
266
- (s for s in all_schemas if s.sqlIdentifier == self.new_wh_schema_name), None
267
- )
268
- if new_tag_schema is not None:
269
- raise ValueError(
270
- f"Entity schema warehouse name {self.new_wh_schema_name} already exists in Benchling."
271
- )
272
- return tag_schema
273
-
274
- def describe_operation(self) -> str:
275
- return f"{self.old_wh_schema_name}: Renaming entity schema warehouse name to {self.new_wh_schema_name}."
276
-
277
- def describe(self) -> str:
278
- return f"{self.old_wh_schema_name}: Entity schema in Benchling has a different warehouse name than in code."
279
-
280
-
281
256
  class CreateEntitySchemaField(BaseOperation):
282
257
  order: ClassVar[int] = 120
283
258
 
284
259
  def __init__(
285
260
  self,
286
261
  wh_schema_name: str,
287
- wh_field_name: str,
288
262
  field_props: BaseFieldProperties,
289
263
  index: int,
290
264
  ) -> None:
291
265
  self.wh_schema_name = wh_schema_name
292
- self.wh_field_name = wh_field_name
293
266
  self.field_props = field_props
294
267
  self.index = index
295
268
 
269
+ self._wh_field_name: str
270
+ if field_props.warehouse_name:
271
+ self._wh_field_name = field_props.warehouse_name
272
+ else:
273
+ raise ValueError("Field warehouse name is required.")
274
+
296
275
  def execute(self, benchling_service: BenchlingService) -> dict[str, Any]:
297
276
  try:
298
277
  field = TagSchemaModel.get_one(
299
278
  benchling_service, self.wh_schema_name
300
- ).get_field(self.wh_field_name)
279
+ ).get_field(self._wh_field_name)
301
280
  except ValueError:
302
281
  field = None
303
282
  if field is None:
@@ -305,35 +284,35 @@ class CreateEntitySchemaField(BaseOperation):
305
284
  else:
306
285
  if field.archiveRecord is None:
307
286
  raise ValueError(
308
- f"Field {self.wh_field_name} is already active on entity schema {self.wh_schema_name}."
287
+ f"Field {self._wh_field_name} is already active on entity schema {self.wh_schema_name}."
309
288
  )
310
289
  dropdowns_map = get_benchling_dropdown_id_name_map(benchling_service)
311
290
  if self.field_props == convert_tag_schema_field_to_field_properties(
312
291
  field, dropdowns_map
313
292
  ):
314
293
  return UnarchiveEntitySchemaField(
315
- self.wh_schema_name, self.wh_field_name, self.index
294
+ self.wh_schema_name, self._wh_field_name, self.index
316
295
  ).execute(benchling_service)
317
296
  else:
318
297
  raise ValueError(
319
- f"Field {self.wh_field_name} on entity schema {self.wh_schema_name} is different in code versus Benchling."
298
+ f"Field {self._wh_field_name} on entity schema {self.wh_schema_name} is different in code versus Benchling."
320
299
  )
321
300
 
322
301
  def _execute_create(self, benchling_service: BenchlingService) -> dict[str, Any]:
323
302
  tag_schema = TagSchemaModel.get_one(benchling_service, self.wh_schema_name)
324
303
  existing_new_field = next(
325
- (f for f in tag_schema.allFields if f.systemName == self.wh_field_name),
304
+ (f for f in tag_schema.allFields if f.systemName == self._wh_field_name),
326
305
  None,
327
306
  )
328
307
  if existing_new_field:
329
308
  raise ValueError(
330
- f"Field {self.wh_field_name} already exists on entity schema {self.wh_schema_name} and is {'archived' if existing_new_field.archiveRecord is not None else 'active'} in Benchling."
309
+ f"Field {self._wh_field_name} already exists on entity schema {self.wh_schema_name} and is {'archived' if existing_new_field.archiveRecord is not None else 'active'} in Benchling."
331
310
  )
332
311
  index_to_insert = (
333
312
  self.index if self.index is not None else len(tag_schema.allFields)
334
313
  )
335
314
  new_field = CreateTagSchemaFieldModel.from_props(
336
- self.wh_field_name, self.field_props, benchling_service
315
+ self.field_props, benchling_service
337
316
  )
338
317
  fields_for_update = tag_schema.allFields
339
318
  fields_for_update.insert(index_to_insert, new_field) # type: ignore
@@ -344,10 +323,21 @@ class CreateEntitySchemaField(BaseOperation):
344
323
  )
345
324
 
346
325
  def describe_operation(self) -> str:
347
- return f"{self.wh_schema_name}: Creating entity schema field '{self.wh_field_name}' at index {self.index}."
326
+ return f"{self.wh_schema_name}: Creating entity schema field '{self._wh_field_name}' at index {self.index}."
348
327
 
349
328
  def describe(self) -> str:
350
- return f"{self.wh_schema_name}: Entity schema field '{self.wh_field_name}' is not defined in Benchling but is defined in code."
329
+ return f"{self.wh_schema_name}: Entity schema field '{self._wh_field_name}' is not defined in Benchling but is defined in code."
330
+
331
+ def validate(self, benchling_service: BenchlingService) -> None:
332
+ if (
333
+ not benchling_service.connection.warehouse_access
334
+ and self.field_props.warehouse_name != to_snake_case(self.field_props.name)
335
+ ):
336
+ raise ValueError(
337
+ f"Warehouse access is required to set a custom field warehouse name. \
338
+ Either set warehouse_access to True in BenchlingConnection or set the column variable name to the given Benchling field warehouse name: {to_snake_case(self.field_props.name)}. \
339
+ Reach out to Benchling support if you need help setting up warehouse access."
340
+ )
351
341
 
352
342
 
353
343
  class ArchiveEntitySchemaField(BaseOperation):
@@ -480,6 +470,17 @@ class UpdateEntitySchemaField(BaseOperation):
480
470
  def describe(self) -> str:
481
471
  return f"{self.wh_schema_name}: Entity schema field '{self.wh_field_name}' in Benchling is different than in code: {str(self.update_props)}."
482
472
 
473
+ def validate(self, benchling_service: BenchlingService) -> None:
474
+ if (
475
+ not benchling_service.connection.warehouse_access
476
+ and self.update_props.warehouse_name is not None
477
+ ):
478
+ raise ValueError(
479
+ "Warehouse access is required to change the field warehouse name. \
480
+ Either set warehouse_access to True in BenchlingConnection or do not change the warehouse name. \
481
+ Reach out to Benchling support if you need help setting up warehouse access."
482
+ )
483
+
483
484
  def _validate(self, benchling_service: BenchlingService) -> TagSchemaModel:
484
485
  tag_schema = TagSchemaModel.get_one(benchling_service, self.wh_schema_name)
485
486
  # Only if changing name of field
@@ -492,46 +493,20 @@ class UpdateEntitySchemaField(BaseOperation):
492
493
  raise ValueError(
493
494
  f"New field name {self.update_props.name} already exists on entity schema {self.wh_schema_name} and is {'archived' if existing_new_field.archiveRecord is not None else 'active'} in Benchling."
494
495
  )
495
- return tag_schema
496
-
497
-
498
- class UpdateEntitySchemaFieldName(BaseOperation):
499
- order: ClassVar[int] = 150
500
-
501
- def __init__(
502
- self,
503
- wh_schema_name: str,
504
- old_wh_field_name: str,
505
- new_wh_field_name: str,
506
- ) -> None:
507
- self.wh_schema_name = wh_schema_name
508
- self.old_wh_field_name = old_wh_field_name
509
- self.new_wh_field_name = new_wh_field_name
510
-
511
- def execute(self, benchling_service: BenchlingService) -> dict[str, Any]:
512
- tag_schema = TagSchemaModel.get_one(benchling_service, self.wh_schema_name)
513
- existing_new_field = next(
514
- (f for f in tag_schema.allFields if f.systemName == self.new_wh_field_name),
515
- None,
516
- )
517
- if existing_new_field is not None:
518
- raise ValueError(
519
- f"Field {self.new_wh_field_name} already exists on entity schema {self.wh_schema_name}."
496
+ if self.update_props.warehouse_name:
497
+ existing_new_field = next(
498
+ (
499
+ f
500
+ for f in tag_schema.allFields
501
+ if f.systemName == self.update_props.warehouse_name
502
+ ),
503
+ None,
520
504
  )
521
- updated_tag_schema = tag_schema.update_field_wh_name(
522
- self.old_wh_field_name, self.new_wh_field_name
523
- )
524
- return update_tag_schema(
525
- benchling_service,
526
- tag_schema.id,
527
- {"fields": [f.model_dump() for f in updated_tag_schema.allFields]},
528
- )
529
-
530
- def describe_operation(self) -> str:
531
- return f"{self.wh_schema_name}: Renaming entity schema field from '{self.old_wh_field_name}' to '{self.new_wh_field_name}'."
532
-
533
- def describe(self) -> str:
534
- return f"{self.wh_schema_name}: Entity schema field '{self.old_wh_field_name}' in Benchling has wh_name '{self.new_wh_field_name}' in code."
505
+ if existing_new_field:
506
+ raise ValueError(
507
+ f"New field warehouse name {self.update_props.warehouse_name} already exists on entity schema {self.wh_schema_name} and is {'archived' if existing_new_field.archiveRecord is not None else 'active'} in Benchling."
508
+ )
509
+ return tag_schema
535
510
 
536
511
 
537
512
  class ReorderEntitySchemaFields(BaseOperation):
@@ -64,7 +64,6 @@ class CreateTagSchemaFieldModel(BaseModel):
64
64
  @classmethod
65
65
  def from_props(
66
66
  cls,
67
- wh_field_name: str,
68
67
  new_props: BaseFieldProperties,
69
68
  benchling_service: BenchlingService | None = None,
70
69
  ) -> CreateTagSchemaFieldModel:
@@ -112,7 +111,7 @@ class CreateTagSchemaFieldModel(BaseModel):
112
111
  ).id
113
112
  return cls(
114
113
  name=new_props.name,
115
- systemName=wh_field_name,
114
+ systemName=new_props.warehouse_name,
116
115
  isMulti=new_props.is_multi,
117
116
  isRequired=new_props.required,
118
117
  isParentLink=new_props.parent_link,
@@ -180,6 +179,12 @@ class TagSchemaFieldModel(BaseModel):
180
179
  update_diff_names = list(update_diff.keys())
181
180
  update_props = BaseFieldProperties(**update_diff)
182
181
  self.name = update_props.name if "name" in update_diff_names else self.name
182
+ self.systemName = (
183
+ update_props.warehouse_name
184
+ if "warehouse_name" in update_diff_names
185
+ and update_props.warehouse_name is not None
186
+ else self.systemName
187
+ )
183
188
  self.isRequired = (
184
189
  update_props.required
185
190
  if "required" in update_diff_names
@@ -63,7 +63,8 @@ def convert_tag_schema_to_internal_schema(
63
63
  BenchlingNamingStrategy(strategy)
64
64
  for strategy in tag_schema.labelingStrategies
65
65
  ),
66
- ).set_archived(tag_schema.archiveRecord is not None),
66
+ _archived=tag_schema.archiveRecord is not None,
67
+ ),
67
68
  {
68
69
  f.systemName: convert_tag_schema_field_to_field_properties(f, dropdowns_map)
69
70
  for f in all_fields
@@ -90,7 +91,8 @@ def convert_tag_schema_field_to_field_properties(
90
91
  if field.requiredLink and field.requiredLink.tagSchema
91
92
  else None,
92
93
  tooltip=field.tooltipText,
93
- ).set_archived(field.archiveRecord is not None)
94
+ _archived=field.archiveRecord is not None,
95
+ )
94
96
 
95
97
 
96
98
  def get_benchling_entity_schemas(
@@ -7,8 +7,11 @@ class BenchlingAPIFieldType(StrEnum):
7
7
 
8
8
  BLOB_LINK = "ft_blob_link"
9
9
  DATE = "ft_date"
10
+ DATETIME = "ft_datetime"
10
11
  ENTRY_LINK = "ft_entry_link"
11
12
  FILE_LINK = "ft_file_link"
13
+ PART_LINK = "ft_part_link"
14
+ TRANSLATION_LINK = "ft_translation_link"
12
15
  FLOAT = "ft_float"
13
16
  INTEGER = "ft_integer"
14
17
  LONG_TEXT = "ft_long_text"
@@ -12,3 +12,4 @@ class BenchlingEntityType(StrEnum):
12
12
  AA_SEQUENCE = "aa_sequence"
13
13
  ENTRY = "entry"
14
14
  MIXTURE = "mixture"
15
+ MOLECULE = "molecule"
@@ -8,7 +8,10 @@ class BenchlingFieldType(StrEnum):
8
8
  BLOB_LINK = "blob_link"
9
9
  CUSTOM_ENTITY_LINK = "custom_entity_link"
10
10
  DATE = "date"
11
+ DATETIME = "datetime"
11
12
  DNA_SEQUENCE_LINK = "dna_sequence_link"
13
+ PART_LINK = "part_link"
14
+ TRANSLATION_LINK = "translation_link"
12
15
  DROPDOWN = "dropdown"
13
16
  ENTITY_LINK = "entity_link"
14
17
  ENTRY_LINK = "entry_link"
@@ -27,4 +30,5 @@ class BenchlingFieldType(StrEnum):
27
30
  cls.DECIMAL,
28
31
  cls.INTEGER,
29
32
  cls.DATE,
33
+ cls.DATETIME,
30
34
  ]
@@ -7,3 +7,4 @@ class BenchlingFolderItemType(StrEnum):
7
7
  SEQUENCE = "sequence"
8
8
  ENTRY = "entry"
9
9
  MIXTURE = "mixture"
10
+ MOLECULE = "molecule"
@@ -22,8 +22,6 @@ from liminal.entity_schemas.operations import (
22
22
  UnarchiveEntitySchemaField,
23
23
  UpdateEntitySchema,
24
24
  UpdateEntitySchemaField,
25
- UpdateEntitySchemaFieldName,
26
- UpdateEntitySchemaName,
27
25
  )
28
26
  from liminal.enums import (
29
27
  BenchlingAPIFieldType,
liminal/mappers.py CHANGED
@@ -16,6 +16,7 @@ from liminal.enums import (
16
16
  def convert_benchling_type_to_python_type(benchling_type: BenchlingFieldType) -> type:
17
17
  benchling_to_python_type_map = {
18
18
  BenchlingFieldType.DATE: datetime,
19
+ BenchlingFieldType.DATETIME: datetime,
19
20
  BenchlingFieldType.DECIMAL: float,
20
21
  BenchlingFieldType.INTEGER: int,
21
22
  BenchlingFieldType.BLOB_LINK: dict[str, Any],
@@ -27,6 +28,7 @@ def convert_benchling_type_to_python_type(benchling_type: BenchlingFieldType) ->
27
28
  BenchlingFieldType.MIXTURE_LINK: str,
28
29
  BenchlingFieldType.LONG_TEXT: str,
29
30
  BenchlingFieldType.STORAGE_LINK: str,
31
+ BenchlingFieldType.PART_LINK: str,
30
32
  BenchlingFieldType.TEXT: str,
31
33
  }
32
34
  if benchling_type in benchling_to_python_type_map:
@@ -40,6 +42,7 @@ def convert_benchling_type_to_sql_alchemy_type(
40
42
  ) -> TypeEngine:
41
43
  benchling_to_sql_alchemy_type_map = {
42
44
  BenchlingFieldType.DATE: DateTime,
45
+ BenchlingFieldType.DATETIME: DateTime,
43
46
  BenchlingFieldType.DECIMAL: Float,
44
47
  BenchlingFieldType.INTEGER: Integer,
45
48
  BenchlingFieldType.BLOB_LINK: JSON,
@@ -50,12 +53,13 @@ def convert_benchling_type_to_sql_alchemy_type(
50
53
  BenchlingFieldType.ENTRY_LINK: String,
51
54
  BenchlingFieldType.LONG_TEXT: String,
52
55
  BenchlingFieldType.STORAGE_LINK: String,
56
+ BenchlingFieldType.PART_LINK: String,
53
57
  BenchlingFieldType.MIXTURE_LINK: String,
54
58
  BenchlingFieldType.AA_SEQUENCE_LINK: String,
55
59
  BenchlingFieldType.TEXT: String,
56
60
  }
57
61
  if benchling_type in benchling_to_sql_alchemy_type_map:
58
- return benchling_to_sql_alchemy_type_map[benchling_type] # type: ignore
62
+ return benchling_to_sql_alchemy_type_map[benchling_type]
59
63
  else:
60
64
  raise ValueError(f"Benchling field type '{benchling_type}' is not supported.")
61
65
 
@@ -66,6 +70,7 @@ def convert_field_type_to_api_field_type(
66
70
  conversion_map = {
67
71
  BenchlingFieldType.BLOB_LINK: (BenchlingAPIFieldType.BLOB_LINK, None),
68
72
  BenchlingFieldType.DATE: (BenchlingAPIFieldType.DATE, None),
73
+ BenchlingFieldType.DATETIME: (BenchlingAPIFieldType.DATETIME, None),
69
74
  BenchlingFieldType.ENTRY_LINK: (BenchlingAPIFieldType.ENTRY_LINK, None),
70
75
  BenchlingFieldType.AA_SEQUENCE_LINK: (
71
76
  BenchlingAPIFieldType.FILE_LINK,
@@ -83,6 +88,14 @@ def convert_field_type_to_api_field_type(
83
88
  BenchlingAPIFieldType.FILE_LINK,
84
89
  BenchlingFolderItemType.SEQUENCE,
85
90
  ),
91
+ BenchlingFieldType.PART_LINK: (
92
+ BenchlingAPIFieldType.PART_LINK,
93
+ BenchlingFolderItemType.SEQUENCE,
94
+ ),
95
+ BenchlingFieldType.TRANSLATION_LINK: (
96
+ BenchlingAPIFieldType.TRANSLATION_LINK,
97
+ BenchlingFolderItemType.SEQUENCE,
98
+ ),
86
99
  BenchlingFieldType.ENTITY_LINK: (BenchlingAPIFieldType.FILE_LINK, None),
87
100
  BenchlingFieldType.DECIMAL: (BenchlingAPIFieldType.FLOAT, None),
88
101
  BenchlingFieldType.INTEGER: (BenchlingAPIFieldType.INTEGER, None),
@@ -104,6 +117,7 @@ def convert_api_field_type_to_field_type(
104
117
  conversion_map = {
105
118
  (BenchlingAPIFieldType.BLOB_LINK, None): BenchlingFieldType.BLOB_LINK,
106
119
  (BenchlingAPIFieldType.DATE, None): BenchlingFieldType.DATE,
120
+ (BenchlingAPIFieldType.DATETIME, None): BenchlingFieldType.DATETIME,
107
121
  (BenchlingAPIFieldType.ENTRY_LINK, None): BenchlingFieldType.ENTRY_LINK,
108
122
  (
109
123
  BenchlingAPIFieldType.FILE_LINK,
@@ -121,6 +135,14 @@ def convert_api_field_type_to_field_type(
121
135
  BenchlingAPIFieldType.FILE_LINK,
122
136
  BenchlingFolderItemType.PROTEIN,
123
137
  ): BenchlingFieldType.AA_SEQUENCE_LINK,
138
+ (
139
+ BenchlingAPIFieldType.PART_LINK,
140
+ BenchlingFolderItemType.SEQUENCE,
141
+ ): BenchlingFieldType.PART_LINK,
142
+ (
143
+ BenchlingAPIFieldType.TRANSLATION_LINK,
144
+ BenchlingFolderItemType.SEQUENCE,
145
+ ): BenchlingFieldType.TRANSLATION_LINK,
124
146
  (BenchlingAPIFieldType.FILE_LINK, None): BenchlingFieldType.ENTITY_LINK,
125
147
  (BenchlingAPIFieldType.FLOAT, None): BenchlingFieldType.DECIMAL,
126
148
  (BenchlingAPIFieldType.INTEGER, None): BenchlingFieldType.INTEGER,
@@ -146,6 +168,7 @@ def convert_api_entity_type_to_entity_type(
146
168
  ): BenchlingEntityType.CUSTOM_ENTITY,
147
169
  (BenchlingFolderItemType.ENTRY, None): BenchlingEntityType.ENTRY,
148
170
  (BenchlingFolderItemType.MIXTURE, None): BenchlingEntityType.MIXTURE,
171
+ (BenchlingFolderItemType.MOLECULE, None): BenchlingEntityType.MOLECULE,
149
172
  (BenchlingFolderItemType.PROTEIN, None): BenchlingEntityType.AA_SEQUENCE,
150
173
  (
151
174
  BenchlingFolderItemType.SEQUENCE,
@@ -182,6 +205,7 @@ def convert_entity_type_to_api_entity_type(
182
205
  ),
183
206
  BenchlingEntityType.ENTRY: (BenchlingFolderItemType.ENTRY, None),
184
207
  BenchlingEntityType.MIXTURE: (BenchlingFolderItemType.MIXTURE, None),
208
+ BenchlingEntityType.MOLECULE: (BenchlingFolderItemType.MOLECULE, None),
185
209
  BenchlingEntityType.AA_SEQUENCE: (BenchlingFolderItemType.PROTEIN, None),
186
210
  BenchlingEntityType.DNA_SEQUENCE: (
187
211
  BenchlingFolderItemType.SEQUENCE,
@@ -1,3 +1,5 @@
1
+ import traceback
2
+
1
3
  from rich import print
2
4
 
3
5
  from liminal.base.base_operation import BaseOperation
@@ -64,7 +66,7 @@ def execute_operations(
64
66
  ) -> bool:
65
67
  """This runs the given operations. It validates the operations and then executes them."""
66
68
  for o in operations:
67
- o.validate()
69
+ o.validate(benchling_service)
68
70
 
69
71
  print("[bold]Executing operations...")
70
72
  index = 1
@@ -73,17 +75,20 @@ def execute_operations(
73
75
  try:
74
76
  o.execute(benchling_service)
75
77
  except Exception as e:
76
- print(f"[bold red]Error executing operation: {e}]")
78
+ traceback.print_exc()
79
+ print(f"[bold red]Error executing operation {o.__class__.__name__}: {e}]")
77
80
  return False
78
81
  index += 1
79
82
  return True
80
83
 
81
84
 
82
- def execute_operations_dry_run(operations: list[BaseOperation]) -> None:
85
+ def execute_operations_dry_run(
86
+ benchling_service: BenchlingService, operations: list[BaseOperation]
87
+ ) -> None:
83
88
  """This runs the given operations in dry run mode. It only prints a description of the operations and validates them."""
84
89
  print("[bold]Executing dry run of operations...")
85
90
  index = 1
86
91
  for o in operations:
87
92
  print(f"{index}. {o.describe()}")
88
- o.validate()
93
+ o.validate(benchling_service)
89
94
  index += 1
liminal/orm/base_model.py CHANGED
@@ -72,9 +72,7 @@ class BaseModel(Generic[T], Base):
72
72
 
73
73
  @declared_attr
74
74
  def creator(cls) -> RelationshipProperty:
75
- return relationship(
76
- "User", foreign_keys=[cls.creator_id], uselist=False, lazy="joined"
77
- )
75
+ return relationship("User", foreign_keys=[cls.creator_id])
78
76
 
79
77
  id = SqlColumn("id", String, nullable=True, primary_key=True)
80
78
  archived = SqlColumn("archived$", Boolean, nullable=True)
@@ -109,7 +107,9 @@ class BaseModel(Generic[T], Base):
109
107
  return list(models.values())
110
108
 
111
109
  @classmethod
112
- def get_columns_dict(cls, exclude_base_columns: bool = False) -> dict[str, Column]:
110
+ def get_columns_dict(
111
+ cls, exclude_base_columns: bool = False, exclude_archived: bool = True
112
+ ) -> dict[str, Column]:
113
113
  """Returns a dictionary of all benchling columns in the class. Benchling Column saves an instance of itself to the sqlalchemy Column info property.
114
114
  This function retrieves the info property and returns a dictionary of the columns.
115
115
  """
@@ -125,6 +125,8 @@ class BaseModel(Generic[T], Base):
125
125
  )
126
126
  fields_to_exclude.append("creator_id$")
127
127
  columns = [c for c in cls.__table__.columns if c.name not in fields_to_exclude]
128
+ if exclude_archived:
129
+ columns = [c for c in columns if not c.properties._archived]
128
130
  return {c.name: c for c in columns}
129
131
 
130
132
  @classmethod
@@ -206,9 +208,8 @@ class BaseModel(Generic[T], Base):
206
208
  return pd.read_sql(query.statement, session.connection())
207
209
 
208
210
  @classmethod
209
- @abstractmethod
210
211
  def query(cls, session: Session) -> Query:
211
- """Abstract method that all subclasses must implement. Each subclass will have a differently defined query
212
+ """Abstract method that users can override to define a specific query
212
213
  to retrieve entities from the database and cover any distinct relationships.
213
214
 
214
215
  Parameters
@@ -221,7 +222,7 @@ class BaseModel(Generic[T], Base):
221
222
  Query
222
223
  sqlalchemy query to retrieve entities from the database.
223
224
  """
224
- raise NotImplementedError
225
+ return session.query(cls)
225
226
 
226
227
  @abstractmethod
227
228
  def get_validators(self) -> list[BenchlingValidator]:
liminal/orm/column.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from typing import Any, Type # noqa: UP035
2
2
 
3
- from sqlalchemy import ARRAY, ForeignKey
4
- from sqlalchemy import Column as SqlColumn
3
+ from sqlalchemy.sql.schema import Column as SqlColumn
4
+ from sqlalchemy.sql.schema import ForeignKey
5
+ from sqlalchemy.types import JSON
5
6
 
6
7
  from liminal.base.base_dropdown import BaseDropdown
7
8
  from liminal.base.properties.base_field_properties import BaseFieldProperties
@@ -43,6 +44,7 @@ class Column(SqlColumn):
43
44
  tooltip: str | None = None,
44
45
  dropdown: Type[BaseDropdown] | None = None, # noqa: UP006
45
46
  entity_link: str | None = None,
47
+ _archived: bool = False,
46
48
  **kwargs: Any,
47
49
  ):
48
50
  """Initializes a Benchling Column object. Validates the type BenchlingFieldType maps to a valid sqlalchemy type.
@@ -56,11 +58,12 @@ class Column(SqlColumn):
56
58
  dropdown_link=dropdown.__benchling_name__ if dropdown else None,
57
59
  entity_link=entity_link,
58
60
  tooltip=tooltip,
61
+ _archived=_archived,
59
62
  )
60
63
  self.properties = properties
61
64
 
62
65
  nested_sql_type = convert_benchling_type_to_sql_alchemy_type(type)
63
- sqlalchemy_type = ARRAY(nested_sql_type) if is_multi else nested_sql_type
66
+ sqlalchemy_type = JSON if is_multi else nested_sql_type
64
67
  if dropdown and type != BenchlingFieldType.DROPDOWN:
65
68
  raise ValueError("Dropdown can only be set if the field type is DROPDOWN.")
66
69
  if dropdown is None and type == BenchlingFieldType.DROPDOWN:
liminal/orm/mixins.py CHANGED
@@ -87,3 +87,12 @@ class MixtureMixin:
87
87
  project_id = SqlColumn("project_id$", String, nullable=True)
88
88
  type = SqlColumn("type$", String, nullable=True)
89
89
  validation_status = SqlColumn("validation_status$", String, nullable=True)
90
+
91
+
92
+ class MoleculeMixin:
93
+ canonical_smiles = SqlColumn("canonical_smiles$", String, nullable=True)
94
+ file_registry_id = SqlColumn("file_registry_id$", String, nullable=True)
95
+ is_registered = SqlColumn("is_registered$", Boolean, nullable=True)
96
+ project_id = SqlColumn("project_id$", String, nullable=True)
97
+ type = SqlColumn("type$", String, nullable=True)
98
+ validation_status = SqlColumn("validation_status$", String, nullable=True)
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- from pydantic import PrivateAttr, model_validator
3
+ from typing import Any
4
+
5
+ from pydantic import model_validator
4
6
 
5
7
  from liminal.base.properties.base_schema_properties import (
6
8
  BaseSchemaProperties,
@@ -22,7 +24,11 @@ class SchemaProperties(BaseSchemaProperties):
22
24
  entity_type: BenchlingEntityType
23
25
  naming_strategies: set[BenchlingNamingStrategy]
24
26
  mixture_schema_config: MixtureSchemaConfig | None = None
25
- _archived: bool | None = PrivateAttr(default=None)
27
+ _archived: bool = False
28
+
29
+ def __init__(self, **data: Any):
30
+ super().__init__(**data)
31
+ self._archived = data.get("_archived", False)
26
32
 
27
33
  @model_validator(mode="after")
28
34
  def validate_mixture_schema_config(self) -> SchemaProperties: