liminal-orm 2.0.4__py3-none-any.whl → 3.0.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 +4 -4
- liminal/base/properties/base_field_properties.py +3 -1
- liminal/base/properties/base_schema_properties.py +8 -2
- liminal/entity_schemas/entity_schema_models.py +40 -13
- liminal/entity_schemas/generate_files.py +1 -1
- liminal/entity_schemas/operations.py +58 -8
- liminal/entity_schemas/tag_schema_models.py +71 -24
- liminal/entity_schemas/utils.py +36 -9
- liminal/enums/__init__.py +1 -0
- liminal/enums/benchling_entity_type.py +9 -0
- liminal/enums/benchling_field_type.py +5 -1
- liminal/enums/sequence_constraint.py +13 -0
- liminal/mappers.py +3 -9
- liminal/orm/base_model.py +46 -11
- liminal/orm/column.py +20 -2
- liminal/orm/schema_properties.py +14 -5
- liminal/unit_dictionary/utils.py +48 -0
- {liminal_orm-2.0.4.dist-info → liminal_orm-3.0.0.dist-info}/METADATA +1 -1
- {liminal_orm-2.0.4.dist-info → liminal_orm-3.0.0.dist-info}/RECORD +22 -20
- {liminal_orm-2.0.4.dist-info → liminal_orm-3.0.0.dist-info}/LICENSE.md +0 -0
- {liminal_orm-2.0.4.dist-info → liminal_orm-3.0.0.dist-info}/WHEEL +0 -0
- {liminal_orm-2.0.4.dist-info → liminal_orm-3.0.0.dist-info}/entry_points.txt +0 -0
liminal/base/base_operation.py
CHANGED
@@ -13,10 +13,10 @@ Order of operations based on order class var:
|
|
13
13
|
6. ArchiveDropdownOption
|
14
14
|
7. ReorderDropdownOptions
|
15
15
|
8. CreateSchema
|
16
|
-
9.
|
17
|
-
10.
|
18
|
-
11.
|
19
|
-
12.
|
16
|
+
9. UnarchiveSchema
|
17
|
+
10. CreateField
|
18
|
+
11. UpdateEntitySchemaNameTemplate
|
19
|
+
12. UpdateSchema
|
20
20
|
13. UnarchiveField
|
21
21
|
14. UpdateField
|
22
22
|
15. ArchiveField
|
@@ -49,6 +49,8 @@ class BaseFieldProperties(BaseModel):
|
|
49
49
|
dropdown_link: str | None = None
|
50
50
|
entity_link: str | None = None
|
51
51
|
tooltip: str | None = None
|
52
|
+
decimal_places: int | None = None
|
53
|
+
unit_name: str | None = None
|
52
54
|
_archived: bool | None = PrivateAttr(default=None)
|
53
55
|
|
54
56
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
@@ -122,4 +124,4 @@ class BaseFieldProperties(BaseModel):
|
|
122
124
|
|
123
125
|
def __repr__(self) -> str:
|
124
126
|
"""Generates a string representation of the class so that it can be executed."""
|
125
|
-
return f"{self.__class__.__name__}({', '.join([f'{k}={v.__repr__()}' for k, v in self.model_dump(exclude_unset=True
|
127
|
+
return f"{self.__class__.__name__}({', '.join([f'{k}={v.__repr__()}' for k, v in self.model_dump(exclude_unset=True).items()])})"
|
@@ -66,8 +66,13 @@ class BaseSchemaProperties(BaseModel):
|
|
66
66
|
include_registry_id_in_chips : bool | None = None
|
67
67
|
Flag for configuring the chip label for entities. Determines if the chip will include the Registry ID in the chip label.
|
68
68
|
constraint_fields : set[str] | None
|
69
|
-
Set of constraints for field values for the schema. Must be a set of column names
|
70
|
-
|
69
|
+
Set of constraints for field values for the schema. Must be a set of warehouse column names. This specifies that their entity field values must be a unique combination within an entity.
|
70
|
+
The following sequence constraints are also supported:
|
71
|
+
- bases: only supported for nucleotide sequence entity types. hasUniqueResidues=True
|
72
|
+
- amino_acids_ignore_case: only supported for amino acid sequence entity types. hasUniqueResidues=True
|
73
|
+
- amino_acids_exact_match: only supported for amino acid sequence entity types. hasUniqueResidues=True, areUniqueResiduesCaseSensitive=True
|
74
|
+
show_bases_in_expanded_view : bool | None
|
75
|
+
Whether the bases should be shown in the expanded view of the entity.
|
71
76
|
_archived : bool | None
|
72
77
|
Whether the schema is archived in Benchling.
|
73
78
|
"""
|
@@ -81,6 +86,7 @@ class BaseSchemaProperties(BaseModel):
|
|
81
86
|
use_registry_id_as_label: bool | None = None
|
82
87
|
include_registry_id_in_chips: bool | None = None
|
83
88
|
constraint_fields: set[str] | None = None
|
89
|
+
show_bases_in_expanded_view: bool | None = None
|
84
90
|
_archived: bool | None = None
|
85
91
|
|
86
92
|
def __init__(self, **data: Any):
|
@@ -9,8 +9,10 @@ from liminal.connection import BenchlingService
|
|
9
9
|
from liminal.dropdowns.utils import get_benchling_dropdown_summary_by_name
|
10
10
|
from liminal.entity_schemas.tag_schema_models import TagSchemaModel
|
11
11
|
from liminal.enums import BenchlingEntityType
|
12
|
+
from liminal.enums.sequence_constraint import SequenceConstraint
|
12
13
|
from liminal.mappers import convert_field_type_to_api_field_type
|
13
14
|
from liminal.orm.schema_properties import MixtureSchemaConfig, SchemaProperties
|
15
|
+
from liminal.unit_dictionary.utils import get_unit_id_from_name
|
14
16
|
|
15
17
|
|
16
18
|
class FieldLinkShortModel(BaseModel):
|
@@ -26,28 +28,44 @@ class EntitySchemaConstraint(BaseModel):
|
|
26
28
|
"""
|
27
29
|
|
28
30
|
areUniqueResiduesCaseSensitive: bool | None = None
|
29
|
-
fields: dict[str, Any] | None = None
|
30
|
-
|
31
|
+
fields: list[dict[str, Any]] | None = None
|
32
|
+
hasUniqueCanonicalSmiles: bool | None = None
|
31
33
|
hasUniqueResidues: bool | None = None
|
32
34
|
|
33
35
|
@classmethod
|
34
36
|
def from_constraint_fields(
|
35
|
-
cls,
|
37
|
+
cls,
|
38
|
+
constraint_fields: set[str],
|
39
|
+
benchling_service: BenchlingService | None = None,
|
36
40
|
) -> EntitySchemaConstraint:
|
37
41
|
"""
|
38
42
|
Generates a Constraint object from a set of constraint fields to create a constraint on a schema.
|
39
43
|
"""
|
40
|
-
if constraint_fields is None:
|
41
|
-
return None
|
42
44
|
hasUniqueResidues = False
|
43
|
-
|
44
|
-
|
45
|
+
areUniqueResiduesCaseSensitive = False
|
46
|
+
if SequenceConstraint.BASES in constraint_fields:
|
47
|
+
constraint_fields.discard(SequenceConstraint.BASES)
|
48
|
+
hasUniqueResidues = True
|
49
|
+
elif SequenceConstraint.AMINO_ACIDS_IGNORE_CASE in constraint_fields:
|
50
|
+
constraint_fields.discard(SequenceConstraint.AMINO_ACIDS_IGNORE_CASE)
|
45
51
|
hasUniqueResidues = True
|
52
|
+
elif SequenceConstraint.AMINO_ACIDS_EXACT_MATCH in constraint_fields:
|
53
|
+
constraint_fields.discard(SequenceConstraint.AMINO_ACIDS_EXACT_MATCH)
|
54
|
+
hasUniqueResidues = True
|
55
|
+
areUniqueResiduesCaseSensitive = True
|
56
|
+
fields = []
|
57
|
+
for field_name in constraint_fields:
|
58
|
+
if benchling_service is None:
|
59
|
+
raise ValueError(
|
60
|
+
"Benchling SDK must be provided to update constraint fields."
|
61
|
+
)
|
62
|
+
field = TagSchemaModel.get_one(benchling_service, field_name)
|
63
|
+
fields.append({"name": field.name, "id": field.id})
|
46
64
|
return cls(
|
47
|
-
fields=
|
65
|
+
fields=fields,
|
48
66
|
hasUniqueResidues=hasUniqueResidues,
|
49
|
-
|
50
|
-
areUniqueResiduesCaseSensitive=
|
67
|
+
hasUniqueCanonicalSmiles=False,
|
68
|
+
areUniqueResiduesCaseSensitive=areUniqueResiduesCaseSensitive,
|
51
69
|
)
|
52
70
|
|
53
71
|
|
@@ -63,6 +81,8 @@ class CreateEntitySchemaFieldModel(BaseModel):
|
|
63
81
|
isParentLink: bool = False
|
64
82
|
dropdownId: str | None = None
|
65
83
|
link: FieldLinkShortModel | None = None
|
84
|
+
unitId: str | None = None
|
85
|
+
decimalPrecision: int | None = None
|
66
86
|
|
67
87
|
@classmethod
|
68
88
|
def from_benchling_props(
|
@@ -107,6 +127,11 @@ class CreateEntitySchemaFieldModel(BaseModel):
|
|
107
127
|
dropdown_summary_id = get_benchling_dropdown_summary_by_name(
|
108
128
|
benchling_service, field_props.dropdown_link
|
109
129
|
).id
|
130
|
+
unit_id = None
|
131
|
+
if field_props.unit_name is not None:
|
132
|
+
if benchling_service is None:
|
133
|
+
raise ValueError("Benchling SDK must be provided to update unit field.")
|
134
|
+
unit_id = get_unit_id_from_name(benchling_service, field_props.unit_name)
|
110
135
|
return CreateEntitySchemaFieldModel(
|
111
136
|
name=field_props.name,
|
112
137
|
systemName=field_props.warehouse_name,
|
@@ -118,6 +143,8 @@ class CreateEntitySchemaFieldModel(BaseModel):
|
|
118
143
|
link=FieldLinkShortModel(
|
119
144
|
tagSchema=tag_schema, folderItemType=folder_item_type
|
120
145
|
),
|
146
|
+
unitId=unit_id,
|
147
|
+
decimalPrecision=field_props.decimal_places,
|
121
148
|
)
|
122
149
|
|
123
150
|
|
@@ -135,6 +162,7 @@ class CreateEntitySchemaModel(BaseModel):
|
|
135
162
|
useOrganizationCollectionAliasForDisplayLabel: bool | None = None
|
136
163
|
labelingStrategies: list[str] | None = None
|
137
164
|
constraint: EntitySchemaConstraint | None = None
|
165
|
+
showResidues: bool | None = None
|
138
166
|
|
139
167
|
@classmethod
|
140
168
|
def from_benchling_props(
|
@@ -168,11 +196,10 @@ class CreateEntitySchemaModel(BaseModel):
|
|
168
196
|
includeRegistryIdInChips=benchling_props.include_registry_id_in_chips,
|
169
197
|
useOrganizationCollectionAliasForDisplayLabel=benchling_props.use_registry_id_as_label,
|
170
198
|
labelingStrategies=[s.value for s in benchling_props.naming_strategies],
|
199
|
+
showResidues=benchling_props.show_bases_in_expanded_view,
|
171
200
|
constraint=EntitySchemaConstraint.from_constraint_fields(
|
172
201
|
benchling_props.constraint_fields
|
173
|
-
)
|
174
|
-
if benchling_props.constraint_fields
|
175
|
-
else None,
|
202
|
+
),
|
176
203
|
fields=[
|
177
204
|
CreateEntitySchemaFieldModel.from_benchling_props(
|
178
205
|
field_props, benchling_service
|
@@ -91,7 +91,7 @@ def generate_all_entity_schema_files(
|
|
91
91
|
dropdown_classname = dropdown_name_to_classname_map[col.dropdown_link]
|
92
92
|
dropdowns.append(dropdown_classname)
|
93
93
|
column_strings.append(
|
94
|
-
f"""{tab}{col_name}: SqlColumn = Column(name="{col.name}", type={str(col.type)}, required={col.required}{', is_multi=True' if col.is_multi else ''}{', parent_link=True' if col.parent_link else ''}{f', entity_link="{col.entity_link}"' if col.entity_link else ''}{f', dropdown={dropdown_classname}' if dropdown_classname else ''}{f', tooltip="{col.tooltip}"' if col.tooltip else ''})"""
|
94
|
+
f"""{tab}{col_name}: SqlColumn = Column(name="{col.name}", type={str(col.type)}, required={col.required}{', is_multi=True' if col.is_multi else ''}{', parent_link=True' if col.parent_link else ''}{f', entity_link="{col.entity_link}"' if col.entity_link else ''}{f', dropdown={dropdown_classname}' if dropdown_classname else ''}{f', tooltip="{col.tooltip}"' if col.tooltip else ''}{f', unit_name="{col.unit_name}"' if col.unit_name else ''}{f', decimal_places={col.decimal_places}' if col.decimal_places is not None else ''})"""
|
95
95
|
)
|
96
96
|
if col.required and col.type:
|
97
97
|
init_strings.append(
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import logging
|
1
2
|
from typing import Any, ClassVar
|
2
3
|
|
3
4
|
from liminal.base.base_operation import BaseOperation
|
@@ -25,8 +26,14 @@ from liminal.entity_schemas.utils import (
|
|
25
26
|
)
|
26
27
|
from liminal.enums import BenchlingNamingStrategy
|
27
28
|
from liminal.orm.schema_properties import SchemaProperties
|
29
|
+
from liminal.unit_dictionary.utils import (
|
30
|
+
get_unit_id_to_name_map,
|
31
|
+
get_unit_name_to_id_map,
|
32
|
+
)
|
28
33
|
from liminal.utils import to_snake_case
|
29
34
|
|
35
|
+
LOGGER = logging.getLogger(__name__)
|
36
|
+
|
30
37
|
|
31
38
|
class CreateEntitySchema(BaseOperation):
|
32
39
|
order: ClassVar[int] = 80
|
@@ -39,7 +46,7 @@ class CreateEntitySchema(BaseOperation):
|
|
39
46
|
self.schema_properties = schema_properties
|
40
47
|
self.fields = fields
|
41
48
|
self._validated_schema_properties = SchemaProperties(
|
42
|
-
**schema_properties.model_dump()
|
49
|
+
**schema_properties.model_dump(exclude_unset=True)
|
43
50
|
)
|
44
51
|
|
45
52
|
def execute(self, benchling_service: BenchlingService) -> dict[str, Any]:
|
@@ -83,6 +90,15 @@ class CreateEntitySchema(BaseOperation):
|
|
83
90
|
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)}. \
|
84
91
|
Reach out to Benchling support if you need help setting up warehouse access."
|
85
92
|
)
|
93
|
+
for field in self.fields:
|
94
|
+
if (
|
95
|
+
field.unit_name
|
96
|
+
and field.unit_name
|
97
|
+
not in get_unit_name_to_id_map(benchling_service).keys()
|
98
|
+
):
|
99
|
+
raise ValueError(
|
100
|
+
f"{self._validated_schema_properties.warehouse_name}: On field {field.warehouse_name}, unit {field.unit_name} not found in Benchling Unit Dictionary as a valid unit. Please check the field definition or your Unit Dictionary."
|
101
|
+
)
|
86
102
|
|
87
103
|
def _validate_create(self, benchling_service: BenchlingService) -> None:
|
88
104
|
all_schemas = TagSchemaModel.get_all_json(benchling_service)
|
@@ -123,8 +139,11 @@ class CreateEntitySchema(BaseOperation):
|
|
123
139
|
f"Entity schema {self._validated_schema_properties.warehouse_name} is already active in Benchling."
|
124
140
|
)
|
125
141
|
dropdowns_map = get_benchling_dropdown_id_name_map(benchling_service)
|
142
|
+
unit_id_to_name_map = get_unit_id_to_name_map(benchling_service)
|
126
143
|
benchling_schema_props, _, benchling_fields_props = (
|
127
|
-
convert_tag_schema_to_internal_schema(
|
144
|
+
convert_tag_schema_to_internal_schema(
|
145
|
+
schema, dropdowns_map, unit_id_to_name_map
|
146
|
+
)
|
128
147
|
)
|
129
148
|
if (
|
130
149
|
self._validated_schema_properties != benchling_schema_props
|
@@ -162,7 +181,7 @@ class ArchiveEntitySchema(BaseOperation):
|
|
162
181
|
|
163
182
|
|
164
183
|
class UnarchiveEntitySchema(BaseOperation):
|
165
|
-
order: ClassVar[int] =
|
184
|
+
order: ClassVar[int] = 90
|
166
185
|
|
167
186
|
def __init__(self, wh_schema_name: str) -> None:
|
168
187
|
self.wh_schema_name = wh_schema_name
|
@@ -187,7 +206,7 @@ class UnarchiveEntitySchema(BaseOperation):
|
|
187
206
|
|
188
207
|
|
189
208
|
class UpdateEntitySchema(BaseOperation):
|
190
|
-
order: ClassVar[int] =
|
209
|
+
order: ClassVar[int] = 120
|
191
210
|
|
192
211
|
def __init__(
|
193
212
|
self,
|
@@ -251,7 +270,7 @@ class UpdateEntitySchema(BaseOperation):
|
|
251
270
|
|
252
271
|
|
253
272
|
class UpdateEntitySchemaNameTemplate(BaseOperation):
|
254
|
-
order: ClassVar[int] =
|
273
|
+
order: ClassVar[int] = 110
|
255
274
|
|
256
275
|
def __init__(
|
257
276
|
self,
|
@@ -283,7 +302,7 @@ class UpdateEntitySchemaNameTemplate(BaseOperation):
|
|
283
302
|
|
284
303
|
|
285
304
|
class CreateEntitySchemaField(BaseOperation):
|
286
|
-
order: ClassVar[int] =
|
305
|
+
order: ClassVar[int] = 100
|
287
306
|
|
288
307
|
def __init__(
|
289
308
|
self,
|
@@ -316,13 +335,20 @@ class CreateEntitySchemaField(BaseOperation):
|
|
316
335
|
f"Field {self._wh_field_name} is already active on entity schema {self.wh_schema_name}."
|
317
336
|
)
|
318
337
|
dropdowns_map = get_benchling_dropdown_id_name_map(benchling_service)
|
338
|
+
unit_id_to_name_map = get_unit_id_to_name_map(benchling_service)
|
319
339
|
if self.field_props == convert_tag_schema_field_to_field_properties(
|
320
|
-
field, dropdowns_map
|
321
|
-
):
|
340
|
+
field, dropdowns_map, unit_id_to_name_map
|
341
|
+
).set_warehouse_name(self._wh_field_name):
|
322
342
|
return UnarchiveEntitySchemaField(
|
323
343
|
self.wh_schema_name, self._wh_field_name, self.index
|
324
344
|
).execute(benchling_service)
|
325
345
|
else:
|
346
|
+
print(self.field_props)
|
347
|
+
print(
|
348
|
+
convert_tag_schema_field_to_field_properties(
|
349
|
+
field, dropdowns_map, unit_id_to_name_map
|
350
|
+
)
|
351
|
+
)
|
326
352
|
raise ValueError(
|
327
353
|
f"Field {self._wh_field_name} on entity schema {self.wh_schema_name} is different in code versus Benchling."
|
328
354
|
)
|
@@ -367,6 +393,14 @@ class CreateEntitySchemaField(BaseOperation):
|
|
367
393
|
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)}. \
|
368
394
|
Reach out to Benchling support if you need help setting up warehouse access."
|
369
395
|
)
|
396
|
+
if (
|
397
|
+
self.field_props.unit_name
|
398
|
+
and self.field_props.unit_name
|
399
|
+
not in get_unit_name_to_id_map(benchling_service).keys()
|
400
|
+
):
|
401
|
+
raise ValueError(
|
402
|
+
f"{self.wh_schema_name}: On field {self._wh_field_name}, unit {self.field_props.unit_name} not found in Benchling Unit Dictionary as a valid unit. Please check the field definition or your Unit Dictionary."
|
403
|
+
)
|
370
404
|
|
371
405
|
|
372
406
|
class ArchiveEntitySchemaField(BaseOperation):
|
@@ -500,6 +534,9 @@ class UpdateEntitySchemaField(BaseOperation):
|
|
500
534
|
return f"{self.wh_schema_name}: Entity schema field '{self.wh_field_name}' in Benchling is different than in code: {str(self.update_props)}."
|
501
535
|
|
502
536
|
def validate(self, benchling_service: BenchlingService) -> None:
|
537
|
+
tag_schema = TagSchemaModel.get_one_cached(
|
538
|
+
benchling_service, self.wh_schema_name
|
539
|
+
)
|
503
540
|
if (
|
504
541
|
not benchling_service.connection.warehouse_access
|
505
542
|
and self.update_props.warehouse_name is not None
|
@@ -509,6 +546,19 @@ class UpdateEntitySchemaField(BaseOperation):
|
|
509
546
|
Either set warehouse_access to True in BenchlingConnection or do not change the warehouse name. \
|
510
547
|
Reach out to Benchling support if you need help setting up warehouse access."
|
511
548
|
)
|
549
|
+
if "unit_name" in self.update_props.model_dump(exclude_unset=True):
|
550
|
+
no_change_message = f"{self.wh_schema_name}: On field {self.wh_field_name}, updating unit name to {self.update_props.unit_name}. The unit of this field CANNOT be changed once it's been set."
|
551
|
+
if tag_schema.get_field(self.wh_field_name).unitApiIdentifier:
|
552
|
+
raise ValueError(no_change_message)
|
553
|
+
else:
|
554
|
+
LOGGER.warning(no_change_message)
|
555
|
+
if (
|
556
|
+
self.update_props.unit_name
|
557
|
+
not in get_unit_name_to_id_map(benchling_service).keys()
|
558
|
+
):
|
559
|
+
raise ValueError(
|
560
|
+
f"{self.wh_schema_name}: On field {self.wh_field_name}, unit {self.update_props.unit_name} not found in Benchling Unit Dictionary as a valid unit. Please check the field definition or your Unit Dictionary."
|
561
|
+
)
|
512
562
|
|
513
563
|
def _validate(self, benchling_service: BenchlingService) -> TagSchemaModel:
|
514
564
|
tag_schema = TagSchemaModel.get_one(benchling_service, self.wh_schema_name)
|
@@ -1,5 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from functools import lru_cache
|
3
4
|
from typing import Any
|
4
5
|
|
5
6
|
import requests
|
@@ -17,6 +18,7 @@ from liminal.enums import (
|
|
17
18
|
BenchlingSequenceType,
|
18
19
|
)
|
19
20
|
from liminal.enums.name_template_part_type import NameTemplatePartType
|
21
|
+
from liminal.enums.sequence_constraint import SequenceConstraint
|
20
22
|
from liminal.mappers import (
|
21
23
|
convert_entity_type_to_api_entity_type,
|
22
24
|
convert_field_type_to_api_field_type,
|
@@ -25,6 +27,7 @@ from liminal.orm.name_template_parts import (
|
|
25
27
|
NameTemplatePart,
|
26
28
|
)
|
27
29
|
from liminal.orm.schema_properties import MixtureSchemaConfig
|
30
|
+
from liminal.unit_dictionary.utils import get_unit_id_from_name
|
28
31
|
|
29
32
|
|
30
33
|
class FieldRequiredLinkShortModel(BaseModel):
|
@@ -91,16 +94,28 @@ class TagSchemaConstraint(BaseModel):
|
|
91
94
|
|
92
95
|
@classmethod
|
93
96
|
def from_constraint_fields(
|
94
|
-
cls,
|
97
|
+
cls,
|
98
|
+
constraint_fields: list[TagSchemaFieldModel],
|
99
|
+
sequence_constraint: SequenceConstraint | None = None,
|
95
100
|
) -> TagSchemaConstraint:
|
96
101
|
"""
|
97
102
|
Generates a Constraint object from a set of constraint fields to create a constraint on a schema.
|
98
103
|
"""
|
104
|
+
uniqueResidues = False
|
105
|
+
areUniqueResiduesCaseSensitive = False
|
106
|
+
match sequence_constraint:
|
107
|
+
case SequenceConstraint.BASES:
|
108
|
+
uniqueResidues = True
|
109
|
+
case SequenceConstraint.AMINO_ACIDS_IGNORE_CASE:
|
110
|
+
uniqueResidues = True
|
111
|
+
case SequenceConstraint.AMINO_ACIDS_EXACT_MATCH:
|
112
|
+
uniqueResidues = True
|
113
|
+
areUniqueResiduesCaseSensitive = True
|
99
114
|
return cls(
|
100
115
|
fields=constraint_fields,
|
101
|
-
uniqueResidues=
|
116
|
+
uniqueResidues=uniqueResidues,
|
102
117
|
uniqueCanonicalSmilers=False,
|
103
|
-
areUniqueResiduesCaseSensitive=
|
118
|
+
areUniqueResiduesCaseSensitive=areUniqueResiduesCaseSensitive,
|
104
119
|
)
|
105
120
|
|
106
121
|
|
@@ -135,6 +150,8 @@ class CreateTagSchemaFieldModel(BaseModel):
|
|
135
150
|
name: str
|
136
151
|
requiredLink: FieldRequiredLinkShortModel | None = None
|
137
152
|
tooltipText: str | None = None
|
153
|
+
decimalPrecision: int | None = None
|
154
|
+
unitApiIdentifier: str | None = None
|
138
155
|
|
139
156
|
@classmethod
|
140
157
|
def from_props(
|
@@ -184,6 +201,13 @@ class CreateTagSchemaFieldModel(BaseModel):
|
|
184
201
|
dropdown_summary_id = get_benchling_dropdown_summary_by_name(
|
185
202
|
benchling_service, new_props.dropdown_link
|
186
203
|
).id
|
204
|
+
unit_api_identifier = None
|
205
|
+
if new_props.unit_name:
|
206
|
+
if benchling_service is None:
|
207
|
+
raise ValueError("Benchling SDK must be provided to update unit field.")
|
208
|
+
unit_api_identifier = get_unit_id_from_name(
|
209
|
+
benchling_service, new_props.unit_name
|
210
|
+
)
|
187
211
|
return cls(
|
188
212
|
name=new_props.name,
|
189
213
|
systemName=new_props.warehouse_name,
|
@@ -197,6 +221,8 @@ class CreateTagSchemaFieldModel(BaseModel):
|
|
197
221
|
tagSchema=tagSchema,
|
198
222
|
),
|
199
223
|
tooltipText=new_props.tooltip,
|
224
|
+
unitApiIdentifier=unit_api_identifier,
|
225
|
+
decimalPrecision=new_props.decimal_places,
|
200
226
|
)
|
201
227
|
|
202
228
|
|
@@ -230,6 +256,8 @@ class TagSchemaFieldModel(BaseModel):
|
|
230
256
|
strictSelector: bool | None
|
231
257
|
systemName: str
|
232
258
|
tooltipText: str | None
|
259
|
+
unitApiIdentifier: str | None
|
260
|
+
unitSymbol: str | None = None
|
233
261
|
|
234
262
|
def update_from_props(
|
235
263
|
self,
|
@@ -307,6 +335,22 @@ class TagSchemaFieldModel(BaseModel):
|
|
307
335
|
benchling_service, update_props.dropdown_link
|
308
336
|
).id
|
309
337
|
self.schemaFieldSelectorId = dropdown_summary_id
|
338
|
+
self.decimalPrecision = (
|
339
|
+
update_props.decimal_places
|
340
|
+
if "decimal_places" in update_diff_names
|
341
|
+
else self.decimalPrecision
|
342
|
+
)
|
343
|
+
if "unit_name" in update_diff_names:
|
344
|
+
if update_props.unit_name is None:
|
345
|
+
self.unitApiIdentifier = None
|
346
|
+
else:
|
347
|
+
if benchling_service is None:
|
348
|
+
raise ValueError(
|
349
|
+
"Benchling SDK must be provided to update unit field."
|
350
|
+
)
|
351
|
+
self.unitApiIdentifier = get_unit_id_from_name(
|
352
|
+
benchling_service, update_props.unit_name
|
353
|
+
)
|
310
354
|
return self
|
311
355
|
|
312
356
|
|
@@ -405,6 +449,15 @@ class TagSchemaModel(BaseModel):
|
|
405
449
|
)
|
406
450
|
return cls.model_validate(schema)
|
407
451
|
|
452
|
+
@classmethod
|
453
|
+
@lru_cache(maxsize=100)
|
454
|
+
def get_one_cached(
|
455
|
+
cls,
|
456
|
+
benchling_service: BenchlingService,
|
457
|
+
wh_schema_name: str,
|
458
|
+
) -> TagSchemaModel:
|
459
|
+
return cls.get_one(benchling_service, wh_schema_name)
|
460
|
+
|
408
461
|
def get_field(self, wh_field_name: str) -> TagSchemaFieldModel:
|
409
462
|
"""Returns a field from the tag schema by its warehouse field name."""
|
410
463
|
for field in self.allFields:
|
@@ -437,37 +490,31 @@ class TagSchemaModel(BaseModel):
|
|
437
490
|
)
|
438
491
|
if "include_registry_id_in_chips" in update_diff_names:
|
439
492
|
self.includeRegistryIdInChips = update_props.include_registry_id_in_chips
|
493
|
+
if "show_bases_in_expanded_view" in update_diff_names:
|
494
|
+
self.showResidues = update_props.show_bases_in_expanded_view
|
440
495
|
|
441
496
|
if "constraint_fields" in update_diff_names:
|
442
497
|
if update_props.constraint_fields:
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
if f.systemName in update_props.constraint_fields
|
451
|
-
]
|
452
|
-
self.constraint = TagSchemaConstraint.from_constraint_fields(
|
453
|
-
constraint_fields, has_bases
|
498
|
+
sequence_constraint = next(
|
499
|
+
(
|
500
|
+
SequenceConstraint(c)
|
501
|
+
for c in update_props.constraint_fields
|
502
|
+
if SequenceConstraint.is_sequence_constraint(c)
|
503
|
+
),
|
504
|
+
None,
|
454
505
|
)
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
has_bases = False
|
461
|
-
if "bases" in update_props.constraint_fields:
|
462
|
-
has_bases = True
|
463
|
-
update_props.constraint_fields.discard("bases")
|
506
|
+
update_props.constraint_fields = {
|
507
|
+
c
|
508
|
+
for c in update_props.constraint_fields
|
509
|
+
if not SequenceConstraint.is_sequence_constraint(c)
|
510
|
+
}
|
464
511
|
constraint_fields = [
|
465
512
|
f
|
466
513
|
for f in self.fields
|
467
514
|
if f.systemName in update_props.constraint_fields
|
468
515
|
]
|
469
516
|
self.constraint = TagSchemaConstraint.from_constraint_fields(
|
470
|
-
constraint_fields,
|
517
|
+
constraint_fields, sequence_constraint
|
471
518
|
)
|
472
519
|
else:
|
473
520
|
self.constraint = None
|
liminal/entity_schemas/utils.py
CHANGED
@@ -5,12 +5,15 @@ from liminal.connection import BenchlingService
|
|
5
5
|
from liminal.dropdowns.utils import get_benchling_dropdown_id_name_map
|
6
6
|
from liminal.entity_schemas.tag_schema_models import TagSchemaFieldModel, TagSchemaModel
|
7
7
|
from liminal.enums import BenchlingAPIFieldType, BenchlingNamingStrategy
|
8
|
+
from liminal.enums.benchling_entity_type import BenchlingEntityType
|
9
|
+
from liminal.enums.sequence_constraint import SequenceConstraint
|
8
10
|
from liminal.mappers import (
|
9
11
|
convert_api_entity_type_to_entity_type,
|
10
12
|
convert_api_field_type_to_field_type,
|
11
13
|
)
|
12
14
|
from liminal.orm.name_template import NameTemplate
|
13
15
|
from liminal.orm.schema_properties import MixtureSchemaConfig, SchemaProperties
|
16
|
+
from liminal.unit_dictionary.utils import get_unit_id_to_name_map
|
14
17
|
|
15
18
|
|
16
19
|
def get_converted_tag_schemas(
|
@@ -24,6 +27,7 @@ def get_converted_tag_schemas(
|
|
24
27
|
"""
|
25
28
|
all_schemas = TagSchemaModel.get_all(benchling_service, wh_schema_names)
|
26
29
|
dropdowns_map = get_benchling_dropdown_id_name_map(benchling_service)
|
30
|
+
unit_id_to_name_map = get_unit_id_to_name_map(benchling_service)
|
27
31
|
all_schemas = (
|
28
32
|
all_schemas
|
29
33
|
if include_archived
|
@@ -31,7 +35,7 @@ def get_converted_tag_schemas(
|
|
31
35
|
)
|
32
36
|
return [
|
33
37
|
convert_tag_schema_to_internal_schema(
|
34
|
-
tag_schema, dropdowns_map, include_archived
|
38
|
+
tag_schema, dropdowns_map, unit_id_to_name_map, include_archived
|
35
39
|
)
|
36
40
|
for tag_schema in all_schemas
|
37
41
|
]
|
@@ -40,24 +44,38 @@ def get_converted_tag_schemas(
|
|
40
44
|
def convert_tag_schema_to_internal_schema(
|
41
45
|
tag_schema: TagSchemaModel,
|
42
46
|
dropdowns_map: dict[str, str],
|
47
|
+
unit_id_to_name_map: dict[str, str],
|
43
48
|
include_archived_fields: bool = False,
|
44
49
|
) -> tuple[SchemaProperties, NameTemplate, dict[str, BaseFieldProperties]]:
|
45
50
|
all_fields = tag_schema.allFields
|
46
51
|
if not include_archived_fields:
|
47
52
|
all_fields = [f for f in all_fields if not f.archiveRecord]
|
48
|
-
constraint_fields: set[str]
|
53
|
+
constraint_fields: set[str] = set()
|
54
|
+
entity_type = convert_api_entity_type_to_entity_type(
|
55
|
+
tag_schema.folderItemType, tag_schema.sequenceType
|
56
|
+
)
|
49
57
|
if tag_schema.constraint:
|
50
|
-
constraint_fields =
|
58
|
+
constraint_fields = constraint_fields.union(
|
59
|
+
[f.systemName for f in tag_schema.constraint.fields]
|
60
|
+
)
|
51
61
|
if tag_schema.constraint.uniqueResidues:
|
52
|
-
|
62
|
+
if entity_type.is_nt_sequence():
|
63
|
+
constraint_fields.add(SequenceConstraint.BASES.value)
|
64
|
+
elif entity_type == BenchlingEntityType.AA_SEQUENCE:
|
65
|
+
if tag_schema.constraint.areUniqueResiduesCaseSensitive:
|
66
|
+
constraint_fields.add(
|
67
|
+
SequenceConstraint.AMINO_ACIDS_EXACT_MATCH.value
|
68
|
+
)
|
69
|
+
else:
|
70
|
+
constraint_fields.add(
|
71
|
+
SequenceConstraint.AMINO_ACIDS_IGNORE_CASE.value
|
72
|
+
)
|
53
73
|
return (
|
54
74
|
SchemaProperties(
|
55
75
|
name=tag_schema.name,
|
56
76
|
prefix=tag_schema.prefix,
|
57
77
|
warehouse_name=tag_schema.sqlIdentifier,
|
58
|
-
entity_type=
|
59
|
-
tag_schema.folderItemType, tag_schema.sequenceType
|
60
|
-
),
|
78
|
+
entity_type=entity_type,
|
61
79
|
mixture_schema_config=MixtureSchemaConfig(
|
62
80
|
allowMeasuredIngredients=tag_schema.mixtureSchemaConfig.allowMeasuredIngredients,
|
63
81
|
componentLotStorageEnabled=tag_schema.mixtureSchemaConfig.componentLotStorageEnabled,
|
@@ -73,20 +91,25 @@ def convert_tag_schema_to_internal_schema(
|
|
73
91
|
_archived=tag_schema.archiveRecord is not None,
|
74
92
|
use_registry_id_as_label=tag_schema.useOrganizationCollectionAliasForDisplayLabel,
|
75
93
|
include_registry_id_in_chips=tag_schema.includeRegistryIdInChips,
|
94
|
+
show_bases_in_expanded_view=tag_schema.showResidues,
|
76
95
|
),
|
77
96
|
NameTemplate(
|
78
97
|
parts=tag_schema.get_internal_name_template_parts(),
|
79
98
|
order_name_parts_by_sequence=tag_schema.shouldOrderNamePartsBySequence,
|
80
99
|
),
|
81
100
|
{
|
82
|
-
f.systemName: convert_tag_schema_field_to_field_properties(
|
101
|
+
f.systemName: convert_tag_schema_field_to_field_properties(
|
102
|
+
f, dropdowns_map, unit_id_to_name_map
|
103
|
+
)
|
83
104
|
for f in all_fields
|
84
105
|
},
|
85
106
|
)
|
86
107
|
|
87
108
|
|
88
109
|
def convert_tag_schema_field_to_field_properties(
|
89
|
-
field: TagSchemaFieldModel,
|
110
|
+
field: TagSchemaFieldModel,
|
111
|
+
dropdowns_map: dict[str, str],
|
112
|
+
unit_id_to_name_map: dict[str, str],
|
90
113
|
) -> BaseFieldProperties:
|
91
114
|
return BaseFieldProperties(
|
92
115
|
name=field.name,
|
@@ -105,6 +128,10 @@ def convert_tag_schema_field_to_field_properties(
|
|
105
128
|
else None,
|
106
129
|
tooltip=field.tooltipText,
|
107
130
|
_archived=field.archiveRecord is not None,
|
131
|
+
unit_name=unit_id_to_name_map.get(field.unitApiIdentifier)
|
132
|
+
if field.unitApiIdentifier
|
133
|
+
else None,
|
134
|
+
decimal_places=field.decimalPrecision,
|
108
135
|
)
|
109
136
|
|
110
137
|
|
liminal/enums/__init__.py
CHANGED
@@ -5,3 +5,4 @@ from liminal.enums.benchling_field_type import BenchlingFieldType
|
|
5
5
|
from liminal.enums.benchling_folder_item_type import BenchlingFolderItemType
|
6
6
|
from liminal.enums.benchling_naming_strategy import BenchlingNamingStrategy
|
7
7
|
from liminal.enums.benchling_sequence_type import BenchlingSequenceType
|
8
|
+
from liminal.enums.sequence_constraint import SequenceConstraint
|
@@ -14,10 +14,19 @@ class BenchlingEntityType(StrEnum):
|
|
14
14
|
MIXTURE = "mixture"
|
15
15
|
MOLECULE = "molecule"
|
16
16
|
|
17
|
+
def is_nt_sequence(self) -> bool:
|
18
|
+
return self in [
|
19
|
+
self.DNA_SEQUENCE,
|
20
|
+
self.RNA_SEQUENCE,
|
21
|
+
self.DNA_OLIGO,
|
22
|
+
self.RNA_OLIGO,
|
23
|
+
]
|
24
|
+
|
17
25
|
def is_sequence(self) -> bool:
|
18
26
|
return self in [
|
19
27
|
self.DNA_SEQUENCE,
|
20
28
|
self.RNA_SEQUENCE,
|
21
29
|
self.DNA_OLIGO,
|
22
30
|
self.RNA_OLIGO,
|
31
|
+
self.AA_SEQUENCE,
|
23
32
|
]
|
@@ -35,4 +35,8 @@ class BenchlingFieldType(StrEnum):
|
|
35
35
|
|
36
36
|
@classmethod
|
37
37
|
def get_entity_link_types(cls) -> list[str]:
|
38
|
-
return [cls.ENTITY_LINK, cls.TRANSLATION_LINK]
|
38
|
+
return [cls.ENTITY_LINK, cls.TRANSLATION_LINK, cls.PART_LINK]
|
39
|
+
|
40
|
+
@classmethod
|
41
|
+
def get_number_field_types(cls) -> list[str]:
|
42
|
+
return [cls.INTEGER, cls.DECIMAL]
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from liminal.base.str_enum import StrEnum
|
2
|
+
|
3
|
+
|
4
|
+
class SequenceConstraint(StrEnum):
|
5
|
+
"""This enum represents hardcoded sequence constraints."""
|
6
|
+
|
7
|
+
BASES = "bases"
|
8
|
+
AMINO_ACIDS_IGNORE_CASE = "amino_acids_ignore_case"
|
9
|
+
AMINO_ACIDS_EXACT_MATCH = "amino_acids_exact_match"
|
10
|
+
|
11
|
+
@classmethod
|
12
|
+
def is_sequence_constraint(cls, constraint: str) -> bool:
|
13
|
+
return constraint in cls._value2member_map_
|
liminal/mappers.py
CHANGED
@@ -91,13 +91,10 @@ def convert_field_type_to_api_field_type(
|
|
91
91
|
BenchlingAPIFieldType.FILE_LINK,
|
92
92
|
BenchlingFolderItemType.SEQUENCE,
|
93
93
|
),
|
94
|
-
BenchlingFieldType.PART_LINK: (
|
95
|
-
BenchlingAPIFieldType.PART_LINK,
|
96
|
-
BenchlingFolderItemType.SEQUENCE,
|
97
|
-
),
|
94
|
+
BenchlingFieldType.PART_LINK: (BenchlingAPIFieldType.PART_LINK, None),
|
98
95
|
BenchlingFieldType.TRANSLATION_LINK: (
|
99
96
|
BenchlingAPIFieldType.TRANSLATION_LINK,
|
100
|
-
|
97
|
+
None,
|
101
98
|
),
|
102
99
|
BenchlingFieldType.ENTITY_LINK: (BenchlingAPIFieldType.FILE_LINK, None),
|
103
100
|
BenchlingFieldType.DECIMAL: (BenchlingAPIFieldType.FLOAT, None),
|
@@ -138,10 +135,7 @@ def convert_api_field_type_to_field_type(
|
|
138
135
|
BenchlingAPIFieldType.FILE_LINK,
|
139
136
|
BenchlingFolderItemType.PROTEIN,
|
140
137
|
): BenchlingFieldType.AA_SEQUENCE_LINK,
|
141
|
-
(
|
142
|
-
BenchlingAPIFieldType.PART_LINK,
|
143
|
-
BenchlingFolderItemType.SEQUENCE,
|
144
|
-
): BenchlingFieldType.PART_LINK,
|
138
|
+
(BenchlingAPIFieldType.PART_LINK, None): BenchlingFieldType.PART_LINK,
|
145
139
|
(
|
146
140
|
BenchlingAPIFieldType.TRANSLATION_LINK,
|
147
141
|
None,
|
liminal/orm/base_model.py
CHANGED
@@ -13,6 +13,8 @@ from sqlalchemy.orm.decl_api import declared_attr
|
|
13
13
|
|
14
14
|
from liminal.base.base_validation_filters import BaseValidatorFilters
|
15
15
|
from liminal.enums import BenchlingNamingStrategy
|
16
|
+
from liminal.enums.benchling_entity_type import BenchlingEntityType
|
17
|
+
from liminal.enums.sequence_constraint import SequenceConstraint
|
16
18
|
from liminal.orm.base import Base
|
17
19
|
from liminal.orm.base_tables.user import User
|
18
20
|
from liminal.orm.name_template import NameTemplate
|
@@ -63,16 +65,49 @@ class BaseModel(Generic[T], Base):
|
|
63
65
|
c[0] for c in cls.__dict__.items() if isinstance(c[1], SqlColumn)
|
64
66
|
]
|
65
67
|
# Validate constraints
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
)
|
68
|
+
invalid_constraints = [
|
69
|
+
c
|
70
|
+
for c in cls.__schema_properties__.constraint_fields
|
71
|
+
if c
|
72
|
+
not in set(column_wh_names)
|
73
|
+
| set(SequenceConstraint._value2member_map_.keys())
|
74
|
+
]
|
75
|
+
if invalid_constraints:
|
76
|
+
raise ValueError(
|
77
|
+
f"Constraints {', '.join(invalid_constraints)} are not fields on schema {cls.__schema_properties__.name}."
|
78
|
+
)
|
79
|
+
sequence_constraints = [
|
80
|
+
SequenceConstraint(c)
|
81
|
+
for c in cls.__schema_properties__.constraint_fields
|
82
|
+
if SequenceConstraint.is_sequence_constraint(c)
|
83
|
+
]
|
84
|
+
if len(sequence_constraints) > 1:
|
85
|
+
raise ValueError(
|
86
|
+
"Only one sequence constraint field can be set for a schema."
|
87
|
+
)
|
88
|
+
sequence_constraint = sequence_constraints[0] if sequence_constraints else None
|
89
|
+
match sequence_constraint:
|
90
|
+
case SequenceConstraint.BASES:
|
91
|
+
if not cls.__schema_properties__.entity_type.is_nt_sequence():
|
92
|
+
raise ValueError(
|
93
|
+
"`bases` constraint is only supported for nucleotide sequence entities."
|
94
|
+
)
|
95
|
+
case SequenceConstraint.AMINO_ACIDS_IGNORE_CASE:
|
96
|
+
if (
|
97
|
+
cls.__schema_properties__.entity_type
|
98
|
+
!= BenchlingEntityType.AA_SEQUENCE
|
99
|
+
):
|
100
|
+
raise ValueError(
|
101
|
+
"`amino_acids_ignore_case` constraint is only supported for aa_sequence entities."
|
102
|
+
)
|
103
|
+
case SequenceConstraint.AMINO_ACIDS_EXACT_MATCH:
|
104
|
+
if (
|
105
|
+
cls.__schema_properties__.entity_type
|
106
|
+
!= BenchlingEntityType.AA_SEQUENCE
|
107
|
+
):
|
108
|
+
raise ValueError(
|
109
|
+
"`amino_acids_exact_match` constraint is only supported for aa_sequence entities."
|
110
|
+
)
|
76
111
|
# Validate naming strategies
|
77
112
|
if any(
|
78
113
|
BenchlingNamingStrategy.is_template_based(strategy)
|
@@ -84,7 +119,7 @@ class BaseModel(Generic[T], Base):
|
|
84
119
|
)
|
85
120
|
# Validate name template
|
86
121
|
if cls.__name_template__:
|
87
|
-
if not cls.__schema_properties__.entity_type.
|
122
|
+
if not cls.__schema_properties__.entity_type.is_nt_sequence():
|
88
123
|
if cls.__name_template__.order_name_parts_by_sequence is True:
|
89
124
|
raise ValueError(
|
90
125
|
"order_name_parts_by_sequence is only supported for sequence entities. Must be set to False if entity type is not a sequence."
|
liminal/orm/column.py
CHANGED
@@ -32,6 +32,10 @@ class Column(SqlColumn):
|
|
32
32
|
The dropdown for the field.
|
33
33
|
entity_link : str | None = None
|
34
34
|
The warehouse name of the entity the field links to.
|
35
|
+
unit : str | None = None
|
36
|
+
The unit of the field. Searches for the unit warehouse name in the Unit Dictionary.
|
37
|
+
decimal_places : int | None = None
|
38
|
+
The number of decimal places for the field. Must be (0-15).
|
35
39
|
_warehouse_name : str | None = None
|
36
40
|
The warehouse name of the column. Necessary when the variable name is not the same as the warehouse name.
|
37
41
|
_archived : bool = False
|
@@ -48,6 +52,8 @@ class Column(SqlColumn):
|
|
48
52
|
tooltip: str | None = None,
|
49
53
|
dropdown: Type[BaseDropdown] | None = None, # noqa: UP006
|
50
54
|
entity_link: str | None = None,
|
55
|
+
unit_name: str | None = None,
|
56
|
+
decimal_places: int | None = None,
|
51
57
|
_warehouse_name: str | None = None,
|
52
58
|
_archived: bool = False,
|
53
59
|
**kwargs: Any,
|
@@ -64,6 +70,8 @@ class Column(SqlColumn):
|
|
64
70
|
entity_link=entity_link,
|
65
71
|
tooltip=tooltip,
|
66
72
|
_archived=_archived,
|
73
|
+
unit_name=unit_name,
|
74
|
+
decimal_places=decimal_places,
|
67
75
|
)
|
68
76
|
self.properties = properties
|
69
77
|
|
@@ -73,13 +81,23 @@ class Column(SqlColumn):
|
|
73
81
|
raise ValueError("Dropdown can only be set if the field type is DROPDOWN.")
|
74
82
|
if dropdown is None and type == BenchlingFieldType.DROPDOWN:
|
75
83
|
raise ValueError("Dropdown must be set if the field type is DROPDOWN.")
|
84
|
+
if unit_name and type not in BenchlingFieldType.get_number_field_types():
|
85
|
+
raise ValueError(
|
86
|
+
f"Unit can only be set if the field type is one of {BenchlingFieldType.get_number_field_types()}."
|
87
|
+
)
|
88
|
+
if decimal_places and type not in BenchlingFieldType.DECIMAL:
|
89
|
+
raise ValueError(
|
90
|
+
"Decimal places can only be set if the field type is DECIMAL."
|
91
|
+
)
|
92
|
+
if decimal_places and (decimal_places < 0 or decimal_places > 15):
|
93
|
+
raise ValueError("Decimal places must be between 0 and 15.")
|
76
94
|
if entity_link and type not in BenchlingFieldType.get_entity_link_types():
|
77
95
|
raise ValueError(
|
78
|
-
"Entity link can only be set if the field type is
|
96
|
+
f"Entity link can only be set if the field type is one of {BenchlingFieldType.get_entity_link_types()}."
|
79
97
|
)
|
80
98
|
if parent_link and type not in BenchlingFieldType.get_entity_link_types():
|
81
99
|
raise ValueError(
|
82
|
-
"Parent link can only be set if the field type is
|
100
|
+
f"Parent link can only be set if the field type is one of {BenchlingFieldType.get_entity_link_types()}."
|
83
101
|
)
|
84
102
|
if type in BenchlingFieldType.get_non_multi_select_types() and is_multi is True:
|
85
103
|
raise ValueError(f"Field type {type} cannot have multi-value set as True.")
|
liminal/orm/schema_properties.py
CHANGED
@@ -35,9 +35,14 @@ class SchemaProperties(BaseSchemaProperties):
|
|
35
35
|
Flag for configuring the chip label for entities. Determines if the chip will use the Registry ID as the main label for items.
|
36
36
|
include_registry_id_in_chips : bool | None = None
|
37
37
|
Flag for configuring the chip label for entities. Determines if the chip will include the Registry ID in the chip label.
|
38
|
-
constraint_fields : set[str]
|
39
|
-
Set of constraints for field values for the schema. Must be a set of column names
|
40
|
-
|
38
|
+
constraint_fields : set[str]
|
39
|
+
Set of constraints for field values for the schema. Must be a set of warehouse column names. This specifies that their entity field values must be a unique combination within an entity.
|
40
|
+
The following sequence constraints are also supported:
|
41
|
+
- bases: only supported for nucleotide sequence entity types. hasUniqueResidues=True
|
42
|
+
- amino_acids_ignore_case: only supported for amino acid sequence entity types. hasUniqueResidues=True
|
43
|
+
- amino_acids_exact_match: only supported for amino acid sequence entity types. hasUniqueResidues=True, areUniqueResiduesCaseSensitive=True
|
44
|
+
show_bases_in_expanded_view : bool | None = None
|
45
|
+
Whether the bases should be shown in the expanded view of the entity.
|
41
46
|
_archived : bool | None
|
42
47
|
Whether the schema is archived in Benchling.
|
43
48
|
"""
|
@@ -50,7 +55,8 @@ class SchemaProperties(BaseSchemaProperties):
|
|
50
55
|
use_registry_id_as_label: bool | None = False
|
51
56
|
include_registry_id_in_chips: bool | None = False
|
52
57
|
mixture_schema_config: MixtureSchemaConfig | None = None
|
53
|
-
constraint_fields: set[str]
|
58
|
+
constraint_fields: set[str] = set()
|
59
|
+
show_bases_in_expanded_view: bool | None = False
|
54
60
|
_archived: bool = False
|
55
61
|
|
56
62
|
def __init__(self, **data: Any):
|
@@ -73,7 +79,10 @@ class SchemaProperties(BaseSchemaProperties):
|
|
73
79
|
raise ValueError(
|
74
80
|
"The entity type is not a Mixture. Remove the mixture schema config."
|
75
81
|
)
|
76
|
-
|
82
|
+
if not self.entity_type.is_sequence() and self.show_bases_in_expanded_view:
|
83
|
+
raise ValueError(
|
84
|
+
"show_bases_in_expanded_view can only be set for sequence entities."
|
85
|
+
)
|
77
86
|
if self.naming_strategies and len(self.naming_strategies) == 0:
|
78
87
|
raise ValueError(
|
79
88
|
"Schema must have at least 1 registry naming option enabled"
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import json
|
2
|
+
from functools import lru_cache
|
3
|
+
|
4
|
+
from liminal.connection.benchling_service import BenchlingService
|
5
|
+
|
6
|
+
|
7
|
+
@lru_cache(maxsize=1)
|
8
|
+
def get_unit_name_to_id_map(benchling_service: BenchlingService) -> dict[str, str]:
|
9
|
+
response = benchling_service.api.get_response(
|
10
|
+
url="/api/v2-alpha/unit-types?pageSize=50"
|
11
|
+
)
|
12
|
+
unit_types = json.loads(response.content)["unitTypes"]
|
13
|
+
all_unit_types_flattened = {}
|
14
|
+
for unit_type in unit_types:
|
15
|
+
for unit in unit_type["units"]:
|
16
|
+
all_unit_types_flattened[unit["name"]] = unit["id"]
|
17
|
+
return all_unit_types_flattened
|
18
|
+
|
19
|
+
|
20
|
+
def get_unit_id_from_name(benchling_service: BenchlingService, unit_name: str) -> str:
|
21
|
+
unit_id = get_unit_name_to_id_map(benchling_service).get(unit_name)
|
22
|
+
if unit_id is None:
|
23
|
+
raise ValueError(
|
24
|
+
f"Unit {unit_name} not found in Benchling Unit Dictionary. Please check the field definition or your Unit Dictionary."
|
25
|
+
)
|
26
|
+
return unit_id
|
27
|
+
|
28
|
+
|
29
|
+
@lru_cache(maxsize=1)
|
30
|
+
def get_unit_id_to_name_map(benchling_service: BenchlingService) -> dict[str, str]:
|
31
|
+
response = benchling_service.api.get_response(
|
32
|
+
url="/api/v2-alpha/unit-types?pageSize=50"
|
33
|
+
)
|
34
|
+
unit_types = json.loads(response.content)["unitTypes"]
|
35
|
+
all_unit_types_flattened = {}
|
36
|
+
for unit_type in unit_types:
|
37
|
+
for unit in unit_type["units"]:
|
38
|
+
all_unit_types_flattened[unit["id"]] = unit["name"]
|
39
|
+
return all_unit_types_flattened
|
40
|
+
|
41
|
+
|
42
|
+
def get_unit_name_from_id(benchling_service: BenchlingService, unit_id: str) -> str:
|
43
|
+
unit_name = get_unit_id_to_name_map(benchling_service).get(unit_id)
|
44
|
+
if unit_name is None:
|
45
|
+
raise ValueError(
|
46
|
+
f"Unit {unit_id} not found in Benchling Unit Dictionary. Please check the field definition or your Unit Dictionary."
|
47
|
+
)
|
48
|
+
return unit_name
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: liminal-orm
|
3
|
-
Version:
|
3
|
+
Version: 3.0.0
|
4
4
|
Summary: An ORM and toolkit that builds on top of Benchling's platform to keep your schemas and downstream code dependencies in sync.
|
5
5
|
Home-page: https://github.com/dynotx/liminal-orm
|
6
6
|
Author: DynoTx Open Source
|
@@ -1,13 +1,13 @@
|
|
1
1
|
liminal/.DS_Store,sha256=s_ehSI1aIzOjVRnFlcSzhtWS3irmEDSGHyS6l0QRcus,8196
|
2
2
|
liminal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
liminal/base/base_dropdown.py,sha256=Unk4l_5Y8rj_eSWYqzFi2BAFSQToQDWW2qdXwiCHTg8,2523
|
4
|
-
liminal/base/base_operation.py,sha256=
|
4
|
+
liminal/base/base_operation.py,sha256=opQfFZeC49YAFkg5ahE6CFpeSUNPh1ootWZxXyEXfFI,3128
|
5
5
|
liminal/base/base_validation_filters.py,sha256=kHG3G5gXkuNHQosMTrxRc57OTmczcaoSx0DmkrScIr4,1043
|
6
6
|
liminal/base/compare_operation.py,sha256=hkpv4ewHhxy4dlTPKgJuzBjsAqO6Km7OrrKB44pRA_o,352
|
7
7
|
liminal/base/name_template_parts.py,sha256=dJeyrhhhZHDwKXe_p7AtgDlbZlzsnYQ8FoM8FXVF7q0,271
|
8
|
-
liminal/base/properties/base_field_properties.py,sha256=
|
8
|
+
liminal/base/properties/base_field_properties.py,sha256=ze8CLf0jPPYwklFYVKXkRvXiBBA9ilG5nK1_oP0rS04,4970
|
9
9
|
liminal/base/properties/base_name_template.py,sha256=AOtaW4QEDRC-mjZOZk6jgc_mopUMsHS2Fj6VVsO07WY,3150
|
10
|
-
liminal/base/properties/base_schema_properties.py,sha256=
|
10
|
+
liminal/base/properties/base_schema_properties.py,sha256=11QaxtUoHZx25QFv7HP8gQBpknrFthVeyWuMnI_lV5g,5891
|
11
11
|
liminal/base/str_enum.py,sha256=jF3d-Lo8zsHUe6GsctX2L-TSj92Y3qCYDrTD-saeJoc,210
|
12
12
|
liminal/cli/cli.py,sha256=JxWHLO9KMeMaOnOYwzdH0w71l0477ScFOkWNtTlc97Y,9045
|
13
13
|
liminal/cli/controller.py,sha256=QNj3QO9TMb9hfc6U-VhLuFa0_aohOHZUmvY4XkATPhw,10118
|
@@ -23,36 +23,37 @@ liminal/dropdowns/operations.py,sha256=-TRIsxqnUtrIUjhrt5k_PdiBCDUXsXDzsOUmznJE-
|
|
23
23
|
liminal/dropdowns/utils.py,sha256=1-H7bTszCUeqeRBpiYXjRjreDzhn1Fd1MFwIsrEI-o4,4109
|
24
24
|
liminal/entity_schemas/api.py,sha256=Emn_Y95cAG9Wis6tpchw6QBVKQh4If86LOdgKk0Ndjw,3575
|
25
25
|
liminal/entity_schemas/compare.py,sha256=t6tl67GWaMoNNcPxyLpCuNAlN3OWNqURTo3EUEMtETE,17549
|
26
|
-
liminal/entity_schemas/entity_schema_models.py,sha256=
|
27
|
-
liminal/entity_schemas/generate_files.py,sha256=
|
28
|
-
liminal/entity_schemas/operations.py,sha256=
|
29
|
-
liminal/entity_schemas/tag_schema_models.py,sha256=
|
30
|
-
liminal/entity_schemas/utils.py,sha256=
|
31
|
-
liminal/enums/__init__.py,sha256
|
26
|
+
liminal/entity_schemas/entity_schema_models.py,sha256=v5A1ELaiuBnUSl1HkUNAeMuIRQeQnIKzfpFxmsiKWh0,8349
|
27
|
+
liminal/entity_schemas/generate_files.py,sha256=u9SoDO9f4qL2nZaddln__2J0zJ3QMFBQhiUabn22aUY,9032
|
28
|
+
liminal/entity_schemas/operations.py,sha256=jd6Wiq_rW0UIjiVqUACio_Lwbv3fGrtoyQRGBHtXJHo,26654
|
29
|
+
liminal/entity_schemas/tag_schema_models.py,sha256=FlpoDWh083nA8iT7Pl84HVXKb7StIApV1ExvwHKiAfc,24002
|
30
|
+
liminal/entity_schemas/utils.py,sha256=2ZHyLxnYITVEuyAWxNdsq5hcNSgvN7pN3-uUzyocYSk,6161
|
31
|
+
liminal/enums/__init__.py,sha256=-szuqAwMED4ai0NaPVUfgihQJAJ27wPu_nDnj4cEgTk,518
|
32
32
|
liminal/enums/benchling_api_field_type.py,sha256=0QamSWEMnxZtedZXlh6zNhSRogS9ZqvWskdHHN19xJo,633
|
33
|
-
liminal/enums/benchling_entity_type.py,sha256=
|
34
|
-
liminal/enums/benchling_field_type.py,sha256=
|
33
|
+
liminal/enums/benchling_entity_type.py,sha256=H_6ZlHJsiVNMpezPBrNKo2eP0pDrt--HU-P7PgznaMA,846
|
34
|
+
liminal/enums/benchling_field_type.py,sha256=kKbLR6_gTP3qI-bNOWDO3csfOXI50Y6p6buQH7cQqUg,1194
|
35
35
|
liminal/enums/benchling_folder_item_type.py,sha256=Jb-YxCvB8O86_qTsfwtLQOkKGjTWGKHFwIKf24eemYk,248
|
36
36
|
liminal/enums/benchling_naming_strategy.py,sha256=jmaR-Vfj3MWhna8tANBNjAgYUyoQ5wMbz1AIy2bv6Zk,1258
|
37
37
|
liminal/enums/benchling_sequence_type.py,sha256=TBI4C5c1XKE4ZXqsz1ApDUzy2wR-04u-M3VO_zLikjM,202
|
38
38
|
liminal/enums/name_template_part_type.py,sha256=Kv0phZIO_dPN3tLHM0lT2tjUd3zBGqpJQGahEpGjNcU,365
|
39
|
+
liminal/enums/sequence_constraint.py,sha256=CT3msm8qzJpcivfbQZ3NOWNRsedH4mSlfhzvQBLrHWA,407
|
39
40
|
liminal/external/__init__.py,sha256=nMpyzpBXpYhTvN3R3HQEiYb24_U2AYjz_20seAUUK9s,1264
|
40
|
-
liminal/mappers.py,sha256=
|
41
|
+
liminal/mappers.py,sha256=TgPMQsLrESAI6D7KBl0UoBBpnxYgcgGOT7a2faWsuhY,9587
|
41
42
|
liminal/migrate/components.py,sha256=2HuFp5KDNhofROMRI-BioUoA4CCjhQ_v_F0QmGJzUBU,3480
|
42
43
|
liminal/migrate/revision.py,sha256=KppU0u-d0JsfPsXsmncxy9Q_XBJyf-o4e16wNZAJODM,7774
|
43
44
|
liminal/migrate/revisions_timeline.py,sha256=G9VwxPrLhLqKOrIXyxrXyHpujc-72m7omsZjI5-0D0M,14520
|
44
45
|
liminal/migrate/utils.py,sha256=HdSr3N2WN_1S-PLRGVWSMYl-4gIcP-Ph2wPycGi2cGg,3404
|
45
46
|
liminal/orm/base.py,sha256=fFSpiNRYgK5UG7lbXdQGV8KgO8pwjMqt0pycM3rWJ2o,615
|
46
|
-
liminal/orm/base_model.py,sha256=
|
47
|
+
liminal/orm/base_model.py,sha256=FFkrB-lMAAgp77BUnQDu39IxZxmhHu52CtCLeBZVNGA,15527
|
47
48
|
liminal/orm/base_tables/registry_entity.py,sha256=4ET1cepTGjZ3AMFI5q-iMYxMObzXwuUDBD0jNNqCipE,2126
|
48
49
|
liminal/orm/base_tables/schema.py,sha256=7_btCVSUJxjVdGcKVRKL8sKcNw7-_gazTpfEh1jru3o,921
|
49
50
|
liminal/orm/base_tables/user.py,sha256=elRAHj7HgO3iVLK_pNCIwf_9Rl_9k6vkBgaYazoJSQc,818
|
50
|
-
liminal/orm/column.py,sha256=
|
51
|
+
liminal/orm/column.py,sha256=aK-MrKabOK5tf3UFPpRACq83YVVrjXITZF_rcOU-wPQ,6207
|
51
52
|
liminal/orm/mixins.py,sha256=yEeUDF1qEBLP523q8bZra4KtNVK0gwZN9mXJSNe3GEE,4802
|
52
53
|
liminal/orm/name_template.py,sha256=ftXZOiRR6gGGvGaZkFVDXKOboIHFWauhQENRguBGWMI,1739
|
53
54
|
liminal/orm/name_template_parts.py,sha256=KCGXAcCuOqCjlgYn-mw1K7fwDI92D20l-FnlpEVrbM8,2771
|
54
55
|
liminal/orm/relationship.py,sha256=Zl4bMHbtDSPx1psGHYnojGGJpA8B8hwcPJdgjB1lmW0,2490
|
55
|
-
liminal/orm/schema_properties.py,sha256=
|
56
|
+
liminal/orm/schema_properties.py,sha256=vqqjnxbh7AYh9ZvSmhCsl69BqSBPpQutNKImb-TBCGg,4167
|
56
57
|
liminal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
57
58
|
liminal/tests/.DS_Store,sha256=0sTLf7flLKL2_3KGceYriAB8_gXTcYwn0c2RVSYmLZk,6148
|
58
59
|
liminal/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -60,11 +61,12 @@ liminal/tests/conftest.py,sha256=B463eOfe1uCHDJsUNvG-6tY8Qx8FJMByGDOtuyM87lA,176
|
|
60
61
|
liminal/tests/from benchling_sdk.py,sha256=CjRUHFB3iaa4rUPLGOqDiBq5EPKldm-Fd8aQQr92zF4,147
|
61
62
|
liminal/tests/test_dropdown_compare.py,sha256=yHB0ovQlBLRu8-qYkqIPd8VtYEOmOft_93FQM86g_z8,8198
|
62
63
|
liminal/tests/test_entity_schema_compare.py,sha256=-26Bu5eYIuHRswB5kYjGDo5Wed5LUWjm1e6IRI1Q-lE,18952
|
64
|
+
liminal/unit_dictionary/utils.py,sha256=o3K06Yyt33iIUSMHPT8f1vSuUSgWjZLf51p78lx4SZs,1817
|
63
65
|
liminal/utils.py,sha256=radRtRsZmCiNblMvxOX1DH0rcO5TR09kFlp6OONIPBU,2951
|
64
66
|
liminal/validation/__init__.py,sha256=TVaHrSF3GnSd4mbZrPn8TBHscGWkAPKAUUPq7-symC8,5275
|
65
67
|
liminal/validation/validation_severity.py,sha256=ib03PTZCQHcbBDc01v4gJF53YtA-ANY6QSFnhTV-FbU,259
|
66
|
-
liminal_orm-
|
67
|
-
liminal_orm-
|
68
|
-
liminal_orm-
|
69
|
-
liminal_orm-
|
70
|
-
liminal_orm-
|
68
|
+
liminal_orm-3.0.0.dist-info/LICENSE.md,sha256=oVA877F_D1AV44dpjsv4f-4k690uNGApX1EtzOo3T8U,11353
|
69
|
+
liminal_orm-3.0.0.dist-info/METADATA,sha256=36MOv4Hh9h8gTmKFAtfn-XpJerX15rLphdD8WWDPsxQ,11032
|
70
|
+
liminal_orm-3.0.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
71
|
+
liminal_orm-3.0.0.dist-info/entry_points.txt,sha256=atIrU63rrzH81dWC2sjUbFLlc5FWMmYRdMxXEWexIZA,47
|
72
|
+
liminal_orm-3.0.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|