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.
- liminal/base/base_operation.py +9 -11
- liminal/base/properties/base_field_properties.py +11 -0
- liminal/base/properties/base_schema_properties.py +6 -2
- liminal/cli/live_test_dropdown_migration.py +8 -9
- liminal/cli/live_test_entity_schema_migration.py +24 -27
- liminal/connection/benchling_connection.py +8 -2
- liminal/connection/benchling_service.py +11 -18
- liminal/dropdowns/operations.py +1 -1
- liminal/entity_schemas/compare.py +17 -7
- liminal/entity_schemas/entity_schema_models.py +6 -9
- liminal/entity_schemas/generate_files.py +7 -6
- liminal/entity_schemas/operations.py +77 -102
- liminal/entity_schemas/tag_schema_models.py +7 -2
- liminal/entity_schemas/utils.py +4 -2
- liminal/enums/benchling_api_field_type.py +3 -0
- liminal/enums/benchling_entity_type.py +1 -0
- liminal/enums/benchling_field_type.py +4 -0
- liminal/enums/benchling_folder_item_type.py +1 -0
- liminal/external/__init__.py +0 -2
- liminal/mappers.py +25 -1
- liminal/migrate/components.py +9 -4
- liminal/orm/base_model.py +8 -7
- liminal/orm/column.py +6 -3
- liminal/orm/mixins.py +9 -0
- liminal/orm/schema_properties.py +8 -2
- liminal/tests/conftest.py +52 -22
- liminal/tests/test_entity_schema_compare.py +11 -5
- liminal/utils.py +11 -4
- {liminal_orm-1.0.6.dist-info → liminal_orm-1.1.0.dist-info}/METADATA +3 -2
- liminal_orm-1.1.0.dist-info/RECORD +61 -0
- liminal_orm-1.0.6.dist-info/RECORD +0 -61
- {liminal_orm-1.0.6.dist-info → liminal_orm-1.1.0.dist-info}/LICENSE.md +0 -0
- {liminal_orm-1.0.6.dist-info → liminal_orm-1.1.0.dist-info}/WHEEL +0 -0
- {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:
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
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
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
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=
|
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
|
liminal/entity_schemas/utils.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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"
|
@@ -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
|
]
|
liminal/external/__init__.py
CHANGED
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]
|
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,
|
liminal/migrate/components.py
CHANGED
@@ -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
|
-
|
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(
|
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(
|
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
|
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
|
-
|
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
|
4
|
-
from sqlalchemy import
|
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 =
|
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)
|
liminal/orm/schema_properties.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
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
|
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:
|