liminal-orm 2.0.3a1__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/.DS_Store ADDED
Binary file
@@ -13,13 +13,13 @@ Order of operations based on order class var:
13
13
  6. ArchiveDropdownOption
14
14
  7. ReorderDropdownOptions
15
15
  8. CreateSchema
16
- 9. UpdateSchema
17
- 10. UnarchiveSchema
18
- 11. CreateField
19
- 12. UnarchiveField
20
- 13. UpdateField
21
- 14. ArchiveField
22
- 15. UpdateEntitySchemaNameTemplate
16
+ 9. UnarchiveSchema
17
+ 10. CreateField
18
+ 11. UpdateEntitySchemaNameTemplate
19
+ 12. UpdateSchema
20
+ 13. UnarchiveField
21
+ 14. UpdateField
22
+ 15. ArchiveField
23
23
  16. ReorderFields
24
24
  17. ArchiveSchema
25
25
  18. ArchiveDropdown
@@ -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):
@@ -18,6 +18,7 @@ from liminal.entity_schemas.operations import (
18
18
  UpdateEntitySchemaNameTemplate,
19
19
  )
20
20
  from liminal.entity_schemas.utils import get_converted_tag_schemas
21
+ from liminal.enums.benchling_naming_strategy import BenchlingNamingStrategy
21
22
  from liminal.orm.base_model import BaseModel
22
23
  from liminal.orm.column import Column
23
24
  from liminal.utils import to_snake_case
@@ -275,6 +276,16 @@ def compare_entity_schemas(
275
276
  col.properties.set_warehouse_name(wh_name)
276
277
  for wh_name, col in model_columns.items()
277
278
  ]
279
+ template_based_naming_strategies = {
280
+ s
281
+ for s in model.__schema_properties__.naming_strategies
282
+ if BenchlingNamingStrategy.is_template_based(s)
283
+ }
284
+ model.__schema_properties__.naming_strategies = {
285
+ s
286
+ for s in model.__schema_properties__.naming_strategies
287
+ if not BenchlingNamingStrategy.is_template_based(s)
288
+ }
278
289
  ops.append(
279
290
  CompareOperation(
280
291
  op=CreateEntitySchema(
@@ -288,28 +299,10 @@ def compare_entity_schemas(
288
299
  ),
289
300
  )
290
301
  )
291
- benchling_given_wh_name = to_snake_case(model.__schema_properties__.name)
292
- if model.__schema_properties__.warehouse_name != benchling_given_wh_name:
293
- ops.append(
294
- CompareOperation(
295
- op=UpdateEntitySchema(
296
- benchling_given_wh_name,
297
- BaseSchemaProperties(
298
- warehouse_name=model.__schema_properties__.warehouse_name
299
- ),
300
- ),
301
- reverse_op=UpdateEntitySchema(
302
- model.__schema_properties__.warehouse_name,
303
- BaseSchemaProperties(
304
- warehouse_name=benchling_given_wh_name
305
- ),
306
- ),
307
- )
308
- )
309
302
  benchling_given_name_template = BaseNameTemplate(
310
303
  parts=[], order_name_parts_by_sequence=False
311
304
  )
312
- if benchling_name_template != model.__name_template__:
305
+ if benchling_given_name_template != model.__name_template__:
313
306
  ops.append(
314
307
  CompareOperation(
315
308
  op=UpdateEntitySchemaNameTemplate(
@@ -323,13 +316,39 @@ def compare_entity_schemas(
323
316
  reverse_op=UpdateEntitySchemaNameTemplate(
324
317
  model.__schema_properties__.warehouse_name,
325
318
  BaseNameTemplate(
326
- **benchling_given_name_template.merge(
327
- model.__name_template__
319
+ **model.__name_template__.merge(
320
+ benchling_given_name_template
328
321
  )
329
322
  ),
330
323
  ),
331
324
  )
332
325
  )
326
+ benchling_given_wh_name = to_snake_case(model.__schema_properties__.name)
327
+ new_schema_props = BaseSchemaProperties()
328
+ rollback_schema_props = BaseSchemaProperties()
329
+ if model.__schema_properties__.warehouse_name != benchling_given_wh_name:
330
+ new_schema_props.warehouse_name = (
331
+ model.__schema_properties__.warehouse_name
332
+ )
333
+ rollback_schema_props.warehouse_name = benchling_given_wh_name
334
+ if template_based_naming_strategies:
335
+ new_schema_props.naming_strategies = template_based_naming_strategies
336
+ rollback_schema_props.naming_strategies = (
337
+ model.__schema_properties__.naming_strategies
338
+ )
339
+ if new_schema_props.model_dump(exclude_unset=True) != {}:
340
+ ops.append(
341
+ CompareOperation(
342
+ op=UpdateEntitySchema(
343
+ benchling_given_wh_name,
344
+ new_schema_props,
345
+ ),
346
+ reverse_op=UpdateEntitySchema(
347
+ model.__schema_properties__.warehouse_name,
348
+ rollback_schema_props,
349
+ ),
350
+ )
351
+ )
333
352
 
334
353
  model_operations[model.__schema_properties__.warehouse_name] = ops
335
354
  running_benchling_schema_names = [
@@ -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(
@@ -105,7 +105,7 @@ def generate_all_entity_schema_files(
105
105
  if not has_date:
106
106
  import_strings.append("from datetime import datetime")
107
107
  if (
108
- col.type == BenchlingFieldType.ENTITY_LINK
108
+ col.type in BenchlingFieldType.get_entity_link_types()
109
109
  and col.entity_link is not None
110
110
  ):
111
111
  if not col.is_multi:
@@ -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] = 100
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] = 90
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] = 150
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] = 110
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,10 +393,18 @@ 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):
373
- order: ClassVar[int] = 140
407
+ order: ClassVar[int] = 150
374
408
 
375
409
  def __init__(
376
410
  self, wh_schema_name: str, wh_field_name: str, index: int | None = None
@@ -421,7 +455,7 @@ class ArchiveEntitySchemaField(BaseOperation):
421
455
 
422
456
 
423
457
  class UnarchiveEntitySchemaField(BaseOperation):
424
- order: ClassVar[int] = 120
458
+ order: ClassVar[int] = 130
425
459
 
426
460
  def __init__(
427
461
  self, wh_schema_name: str, wh_field_name: str, index: int | None = None
@@ -468,7 +502,7 @@ class UnarchiveEntitySchemaField(BaseOperation):
468
502
 
469
503
 
470
504
  class UpdateEntitySchemaField(BaseOperation):
471
- order: ClassVar[int] = 130
505
+ order: ClassVar[int] = 140
472
506
 
473
507
  def __init__(
474
508
  self,
@@ -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, constraint_fields: list[TagSchemaFieldModel], bases: bool
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=bases,
116
+ uniqueResidues=uniqueResidues,
102
117
  uniqueCanonicalSmilers=False,
103
- areUniqueResiduesCaseSensitive=False,
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(
@@ -164,7 +181,7 @@ class CreateTagSchemaFieldModel(BaseModel):
164
181
  )
165
182
 
166
183
  tagSchema = None
167
- if new_props.type == BenchlingFieldType.ENTITY_LINK:
184
+ if new_props.type in BenchlingFieldType.get_entity_link_types():
168
185
  if new_props.entity_link is not None:
169
186
  if benchling_service is None:
170
187
  raise ValueError(
@@ -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
- 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
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
- 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")
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, has_bases
517
+ constraint_fields, sequence_constraint
471
518
  )
472
519
  else:
473
520
  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
  ]
@@ -32,3 +32,11 @@ class BenchlingFieldType(StrEnum):
32
32
  cls.DATE,
33
33
  cls.DATETIME,
34
34
  ]
35
+
36
+ @classmethod
37
+ def get_entity_link_types(cls) -> list[str]:
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]
@@ -11,6 +11,7 @@ class BenchlingNamingStrategy(StrEnum):
11
11
  )
12
12
  RENAME_WITH_TEMPLATE_WITH_ALIAS = "SET_FROM_NAME_PARTS" # Generate new registry IDs, rename according to name template, and keep old name as alias
13
13
  REPLACE_NAMES_WITH_TEMPLATE = "REPLACE_NAMES_FROM_PARTS" # Generate new registry IDs, and replace name according to name template
14
+ KEEP_NAMES = "KEEP_NAMES" # Keeps the original name
14
15
 
15
16
  @classmethod
16
17
  def is_template_based(cls, strategy: BenchlingNamingStrategy) -> bool:
@@ -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
@@ -22,6 +22,8 @@ def convert_benchling_type_to_python_type(benchling_type: BenchlingFieldType) ->
22
22
  BenchlingFieldType.BLOB_LINK: dict[str, Any],
23
23
  BenchlingFieldType.CUSTOM_ENTITY_LINK: str,
24
24
  BenchlingFieldType.DNA_SEQUENCE_LINK: str,
25
+ BenchlingFieldType.AA_SEQUENCE_LINK: str,
26
+ BenchlingFieldType.TRANSLATION_LINK: str,
25
27
  BenchlingFieldType.DROPDOWN: str,
26
28
  BenchlingFieldType.ENTITY_LINK: str,
27
29
  BenchlingFieldType.ENTRY_LINK: str,
@@ -48,6 +50,8 @@ def convert_benchling_type_to_sql_alchemy_type(
48
50
  BenchlingFieldType.BLOB_LINK: JSON,
49
51
  BenchlingFieldType.CUSTOM_ENTITY_LINK: String,
50
52
  BenchlingFieldType.DNA_SEQUENCE_LINK: String,
53
+ BenchlingFieldType.AA_SEQUENCE_LINK: String,
54
+ BenchlingFieldType.TRANSLATION_LINK: String,
51
55
  BenchlingFieldType.DROPDOWN: String,
52
56
  BenchlingFieldType.ENTITY_LINK: String,
53
57
  BenchlingFieldType.ENTRY_LINK: String,
@@ -55,7 +59,6 @@ def convert_benchling_type_to_sql_alchemy_type(
55
59
  BenchlingFieldType.STORAGE_LINK: String,
56
60
  BenchlingFieldType.PART_LINK: String,
57
61
  BenchlingFieldType.MIXTURE_LINK: String,
58
- BenchlingFieldType.AA_SEQUENCE_LINK: String,
59
62
  BenchlingFieldType.TEXT: String,
60
63
  }
61
64
  if benchling_type in benchling_to_sql_alchemy_type_map:
@@ -88,13 +91,10 @@ def convert_field_type_to_api_field_type(
88
91
  BenchlingAPIFieldType.FILE_LINK,
89
92
  BenchlingFolderItemType.SEQUENCE,
90
93
  ),
91
- BenchlingFieldType.PART_LINK: (
92
- BenchlingAPIFieldType.PART_LINK,
93
- BenchlingFolderItemType.SEQUENCE,
94
- ),
94
+ BenchlingFieldType.PART_LINK: (BenchlingAPIFieldType.PART_LINK, None),
95
95
  BenchlingFieldType.TRANSLATION_LINK: (
96
96
  BenchlingAPIFieldType.TRANSLATION_LINK,
97
- BenchlingFolderItemType.SEQUENCE,
97
+ None,
98
98
  ),
99
99
  BenchlingFieldType.ENTITY_LINK: (BenchlingAPIFieldType.FILE_LINK, None),
100
100
  BenchlingFieldType.DECIMAL: (BenchlingAPIFieldType.FLOAT, None),
@@ -135,13 +135,10 @@ def convert_api_field_type_to_field_type(
135
135
  BenchlingAPIFieldType.FILE_LINK,
136
136
  BenchlingFolderItemType.PROTEIN,
137
137
  ): BenchlingFieldType.AA_SEQUENCE_LINK,
138
- (
139
- BenchlingAPIFieldType.PART_LINK,
140
- BenchlingFolderItemType.SEQUENCE,
141
- ): BenchlingFieldType.PART_LINK,
138
+ (BenchlingAPIFieldType.PART_LINK, None): BenchlingFieldType.PART_LINK,
142
139
  (
143
140
  BenchlingAPIFieldType.TRANSLATION_LINK,
144
- BenchlingFolderItemType.SEQUENCE,
141
+ None,
145
142
  ): BenchlingFieldType.TRANSLATION_LINK,
146
143
  (BenchlingAPIFieldType.FILE_LINK, None): BenchlingFieldType.ENTITY_LINK,
147
144
  (BenchlingAPIFieldType.FLOAT, None): BenchlingFieldType.DECIMAL,
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,19 +81,29 @@ 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.")
76
- if entity_link and type != BenchlingFieldType.ENTITY_LINK:
84
+ if unit_name and type not in BenchlingFieldType.get_number_field_types():
77
85
  raise ValueError(
78
- "Entity link can only be set if the field type is ENTITY_LINK."
86
+ f"Unit can only be set if the field type is one of {BenchlingFieldType.get_number_field_types()}."
79
87
  )
80
- if parent_link and type != BenchlingFieldType.ENTITY_LINK:
88
+ if decimal_places and type not in BenchlingFieldType.DECIMAL:
81
89
  raise ValueError(
82
- "Parent link can only be set if the field type is ENTITY_LINK."
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.")
94
+ if entity_link and type not in BenchlingFieldType.get_entity_link_types():
95
+ raise ValueError(
96
+ f"Entity link can only be set if the field type is one of {BenchlingFieldType.get_entity_link_types()}."
97
+ )
98
+ if parent_link and type not in BenchlingFieldType.get_entity_link_types():
99
+ raise ValueError(
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.")
86
104
  self.sqlalchemy_type = sqlalchemy_type
87
105
  foreign_key = None
88
- if type == BenchlingFieldType.ENTITY_LINK and entity_link:
106
+ if type in BenchlingFieldType.get_entity_link_types() and entity_link:
89
107
  foreign_key = ForeignKey(f"{entity_link}$raw.id")
90
108
  if _warehouse_name:
91
109
  kwargs["name"] = _warehouse_name
@@ -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"
Binary file
@@ -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,4 +1,3 @@
1
- import inspect
2
1
  from datetime import datetime
3
2
  from functools import partial, wraps
4
3
  from typing import TYPE_CHECKING, Any, Callable
@@ -102,7 +101,7 @@ class BenchlingValidatorReport(BaseModel):
102
101
 
103
102
 
104
103
  def liminal_validator(
105
- func: Callable[[type["BenchlingBaseModel"]], BenchlingValidatorReport | None]
104
+ func: Callable[["BenchlingBaseModel"], BenchlingValidatorReport | None]
106
105
  | None = None,
107
106
  *,
108
107
  validator_level: ValidationSeverity = ValidationSeverity.LOW,
@@ -125,15 +124,9 @@ def liminal_validator(
125
124
  validator_level=validator_level,
126
125
  validator_name=validator_name,
127
126
  )
128
- elif not isinstance(
129
- func, Callable[[["BenchlingBaseModel"]], BenchlingValidatorReport | None]
130
- ):
131
- raise ValueError(
132
- "Parameters passed to liminal_validator must be keyword arguments, not positional arguments."
133
- )
134
127
 
135
128
  @wraps(func)
136
- def wrapper(self: type["BenchlingBaseModel"]) -> BenchlingValidatorReport:
129
+ def wrapper(self: "BenchlingBaseModel") -> BenchlingValidatorReport:
137
130
  """Wrapper that runs the validator function and returns a BenchlingValidatorReport."""
138
131
  try:
139
132
  ret_val = func(self)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: liminal-orm
3
- Version: 2.0.3a1
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,12 +1,13 @@
1
+ liminal/.DS_Store,sha256=s_ehSI1aIzOjVRnFlcSzhtWS3irmEDSGHyS6l0QRcus,8196
1
2
  liminal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
3
  liminal/base/base_dropdown.py,sha256=Unk4l_5Y8rj_eSWYqzFi2BAFSQToQDWW2qdXwiCHTg8,2523
3
- liminal/base/base_operation.py,sha256=RjGRaCTNQ5oVI4PAQ5D3svymw_HAcGvzBXWWrMRo29k,3128
4
+ liminal/base/base_operation.py,sha256=opQfFZeC49YAFkg5ahE6CFpeSUNPh1ootWZxXyEXfFI,3128
4
5
  liminal/base/base_validation_filters.py,sha256=kHG3G5gXkuNHQosMTrxRc57OTmczcaoSx0DmkrScIr4,1043
5
6
  liminal/base/compare_operation.py,sha256=hkpv4ewHhxy4dlTPKgJuzBjsAqO6Km7OrrKB44pRA_o,352
6
7
  liminal/base/name_template_parts.py,sha256=dJeyrhhhZHDwKXe_p7AtgDlbZlzsnYQ8FoM8FXVF7q0,271
7
- liminal/base/properties/base_field_properties.py,sha256=NvuQjYZ5U_2363P2c8FXmmLfOtdzUqTuZTjDDfXnO-E,4922
8
+ liminal/base/properties/base_field_properties.py,sha256=ze8CLf0jPPYwklFYVKXkRvXiBBA9ilG5nK1_oP0rS04,4970
8
9
  liminal/base/properties/base_name_template.py,sha256=AOtaW4QEDRC-mjZOZk6jgc_mopUMsHS2Fj6VVsO07WY,3150
9
- liminal/base/properties/base_schema_properties.py,sha256=pVeIVHea-3Wt0fde9CsOvluwVmYMbsMhGeSncRL2Ats,5351
10
+ liminal/base/properties/base_schema_properties.py,sha256=11QaxtUoHZx25QFv7HP8gQBpknrFthVeyWuMnI_lV5g,5891
10
11
  liminal/base/str_enum.py,sha256=jF3d-Lo8zsHUe6GsctX2L-TSj92Y3qCYDrTD-saeJoc,210
11
12
  liminal/cli/cli.py,sha256=JxWHLO9KMeMaOnOYwzdH0w71l0477ScFOkWNtTlc97Y,9045
12
13
  liminal/cli/controller.py,sha256=QNj3QO9TMb9hfc6U-VhLuFa0_aohOHZUmvY4XkATPhw,10118
@@ -21,48 +22,51 @@ liminal/dropdowns/generate_files.py,sha256=IqnBs-IyLsIZE0NUkdB99zd5EAF-1f9CPBebl
21
22
  liminal/dropdowns/operations.py,sha256=-TRIsxqnUtrIUjhrt5k_PdiBCDUXsXDzsOUmznJE-6Q,13516
22
23
  liminal/dropdowns/utils.py,sha256=1-H7bTszCUeqeRBpiYXjRjreDzhn1Fd1MFwIsrEI-o4,4109
23
24
  liminal/entity_schemas/api.py,sha256=Emn_Y95cAG9Wis6tpchw6QBVKQh4If86LOdgKk0Ndjw,3575
24
- liminal/entity_schemas/compare.py,sha256=CIYglq1F-g9jGc1eRRD4LnNErrH2n__pPIJc4EB1hkM,16570
25
- liminal/entity_schemas/entity_schema_models.py,sha256=YDpz1XkNc9e51zah8Z6qCk30gAuXP6xLEYv1Lb3ECpA,6838
26
- liminal/entity_schemas/generate_files.py,sha256=oYx0t76oRa36gWwVl1W5jHryh-POIyZPDMbGNjT5bfY,8877
27
- liminal/entity_schemas/operations.py,sha256=rs9EXmHDgnUad2SSfZ3tnDPFA6L-2wvllAGqBwBurMs,24020
28
- liminal/entity_schemas/tag_schema_models.py,sha256=h6Zf_zXYG_gTh2eQh_lEGZKUAmvzdnnArlmAhIeX1bM,22016
29
- liminal/entity_schemas/utils.py,sha256=iZ1_M2r8zKOCFj9QSMdrv-_4XznDn_znAOfoP4Mh1jA,4943
30
- liminal/enums/__init__.py,sha256=Ue_3QtElW-JMSWtu4vGsAOFQbYnzHHZUdkWpdkzkKA4,453
25
+ liminal/entity_schemas/compare.py,sha256=t6tl67GWaMoNNcPxyLpCuNAlN3OWNqURTo3EUEMtETE,17549
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
31
32
  liminal/enums/benchling_api_field_type.py,sha256=0QamSWEMnxZtedZXlh6zNhSRogS9ZqvWskdHHN19xJo,633
32
- liminal/enums/benchling_entity_type.py,sha256=BS6U8qnRM3I3xTTqp9BbInV7yjPh9gC3ULvN6-zLaCM,624
33
- liminal/enums/benchling_field_type.py,sha256=uinDm5Mn_yGK1jlmlRH3NlAlXUzA1guNk8wF6lbUKD4,947
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
34
35
  liminal/enums/benchling_folder_item_type.py,sha256=Jb-YxCvB8O86_qTsfwtLQOkKGjTWGKHFwIKf24eemYk,248
35
- liminal/enums/benchling_naming_strategy.py,sha256=wG3AfnPOui5Qfc0Fihszm5uKWjuc7gdpI8jptNB5A-w,1201
36
+ liminal/enums/benchling_naming_strategy.py,sha256=jmaR-Vfj3MWhna8tANBNjAgYUyoQ5wMbz1AIy2bv6Zk,1258
36
37
  liminal/enums/benchling_sequence_type.py,sha256=TBI4C5c1XKE4ZXqsz1ApDUzy2wR-04u-M3VO_zLikjM,202
37
38
  liminal/enums/name_template_part_type.py,sha256=Kv0phZIO_dPN3tLHM0lT2tjUd3zBGqpJQGahEpGjNcU,365
39
+ liminal/enums/sequence_constraint.py,sha256=CT3msm8qzJpcivfbQZ3NOWNRsedH4mSlfhzvQBLrHWA,407
38
40
  liminal/external/__init__.py,sha256=nMpyzpBXpYhTvN3R3HQEiYb24_U2AYjz_20seAUUK9s,1264
39
- liminal/mappers.py,sha256=O9gc95b7JvfaR8xVrn0X1d0Tcs6Iwh-yhBHXhWSX8i0,9616
41
+ liminal/mappers.py,sha256=TgPMQsLrESAI6D7KBl0UoBBpnxYgcgGOT7a2faWsuhY,9587
40
42
  liminal/migrate/components.py,sha256=2HuFp5KDNhofROMRI-BioUoA4CCjhQ_v_F0QmGJzUBU,3480
41
43
  liminal/migrate/revision.py,sha256=KppU0u-d0JsfPsXsmncxy9Q_XBJyf-o4e16wNZAJODM,7774
42
44
  liminal/migrate/revisions_timeline.py,sha256=G9VwxPrLhLqKOrIXyxrXyHpujc-72m7omsZjI5-0D0M,14520
43
45
  liminal/migrate/utils.py,sha256=HdSr3N2WN_1S-PLRGVWSMYl-4gIcP-Ph2wPycGi2cGg,3404
44
46
  liminal/orm/base.py,sha256=fFSpiNRYgK5UG7lbXdQGV8KgO8pwjMqt0pycM3rWJ2o,615
45
- liminal/orm/base_model.py,sha256=Nf4gSEwvORRdnY5ODW79ddJWxnshLLvrPo8xcimHH6c,13891
47
+ liminal/orm/base_model.py,sha256=FFkrB-lMAAgp77BUnQDu39IxZxmhHu52CtCLeBZVNGA,15527
46
48
  liminal/orm/base_tables/registry_entity.py,sha256=4ET1cepTGjZ3AMFI5q-iMYxMObzXwuUDBD0jNNqCipE,2126
47
49
  liminal/orm/base_tables/schema.py,sha256=7_btCVSUJxjVdGcKVRKL8sKcNw7-_gazTpfEh1jru3o,921
48
50
  liminal/orm/base_tables/user.py,sha256=elRAHj7HgO3iVLK_pNCIwf_9Rl_9k6vkBgaYazoJSQc,818
49
- liminal/orm/column.py,sha256=e4JWn97s_4EVJ1LOO5l6iucHQUd39Vl0stqMEj0uet8,5114
51
+ liminal/orm/column.py,sha256=aK-MrKabOK5tf3UFPpRACq83YVVrjXITZF_rcOU-wPQ,6207
50
52
  liminal/orm/mixins.py,sha256=yEeUDF1qEBLP523q8bZra4KtNVK0gwZN9mXJSNe3GEE,4802
51
53
  liminal/orm/name_template.py,sha256=ftXZOiRR6gGGvGaZkFVDXKOboIHFWauhQENRguBGWMI,1739
52
54
  liminal/orm/name_template_parts.py,sha256=KCGXAcCuOqCjlgYn-mw1K7fwDI92D20l-FnlpEVrbM8,2771
53
55
  liminal/orm/relationship.py,sha256=Zl4bMHbtDSPx1psGHYnojGGJpA8B8hwcPJdgjB1lmW0,2490
54
- liminal/orm/schema_properties.py,sha256=yv6MOsE_16OWJnGDh2K8wI_054PJwafYmHgY_Awr3XA,3420
56
+ liminal/orm/schema_properties.py,sha256=vqqjnxbh7AYh9ZvSmhCsl69BqSBPpQutNKImb-TBCGg,4167
55
57
  liminal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
+ liminal/tests/.DS_Store,sha256=0sTLf7flLKL2_3KGceYriAB8_gXTcYwn0c2RVSYmLZk,6148
56
59
  liminal/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
60
  liminal/tests/conftest.py,sha256=B463eOfe1uCHDJsUNvG-6tY8Qx8FJMByGDOtuyM87lA,17669
58
61
  liminal/tests/from benchling_sdk.py,sha256=CjRUHFB3iaa4rUPLGOqDiBq5EPKldm-Fd8aQQr92zF4,147
59
62
  liminal/tests/test_dropdown_compare.py,sha256=yHB0ovQlBLRu8-qYkqIPd8VtYEOmOft_93FQM86g_z8,8198
60
63
  liminal/tests/test_entity_schema_compare.py,sha256=-26Bu5eYIuHRswB5kYjGDo5Wed5LUWjm1e6IRI1Q-lE,18952
64
+ liminal/unit_dictionary/utils.py,sha256=o3K06Yyt33iIUSMHPT8f1vSuUSgWjZLf51p78lx4SZs,1817
61
65
  liminal/utils.py,sha256=radRtRsZmCiNblMvxOX1DH0rcO5TR09kFlp6OONIPBU,2951
62
- liminal/validation/__init__.py,sha256=1EOF2cQ_MQu_K7eQrBgsaP3GVp5AalKRD9NNE0px-0Y,5558
66
+ liminal/validation/__init__.py,sha256=TVaHrSF3GnSd4mbZrPn8TBHscGWkAPKAUUPq7-symC8,5275
63
67
  liminal/validation/validation_severity.py,sha256=ib03PTZCQHcbBDc01v4gJF53YtA-ANY6QSFnhTV-FbU,259
64
- liminal_orm-2.0.3a1.dist-info/LICENSE.md,sha256=oVA877F_D1AV44dpjsv4f-4k690uNGApX1EtzOo3T8U,11353
65
- liminal_orm-2.0.3a1.dist-info/METADATA,sha256=s0aHimEXa07bwDqvmNH8fdLQ0pmLcL3ZNg0kpk1m3r8,11034
66
- liminal_orm-2.0.3a1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
67
- liminal_orm-2.0.3a1.dist-info/entry_points.txt,sha256=atIrU63rrzH81dWC2sjUbFLlc5FWMmYRdMxXEWexIZA,47
68
- liminal_orm-2.0.3a1.dist-info/RECORD,,
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,,