liminal-orm 2.0.4__py3-none-any.whl → 3.0.1__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.
@@ -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. UpdateEntitySchemaNameTemplate
17
- 10. UpdateSchema
18
- 11. UnarchiveSchema
19
- 12. CreateField
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, exclude_defaults=True).items()])})"
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 that specify that their values must be a unique combination within an entity.
70
- If the entity type is a Sequence, "bases" can be a constraint field.
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
- hasUniqueCanonicalSmilers: bool | None = None
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, constraint_fields: set[str]
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
- if "bases" in constraint_fields:
44
- constraint_fields.discard("bases")
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=[{"name": f} for f in constraint_fields],
65
+ fields=fields,
48
66
  hasUniqueResidues=hasUniqueResidues,
49
- hasUniqueCanonicalSmilers=False,
50
- areUniqueResiduesCaseSensitive=False,
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(schema, dropdowns_map)
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] = 110
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] = 100
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] = 90
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] = 120
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):
@@ -55,8 +58,12 @@ class NameTemplatePartModel(BaseModel):
55
58
  field = next((f for f in fields if f.systemName == wh_field_name), None)
56
59
  if field is None:
57
60
  raise ValueError(f"Field {wh_field_name} not found in fields")
58
- field_id = field.id
59
- if part.component_type == NameTemplatePartType.CHILD_ENTITY_LOT_NUMBER:
61
+ field_id = field.apiId
62
+ if (
63
+ part.component_type == NameTemplatePartType.CHILD_ENTITY_LOT_NUMBER
64
+ or part.component_type
65
+ == NameTemplatePartType.LINKED_BIOENTITY_REGISTRY_IDENTIFIER
66
+ ):
60
67
  if not field.isParentLink:
61
68
  raise ValueError(
62
69
  f"Field {wh_field_name} is not a parent link field. The field for type {part.component_type} must be a parent link field."
@@ -91,16 +98,28 @@ class TagSchemaConstraint(BaseModel):
91
98
 
92
99
  @classmethod
93
100
  def from_constraint_fields(
94
- cls, constraint_fields: list[TagSchemaFieldModel], bases: bool
101
+ cls,
102
+ constraint_fields: list[TagSchemaFieldModel],
103
+ sequence_constraint: SequenceConstraint | None = None,
95
104
  ) -> TagSchemaConstraint:
96
105
  """
97
106
  Generates a Constraint object from a set of constraint fields to create a constraint on a schema.
98
107
  """
108
+ uniqueResidues = False
109
+ areUniqueResiduesCaseSensitive = False
110
+ match sequence_constraint:
111
+ case SequenceConstraint.BASES:
112
+ uniqueResidues = True
113
+ case SequenceConstraint.AMINO_ACIDS_IGNORE_CASE:
114
+ uniqueResidues = True
115
+ case SequenceConstraint.AMINO_ACIDS_EXACT_MATCH:
116
+ uniqueResidues = True
117
+ areUniqueResiduesCaseSensitive = True
99
118
  return cls(
100
119
  fields=constraint_fields,
101
- uniqueResidues=bases,
120
+ uniqueResidues=uniqueResidues,
102
121
  uniqueCanonicalSmilers=False,
103
- areUniqueResiduesCaseSensitive=False,
122
+ areUniqueResiduesCaseSensitive=areUniqueResiduesCaseSensitive,
104
123
  )
105
124
 
106
125
 
@@ -135,6 +154,8 @@ class CreateTagSchemaFieldModel(BaseModel):
135
154
  name: str
136
155
  requiredLink: FieldRequiredLinkShortModel | None = None
137
156
  tooltipText: str | None = None
157
+ decimalPrecision: int | None = None
158
+ unitApiIdentifier: str | None = None
138
159
 
139
160
  @classmethod
140
161
  def from_props(
@@ -184,6 +205,13 @@ class CreateTagSchemaFieldModel(BaseModel):
184
205
  dropdown_summary_id = get_benchling_dropdown_summary_by_name(
185
206
  benchling_service, new_props.dropdown_link
186
207
  ).id
208
+ unit_api_identifier = None
209
+ if new_props.unit_name:
210
+ if benchling_service is None:
211
+ raise ValueError("Benchling SDK must be provided to update unit field.")
212
+ unit_api_identifier = get_unit_id_from_name(
213
+ benchling_service, new_props.unit_name
214
+ )
187
215
  return cls(
188
216
  name=new_props.name,
189
217
  systemName=new_props.warehouse_name,
@@ -197,6 +225,8 @@ class CreateTagSchemaFieldModel(BaseModel):
197
225
  tagSchema=tagSchema,
198
226
  ),
199
227
  tooltipText=new_props.tooltip,
228
+ unitApiIdentifier=unit_api_identifier,
229
+ decimalPrecision=new_props.decimal_places,
200
230
  )
201
231
 
202
232
 
@@ -230,6 +260,8 @@ class TagSchemaFieldModel(BaseModel):
230
260
  strictSelector: bool | None
231
261
  systemName: str
232
262
  tooltipText: str | None
263
+ unitApiIdentifier: str | None
264
+ unitSymbol: str | None = None
233
265
 
234
266
  def update_from_props(
235
267
  self,
@@ -307,6 +339,22 @@ class TagSchemaFieldModel(BaseModel):
307
339
  benchling_service, update_props.dropdown_link
308
340
  ).id
309
341
  self.schemaFieldSelectorId = dropdown_summary_id
342
+ self.decimalPrecision = (
343
+ update_props.decimal_places
344
+ if "decimal_places" in update_diff_names
345
+ else self.decimalPrecision
346
+ )
347
+ if "unit_name" in update_diff_names:
348
+ if update_props.unit_name is None:
349
+ self.unitApiIdentifier = None
350
+ else:
351
+ if benchling_service is None:
352
+ raise ValueError(
353
+ "Benchling SDK must be provided to update unit field."
354
+ )
355
+ self.unitApiIdentifier = get_unit_id_from_name(
356
+ benchling_service, update_props.unit_name
357
+ )
310
358
  return self
311
359
 
312
360
 
@@ -405,6 +453,15 @@ class TagSchemaModel(BaseModel):
405
453
  )
406
454
  return cls.model_validate(schema)
407
455
 
456
+ @classmethod
457
+ @lru_cache(maxsize=100)
458
+ def get_one_cached(
459
+ cls,
460
+ benchling_service: BenchlingService,
461
+ wh_schema_name: str,
462
+ ) -> TagSchemaModel:
463
+ return cls.get_one(benchling_service, wh_schema_name)
464
+
408
465
  def get_field(self, wh_field_name: str) -> TagSchemaFieldModel:
409
466
  """Returns a field from the tag schema by its warehouse field name."""
410
467
  for field in self.allFields:
@@ -437,37 +494,31 @@ class TagSchemaModel(BaseModel):
437
494
  )
438
495
  if "include_registry_id_in_chips" in update_diff_names:
439
496
  self.includeRegistryIdInChips = update_props.include_registry_id_in_chips
497
+ if "show_bases_in_expanded_view" in update_diff_names:
498
+ self.showResidues = update_props.show_bases_in_expanded_view
440
499
 
441
500
  if "constraint_fields" in update_diff_names:
442
501
  if update_props.constraint_fields:
443
- has_bases = False
444
- if "bases" in update_props.constraint_fields:
445
- has_bases = True
446
- update_props.constraint_fields.discard("bases")
447
- constraint_fields = [
448
- f
449
- for f in self.fields
450
- if f.systemName in update_props.constraint_fields
451
- ]
452
- self.constraint = TagSchemaConstraint.from_constraint_fields(
453
- constraint_fields, has_bases
502
+ sequence_constraint = next(
503
+ (
504
+ SequenceConstraint(c)
505
+ for c in update_props.constraint_fields
506
+ if SequenceConstraint.is_sequence_constraint(c)
507
+ ),
508
+ None,
454
509
  )
455
- else:
456
- self.constraint = None
457
-
458
- if "constraint_fields" in update_diff_names:
459
- if update_props.constraint_fields:
460
- has_bases = False
461
- if "bases" in update_props.constraint_fields:
462
- has_bases = True
463
- update_props.constraint_fields.discard("bases")
510
+ update_props.constraint_fields = {
511
+ c
512
+ for c in update_props.constraint_fields
513
+ if not SequenceConstraint.is_sequence_constraint(c)
514
+ }
464
515
  constraint_fields = [
465
516
  f
466
517
  for f in self.fields
467
518
  if f.systemName in update_props.constraint_fields
468
519
  ]
469
520
  self.constraint = TagSchemaConstraint.from_constraint_fields(
470
- constraint_fields, has_bases
521
+ constraint_fields, sequence_constraint
471
522
  )
472
523
  else:
473
524
  self.constraint = None
@@ -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] | None = None
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 = set([f.systemName for f in tag_schema.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
- constraint_fields.add("bases")
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=convert_api_entity_type_to_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(f, dropdowns_map)
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, dropdowns_map: dict[str, str]
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]
@@ -8,5 +8,7 @@ class NameTemplatePartType(StrEnum):
8
8
  CREATION_DATE = "CREATED_AT_DATE"
9
9
  FIELD = "FIELD"
10
10
  REGISTRY_IDENTIFIER_NUMBER = "REGISTRY_IDENTIFIER_NUMBER"
11
+ LINKED_BIOENTITY_REGISTRY_IDENTIFIER = "LINKED_BIOENTITY_REGISTRY_IDENTIFIER"
11
12
  PROJECT = "PROJECT"
12
13
  CHILD_ENTITY_LOT_NUMBER = "CHILD_ENTITY_LOT_NUMBER"
14
+ COMPLEX_POLYMER_COMPONENT = "COMPLEX_POLYMER_COMPONENT"
@@ -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_
@@ -34,9 +34,12 @@ from liminal.enums import (
34
34
  BenchlingSequenceType,
35
35
  )
36
36
  from liminal.orm.name_template_parts import (
37
+ ComplexPolymerComponentPart,
37
38
  CreationDatePart,
38
39
  CreationYearPart,
39
40
  FieldPart,
41
+ ParentLotNumberPart,
42
+ ParentRegistryIdPart,
40
43
  ProjectPart,
41
44
  RegistryIdentifierNumberPart,
42
45
  SeparatorPart,
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
- BenchlingFolderItemType.SEQUENCE,
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
- if cls.__schema_properties__.constraint_fields:
67
- invalid_constraints = [
68
- c
69
- for c in cls.__schema_properties__.constraint_fields
70
- if c not in (set(column_wh_names) | {"bases"})
71
- ]
72
- if invalid_constraints:
73
- raise ValueError(
74
- f"Constraints {', '.join(invalid_constraints)} are not fields on schema {cls.__schema_properties__.name}."
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.is_sequence():
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 ENTITY_LINK or TRANSLATION_LINK."
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 ENTITY_LINK or TRANSLATION_LINK."
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.")
@@ -74,6 +74,13 @@ class ParentLotNumberPart(NameTemplatePart):
74
74
  wh_field_name: str
75
75
 
76
76
 
77
+ class ParentRegistryIdPart(NameTemplatePart):
78
+ component_type: ClassVar[NameTemplatePartType] = (
79
+ NameTemplatePartType.LINKED_BIOENTITY_REGISTRY_IDENTIFIER
80
+ )
81
+ wh_field_name: str
82
+
83
+
77
84
  class RegistryIdentifierNumberPart(NameTemplatePart):
78
85
  component_type: ClassVar[NameTemplatePartType] = (
79
86
  NameTemplatePartType.REGISTRY_IDENTIFIER_NUMBER
@@ -84,6 +91,12 @@ class ProjectPart(NameTemplatePart):
84
91
  component_type: ClassVar[NameTemplatePartType] = NameTemplatePartType.PROJECT
85
92
 
86
93
 
94
+ class ComplexPolymerComponentPart(NameTemplatePart):
95
+ component_type: ClassVar[NameTemplatePartType] = (
96
+ NameTemplatePartType.COMPLEX_POLYMER_COMPONENT
97
+ )
98
+
99
+
87
100
  NameTemplateParts = (
88
101
  SeparatorPart
89
102
  | TextPart
@@ -93,4 +106,6 @@ NameTemplateParts = (
93
106
  | RegistryIdentifierNumberPart
94
107
  | ProjectPart
95
108
  | ParentLotNumberPart
109
+ | ParentRegistryIdPart
110
+ | ComplexPolymerComponentPart
96
111
  )
@@ -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] | None
39
- Set of constraints for field values for the schema. Must be a set of column names that specify that their values must be a unique combination within an entity.
40
- If the entity type is a Sequence, "bases" can be a constraint field.
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] | None = None
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: 2.0.4
3
+ Version: 3.0.1
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=8Mus5Aao3QeYyMb6EBcuAu9kpVgiomtNH5ulq9kMfG8,3128
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=NvuQjYZ5U_2363P2c8FXmmLfOtdzUqTuZTjDDfXnO-E,4922
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=pVeIVHea-3Wt0fde9CsOvluwVmYMbsMhGeSncRL2Ats,5351
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=YDpz1XkNc9e51zah8Z6qCk30gAuXP6xLEYv1Lb3ECpA,6838
27
- liminal/entity_schemas/generate_files.py,sha256=2_E-QdYdci10ORqs-AguPUXmA1le0wnyh7EmflrcTEA,8889
28
- liminal/entity_schemas/operations.py,sha256=PvsPYHd5_geDwbkcQjRK4yfnYT7t6V36QLG-C8GISEQ,24020
29
- liminal/entity_schemas/tag_schema_models.py,sha256=by5HnKi08Oj00buc751jFnDVAhbdKep7tbBaE5A9Plg,22028
30
- liminal/entity_schemas/utils.py,sha256=iZ1_M2r8zKOCFj9QSMdrv-_4XznDn_znAOfoP4Mh1jA,4943
31
- liminal/enums/__init__.py,sha256=Ue_3QtElW-JMSWtu4vGsAOFQbYnzHHZUdkWpdkzkKA4,453
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=DZQYzlxt3aEHbLy00qEyDZC_mRyi9I325ggkfcNgR1I,24153
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=BS6U8qnRM3I3xTTqp9BbInV7yjPh9gC3ULvN6-zLaCM,624
34
- liminal/enums/benchling_field_type.py,sha256=VI0gdpeQR9Ja5VuusxLdScZKwLmN9rqIRD7_9ByC5O4,1069
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
- liminal/enums/name_template_part_type.py,sha256=Kv0phZIO_dPN3tLHM0lT2tjUd3zBGqpJQGahEpGjNcU,365
39
- liminal/external/__init__.py,sha256=nMpyzpBXpYhTvN3R3HQEiYb24_U2AYjz_20seAUUK9s,1264
40
- liminal/mappers.py,sha256=4GkotNH_yrcRdbfbTWUtUY_bMqXwMKcIGaUP-MEtZNA,9741
38
+ liminal/enums/name_template_part_type.py,sha256=Z3Zv5PpzoUrIj_EvwPVgDDkY2G0kO-wE3-ZvEvnv86M,507
39
+ liminal/enums/sequence_constraint.py,sha256=CT3msm8qzJpcivfbQZ3NOWNRsedH4mSlfhzvQBLrHWA,407
40
+ liminal/external/__init__.py,sha256=EundQBe68_ZIhcsuSOhc-CznzYauNDYlNG1CjRDui_Y,1348
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=Nf4gSEwvORRdnY5ODW79ddJWxnshLLvrPo8xcimHH6c,13891
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=_Ie2edtkAmTNV80fswM6plz2LfuqTfO2nvyTZPkyFc8,5198
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
- liminal/orm/name_template_parts.py,sha256=KCGXAcCuOqCjlgYn-mw1K7fwDI92D20l-FnlpEVrbM8,2771
54
+ liminal/orm/name_template_parts.py,sha256=iI4S9ZGvckQbYjhgFdn6xeJ3rS90lzCvjH0863HnAi8,3201
54
55
  liminal/orm/relationship.py,sha256=Zl4bMHbtDSPx1psGHYnojGGJpA8B8hwcPJdgjB1lmW0,2490
55
- liminal/orm/schema_properties.py,sha256=yv6MOsE_16OWJnGDh2K8wI_054PJwafYmHgY_Awr3XA,3420
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-2.0.4.dist-info/LICENSE.md,sha256=oVA877F_D1AV44dpjsv4f-4k690uNGApX1EtzOo3T8U,11353
67
- liminal_orm-2.0.4.dist-info/METADATA,sha256=Zfj3MIn8MMxLTUbQJODThCNCDTmefMwpuV-myafdaTA,11032
68
- liminal_orm-2.0.4.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
69
- liminal_orm-2.0.4.dist-info/entry_points.txt,sha256=atIrU63rrzH81dWC2sjUbFLlc5FWMmYRdMxXEWexIZA,47
70
- liminal_orm-2.0.4.dist-info/RECORD,,
68
+ liminal_orm-3.0.1.dist-info/LICENSE.md,sha256=oVA877F_D1AV44dpjsv4f-4k690uNGApX1EtzOo3T8U,11353
69
+ liminal_orm-3.0.1.dist-info/METADATA,sha256=gA_yNA-bEIUIe1nAJ6kZvF8fn-NT_Ov2sFbF-zngGsY,11032
70
+ liminal_orm-3.0.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
71
+ liminal_orm-3.0.1.dist-info/entry_points.txt,sha256=atIrU63rrzH81dWC2sjUbFLlc5FWMmYRdMxXEWexIZA,47
72
+ liminal_orm-3.0.1.dist-info/RECORD,,