liminal-orm 1.1.4__py3-none-any.whl → 2.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.
Files changed (34) hide show
  1. liminal/base/base_operation.py +4 -3
  2. liminal/base/base_validation_filters.py +15 -0
  3. liminal/base/name_template_parts.py +9 -0
  4. liminal/base/properties/base_field_properties.py +2 -2
  5. liminal/base/properties/base_name_template.py +83 -0
  6. liminal/base/properties/base_schema_properties.py +13 -1
  7. liminal/dropdowns/compare.py +8 -0
  8. liminal/dropdowns/operations.py +1 -1
  9. liminal/entity_schemas/api.py +18 -0
  10. liminal/entity_schemas/compare.py +62 -8
  11. liminal/entity_schemas/entity_schema_models.py +43 -0
  12. liminal/entity_schemas/generate_files.py +13 -11
  13. liminal/entity_schemas/operations.py +43 -18
  14. liminal/entity_schemas/tag_schema_models.py +146 -3
  15. liminal/entity_schemas/utils.py +15 -2
  16. liminal/enums/__init__.py +0 -1
  17. liminal/enums/benchling_entity_type.py +8 -0
  18. liminal/enums/name_template_part_type.py +12 -0
  19. liminal/external/__init__.py +11 -1
  20. liminal/migrate/revisions_timeline.py +2 -1
  21. liminal/orm/base_model.py +90 -29
  22. liminal/orm/name_template.py +39 -0
  23. liminal/orm/name_template_parts.py +96 -0
  24. liminal/orm/schema_properties.py +27 -1
  25. liminal/tests/conftest.py +18 -9
  26. liminal/tests/test_entity_schema_compare.py +61 -12
  27. liminal/utils.py +9 -0
  28. liminal/validation/__init__.py +84 -108
  29. liminal/{enums/benchling_report_level.py → validation/validation_severity.py} +2 -2
  30. {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.1.dist-info}/METADATA +17 -20
  31. {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.1.dist-info}/RECORD +34 -29
  32. {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.1.dist-info}/LICENSE.md +0 -0
  33. {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.1.dist-info}/WHEEL +0 -0
  34. {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.1.dist-info}/entry_points.txt +0 -0
@@ -19,9 +19,10 @@ Order of operations based on order class var:
19
19
  12. UnarchiveField
20
20
  13. UpdateField
21
21
  14. ArchiveField
22
- 15. ReorderFields
23
- 16. ArchiveSchema
24
- 17. ArchiveDropdown
22
+ 15. UpdateEntitySchemaNameTemplate
23
+ 16. ReorderFields
24
+ 17. ArchiveSchema
25
+ 18. ArchiveDropdown
25
26
  """
26
27
 
27
28
 
@@ -7,6 +7,21 @@ class BaseValidatorFilters(BaseModel):
7
7
  """
8
8
  This class is used to pass base filters to benchling warehouse database queries.
9
9
  These columns are found on all tables in the benchling warehouse database.
10
+
11
+ Parameters
12
+ ----------
13
+ created_date_start: date | None
14
+ Start date for created date filter.
15
+ created_date_end: date | None
16
+ End date for created date filter.
17
+ updated_date_start: date | None
18
+ Start date for updated date filter.
19
+ updated_date_end: date | None
20
+ End date for updated date filter.
21
+ entity_ids: list[str] | None
22
+ List of entity IDs to filter by.
23
+ creator_full_names: list[str] | None
24
+ List of creator full names to filter by.
10
25
  """
11
26
 
12
27
  created_date_start: date | None = None
@@ -0,0 +1,9 @@
1
+ import warnings
2
+
3
+ from liminal.orm.name_template_parts import * # noqa: F403
4
+
5
+ warnings.warn(
6
+ "Importing from 'liminal.base.name_template_parts' is deprecated. Please import from 'liminal.orm.name_template_parts' instead.",
7
+ DeprecationWarning,
8
+ stacklevel=2,
9
+ )
@@ -65,7 +65,7 @@ class BaseFieldProperties(BaseModel):
65
65
  self.warehouse_name = wh_name
66
66
  return self
67
67
 
68
- def validate_column(self, wh_name: str) -> bool:
68
+ def validate_column_definition(self, wh_name: str) -> bool:
69
69
  """If the Field Properties are meant to represent a column in Benchling,
70
70
  this will validate the properties and ensure that the entity_link and dropdowns are valid names that exist in our code.
71
71
  """
@@ -122,4 +122,4 @@ class BaseFieldProperties(BaseModel):
122
122
 
123
123
  def __repr__(self) -> str:
124
124
  """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_defaults=True).items()])})"
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()])})"
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ from liminal.orm.name_template_parts import NameTemplateParts
8
+
9
+
10
+ class BaseNameTemplate(BaseModel):
11
+ """
12
+ This class is the generic class for defining the name template.
13
+ It is used to create a diff between the old and new name template.
14
+
15
+ Parameters
16
+ ----------
17
+ parts : list[NameTemplatePart] | None
18
+ The list of name template parts that make up the name template (order matters).
19
+ order_name_parts_by_sequence : bool | None
20
+ Whether to order the name parts by sequence. This can only be set to True for sequence enity types. If one or many part link fields are included in the name template,
21
+ list parts in the order they appear on the sequence map, sorted by start position and then end position.
22
+ """
23
+
24
+ parts: list[NameTemplateParts] | None = None
25
+ order_name_parts_by_sequence: bool | None = None
26
+
27
+ model_config = ConfigDict(arbitrary_types_allowed=True)
28
+
29
+ def merge(self, new_props: BaseNameTemplate) -> dict[str, Any]:
30
+ """Creates a diff between the current name template and the new name template.
31
+ Sets value to None if the values are equal, otherwise sets the value to the new value.
32
+
33
+ Parameters
34
+ ----------
35
+ new_props : BaseNameTemplate
36
+ The new name template.
37
+
38
+ Returns
39
+ -------
40
+ dict[str, Any]
41
+ A dictionary of the differences between the old and new name template.
42
+ """
43
+ diff = {}
44
+ for field_name in self.model_fields:
45
+ new_val = getattr(new_props, field_name)
46
+ if getattr(self, field_name) != new_val:
47
+ diff[field_name] = new_val
48
+ return diff
49
+
50
+ def __eq__(self, other: object) -> bool:
51
+ if not isinstance(other, BaseNameTemplate):
52
+ return False
53
+ return self.model_dump() == other.model_dump()
54
+
55
+ def __str__(self) -> str:
56
+ parts_str = (
57
+ f"parts=[{', '.join(repr(part) for part in self.parts)}]"
58
+ if self.parts is not None
59
+ else None
60
+ )
61
+ order_name_parts_by_sequence_str = (
62
+ f"order_name_parts_by_sequence={self.order_name_parts_by_sequence}"
63
+ if self.order_name_parts_by_sequence is not None
64
+ else None
65
+ )
66
+ return ", ".join(filter(None, [parts_str, order_name_parts_by_sequence_str]))
67
+
68
+ def __repr__(self) -> str:
69
+ """Generates a string representation of the class so that it can be executed."""
70
+ model_dump = self.model_dump(exclude_defaults=True, exclude_unset=True)
71
+ props = []
72
+ if "parts" in model_dump:
73
+ parts_repr = (
74
+ f"[{', '.join(repr(part) for part in self.parts)}]"
75
+ if self.parts
76
+ else "[]"
77
+ )
78
+ props.append(f"parts={parts_repr}")
79
+ if "order_name_parts_by_sequence" in model_dump:
80
+ props.append(
81
+ f"order_name_parts_by_sequence={self.order_name_parts_by_sequence}"
82
+ )
83
+ return f"{self.__class__.__name__}({', '.join(props)})"
@@ -57,8 +57,17 @@ class BaseSchemaProperties(BaseModel):
57
57
  The prefix to use for the schema.
58
58
  entity_type : BenchlingEntityType | None
59
59
  The entity type of the schema.
60
+ naming_strategies : set[BenchlingNamingStrategy] | None
61
+ The naming strategies of the schema.
60
62
  mixture_schema_config : MixtureSchemaConfig | None
61
63
  The mixture schema config of the schema.
64
+ use_registry_id_as_label : bool | None = None
65
+ Flag for configuring the chip label for entities. Determines if the chip will use the Registry ID as the main label for items.
66
+ include_registry_id_in_chips : bool | None = None
67
+ Flag for configuring the chip label for entities. Determines if the chip will include the Registry ID in the chip label.
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.
62
71
  _archived : bool | None
63
72
  Whether the schema is archived in Benchling.
64
73
  """
@@ -69,6 +78,9 @@ class BaseSchemaProperties(BaseModel):
69
78
  entity_type: BenchlingEntityType | None = None
70
79
  naming_strategies: set[BenchlingNamingStrategy] | None = None
71
80
  mixture_schema_config: MixtureSchemaConfig | None = None
81
+ use_registry_id_as_label: bool | None = None
82
+ include_registry_id_in_chips: bool | None = None
83
+ constraint_fields: set[str] | None = None
72
84
  _archived: bool | None = None
73
85
 
74
86
  def __init__(self, **data: Any):
@@ -109,4 +121,4 @@ class BaseSchemaProperties(BaseModel):
109
121
 
110
122
  def __repr__(self) -> str:
111
123
  """Generates a string representation of the class so that it can be executed."""
112
- return f"{self.__class__.__name__}({', '.join([f'{k}={v.__repr__()}' for k, v in self.model_dump(exclude_defaults=True).items()])})"
124
+ return f"{self.__class__.__name__}({', '.join([f'{k}={v.__repr__()}' for k, v in self.model_dump(exclude_unset=True, exclude_defaults=True).items()])})"
@@ -1,3 +1,5 @@
1
+ import logging
2
+
1
3
  from benchling_sdk.models import Dropdown
2
4
 
3
5
  from liminal.base.base_dropdown import BaseDropdown
@@ -13,6 +15,8 @@ from liminal.dropdowns.operations import (
13
15
  )
14
16
  from liminal.dropdowns.utils import get_benchling_dropdowns_dict
15
17
 
18
+ LOGGER = logging.getLogger(__name__)
19
+
16
20
 
17
21
  def compare_dropdowns(
18
22
  benchling_service: BenchlingService, dropdown_names: set[str] | None = None
@@ -23,6 +27,10 @@ def compare_dropdowns(
23
27
  )
24
28
  processed_benchling_names = set()
25
29
  model_dropdowns = BaseDropdown.get_all_subclasses(dropdown_names)
30
+ if len(model_dropdowns) == 0 and len(benchling_dropdowns.keys()) > 0:
31
+ LOGGER.warning(
32
+ "WARNING: No dropdown classes found that inherit from BaseDropdown. Ensure that the dropdown classes are defined and imported correctly."
33
+ )
26
34
  if dropdown_names:
27
35
  benchling_dropdowns = {
28
36
  name: benchling_dropdowns[name]
@@ -76,7 +76,7 @@ class CreateDropdown(BaseOperation):
76
76
 
77
77
 
78
78
  class ArchiveDropdown(BaseOperation):
79
- order: ClassVar[int] = 190
79
+ order: ClassVar[int] = 180
80
80
 
81
81
  def __init__(self, dropdown_name: str) -> None:
82
82
  self.dropdown_name = dropdown_name
@@ -75,3 +75,21 @@ def update_tag_schema(
75
75
  return await_queued_response(
76
76
  queued_response.json()["status_url"], benchling_service
77
77
  )
78
+
79
+
80
+ def set_tag_schema_name_template(
81
+ benchling_service: BenchlingService, entity_schema_id: str, payload: dict[str, Any]
82
+ ) -> dict[str, Any]:
83
+ """
84
+ Update the tag schema name template. Must be in a separate endpoint compared to update_tag_schema.
85
+ """
86
+ with requests.Session() as session:
87
+ response = session.post(
88
+ f"https://{benchling_service.benchling_tenant}.benchling.com/1/api/tag-schemas/{entity_schema_id}/actions/set-name-template",
89
+ data=json.dumps(payload),
90
+ headers=benchling_service.custom_post_headers,
91
+ cookies=benchling_service.custom_post_cookies,
92
+ )
93
+ if not response.ok:
94
+ raise Exception("Failed to set tag schema name template:", response.content)
95
+ return response.json()
@@ -1,5 +1,8 @@
1
+ import logging
2
+
1
3
  from liminal.base.compare_operation import CompareOperation
2
4
  from liminal.base.properties.base_field_properties import BaseFieldProperties
5
+ from liminal.base.properties.base_name_template import BaseNameTemplate
3
6
  from liminal.base.properties.base_schema_properties import BaseSchemaProperties
4
7
  from liminal.connection import BenchlingService
5
8
  from liminal.entity_schemas.operations import (
@@ -12,12 +15,15 @@ from liminal.entity_schemas.operations import (
12
15
  UnarchiveEntitySchemaField,
13
16
  UpdateEntitySchema,
14
17
  UpdateEntitySchemaField,
18
+ UpdateEntitySchemaNameTemplate,
15
19
  )
16
20
  from liminal.entity_schemas.utils import get_converted_tag_schemas
17
21
  from liminal.orm.base_model import BaseModel
18
22
  from liminal.orm.column import Column
19
23
  from liminal.utils import to_snake_case
20
24
 
25
+ LOGGER = logging.getLogger(__name__)
26
+
21
27
 
22
28
  def compare_entity_schemas(
23
29
  benchling_service: BenchlingService, schema_names: set[str] | None = None
@@ -46,13 +52,18 @@ def compare_entity_schemas(
46
52
  for m in BaseModel.get_all_subclasses(schema_names)
47
53
  if not m.__schema_properties__._archived
48
54
  ]
55
+ if len(models) == 0 and len(benchling_schemas) > 0:
56
+ LOGGER.warning(
57
+ "WARNING: No model classes found that inherit from BaseModel. Ensure that the model classes are defined and imported correctly."
58
+ )
59
+
49
60
  archived_benchling_schema_wh_names = [
50
- s.warehouse_name for s, _ in benchling_schemas if s._archived is True
61
+ s.warehouse_name for s, _, _ in benchling_schemas if s._archived is True
51
62
  ]
52
63
  # Running list of schema names from benchling. As each model is checked, remove the schema name from this list.
53
64
  # This is used at the end to check if there are any schemas left (schemas that exist in benchling but not in code) and archive them if they are.
54
65
  running_benchling_schema_names = list(
55
- [s.warehouse_name for s, _ in benchling_schemas]
66
+ [s.warehouse_name for s, _, _ in benchling_schemas]
56
67
  )
57
68
  # Iterate through each benchling model defined in code.
58
69
  for model in models:
@@ -61,15 +72,17 @@ def compare_entity_schemas(
61
72
  exclude_base_columns=True
62
73
  )
63
74
  # Validate the entity_link and dropdown_link reference an entity_schema or dropdown that exists in code.
64
- model.validate_model()
75
+ model.validate_model_definition()
65
76
  # if the model table_name is found in the benchling schemas, check for changes...
66
77
  if (model_wh_name := model.__schema_properties__.warehouse_name) in [
67
- s.warehouse_name for s, _ in benchling_schemas
78
+ s.warehouse_name for s, _, _ in benchling_schemas
68
79
  ]:
69
- benchling_schema_props, benchling_schema_fields = next(
70
- (s, lof)
71
- for s, lof in benchling_schemas
72
- if s.warehouse_name == model_wh_name
80
+ benchling_schema_props, benchling_name_template, benchling_schema_fields = (
81
+ next(
82
+ (s, nt, lof)
83
+ for s, nt, lof in benchling_schemas
84
+ if s.warehouse_name == model_wh_name
85
+ )
73
86
  )
74
87
  archived_benchling_schema_fields = {
75
88
  k: v for k, v in benchling_schema_fields.items() if v._archived is True
@@ -237,6 +250,23 @@ def compare_entity_schemas(
237
250
  ),
238
251
  ),
239
252
  )
253
+ if benchling_name_template != model.__name_template__:
254
+ ops.append(
255
+ CompareOperation(
256
+ op=UpdateEntitySchemaNameTemplate(
257
+ model.__schema_properties__.warehouse_name,
258
+ BaseNameTemplate(
259
+ **benchling_name_template.merge(model.__name_template__)
260
+ ),
261
+ ),
262
+ reverse_op=UpdateEntitySchemaNameTemplate(
263
+ model.__schema_properties__.warehouse_name,
264
+ BaseNameTemplate(
265
+ **model.__name_template__.merge(benchling_name_template)
266
+ ),
267
+ ),
268
+ )
269
+ )
240
270
  # If the model is not found as the benchling schema, Create.
241
271
  # Benchling api does not allow for setting a custom warehouse_name,
242
272
  # so we need to run another UpdateEntitySchema to set the warehouse_name if it is different from the snakecase version of the model name.
@@ -276,6 +306,30 @@ def compare_entity_schemas(
276
306
  ),
277
307
  )
278
308
  )
309
+ benchling_given_name_template = BaseNameTemplate(
310
+ parts=[], order_name_parts_by_sequence=False
311
+ )
312
+ if benchling_name_template != model.__name_template__:
313
+ ops.append(
314
+ CompareOperation(
315
+ op=UpdateEntitySchemaNameTemplate(
316
+ model.__schema_properties__.warehouse_name,
317
+ BaseNameTemplate(
318
+ **benchling_given_name_template.merge(
319
+ model.__name_template__
320
+ )
321
+ ),
322
+ ),
323
+ reverse_op=UpdateEntitySchemaNameTemplate(
324
+ model.__schema_properties__.warehouse_name,
325
+ BaseNameTemplate(
326
+ **benchling_given_name_template.merge(
327
+ model.__name_template__
328
+ )
329
+ ),
330
+ ),
331
+ )
332
+ )
279
333
 
280
334
  model_operations[model.__schema_properties__.warehouse_name] = ops
281
335
  running_benchling_schema_names = [
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import Any
4
+
3
5
  from pydantic import BaseModel
4
6
 
5
7
  from liminal.base.properties.base_field_properties import BaseFieldProperties
@@ -18,6 +20,37 @@ class FieldLinkShortModel(BaseModel):
18
20
  folderItemType: str | None = None
19
21
 
20
22
 
23
+ class EntitySchemaConstraint(BaseModel):
24
+ """
25
+ A class to define a constraint on an entity schema.
26
+ """
27
+
28
+ areUniqueResiduesCaseSensitive: bool | None = None
29
+ fields: dict[str, Any] | None = None
30
+ hasUniqueCanonicalSmilers: bool | None = None
31
+ hasUniqueResidues: bool | None = None
32
+
33
+ @classmethod
34
+ def from_constraint_fields(
35
+ cls, constraint_fields: set[str]
36
+ ) -> EntitySchemaConstraint:
37
+ """
38
+ Generates a Constraint object from a set of constraint fields to create a constraint on a schema.
39
+ """
40
+ if constraint_fields is None:
41
+ return None
42
+ hasUniqueResidues = False
43
+ if "bases" in constraint_fields:
44
+ constraint_fields.discard("bases")
45
+ hasUniqueResidues = True
46
+ return cls(
47
+ fields=[{"name": f} for f in constraint_fields],
48
+ hasUniqueResidues=hasUniqueResidues,
49
+ hasUniqueCanonicalSmilers=False,
50
+ areUniqueResiduesCaseSensitive=False,
51
+ )
52
+
53
+
21
54
  class CreateEntitySchemaFieldModel(BaseModel):
22
55
  """A pydantic model to define a field for the create entity schema endpoint.
23
56
  This model is used as input for the benchling alpha create entity schema endpoint."""
@@ -98,7 +131,10 @@ class CreateEntitySchemaModel(BaseModel):
98
131
  registryId: str
99
132
  type: BenchlingEntityType
100
133
  mixtureSchemaConfig: MixtureSchemaConfig | None = None
134
+ includeRegistryIdInChips: bool | None = None
135
+ useOrganizationCollectionAliasForDisplayLabel: bool | None = None
101
136
  labelingStrategies: list[str] | None = None
137
+ constraint: EntitySchemaConstraint | None = None
102
138
 
103
139
  @classmethod
104
140
  def from_benchling_props(
@@ -129,7 +165,14 @@ class CreateEntitySchemaModel(BaseModel):
129
165
  registryId=benchling_service.registry_id,
130
166
  type=benchling_props.entity_type,
131
167
  mixtureSchemaConfig=benchling_props.mixture_schema_config,
168
+ includeRegistryIdInChips=benchling_props.include_registry_id_in_chips,
169
+ useOrganizationCollectionAliasForDisplayLabel=benchling_props.use_registry_id_as_label,
132
170
  labelingStrategies=[s.value for s in benchling_props.naming_strategies],
171
+ constraint=EntitySchemaConstraint.from_constraint_fields(
172
+ benchling_props.constraint_fields
173
+ )
174
+ if benchling_props.constraint_fields
175
+ else None,
133
176
  fields=[
134
177
  CreateEntitySchemaFieldModel.from_benchling_props(
135
178
  field_props, benchling_service
@@ -7,6 +7,7 @@ from liminal.dropdowns.utils import get_benchling_dropdowns_dict
7
7
  from liminal.entity_schemas.utils import get_converted_tag_schemas
8
8
  from liminal.enums import BenchlingEntityType, BenchlingFieldType
9
9
  from liminal.mappers import convert_benchling_type_to_python_type
10
+ from liminal.orm.name_template import NameTemplate
10
11
  from liminal.utils import pascalize, to_snake_case
11
12
 
12
13
 
@@ -62,13 +63,13 @@ def generate_all_entity_schema_files(
62
63
  for dropdown_name in benchling_dropdowns.keys()
63
64
  }
64
65
  wh_name_to_classname: dict[str, str] = {
65
- sp.warehouse_name: pascalize(sp.name) for sp, _ in models
66
+ sp.warehouse_name: pascalize(sp.name) for sp, _, _ in models
66
67
  }
67
68
 
68
- for schema_properties, columns in models:
69
+ for schema_properties, name_template, columns in models:
69
70
  classname = pascalize(schema_properties.name)
70
71
 
71
- for schema_properties, columns in models:
72
+ for schema_properties, name_template, columns in models:
72
73
  classname = pascalize(schema_properties.name)
73
74
  filename = to_snake_case(schema_properties.name) + ".py"
74
75
  columns = {key: columns[key] for key in columns}
@@ -78,7 +79,6 @@ def generate_all_entity_schema_files(
78
79
  "from liminal.orm.base_model import BaseModel",
79
80
  "from liminal.orm.schema_properties import SchemaProperties",
80
81
  "from liminal.enums import BenchlingEntityType, BenchlingFieldType, BenchlingNamingStrategy",
81
- "from liminal.validation import BenchlingValidator",
82
82
  f"from liminal.orm.mixins import {get_entity_mixin(schema_properties.entity_type)}",
83
83
  ]
84
84
  init_strings = [f"{tab}def __init__(", f"{tab}self,"]
@@ -132,6 +132,12 @@ def generate_all_entity_schema_files(
132
132
  init_strings.append(f"{tab}self.{col_name} = {col_name}")
133
133
  if len(dropdowns) > 0:
134
134
  import_strings.append(f"from ...dropdowns import {', '.join(dropdowns)}")
135
+ if name_template != NameTemplate():
136
+ import_strings.append("from liminal.orm.name_template import NameTemplate")
137
+ parts_imports = [
138
+ f"from liminal.orm.name_template_parts import {', '.join(set([part.__class__.__name__ for part in name_template.parts]))}"
139
+ ]
140
+ import_strings.extend(parts_imports)
135
141
  for col_name, col in columns.items():
136
142
  if col.dropdown_link:
137
143
  init_strings.append(
@@ -144,15 +150,12 @@ def generate_all_entity_schema_files(
144
150
  relationship_string = "\n".join(relationship_strings)
145
151
  import_string = "\n".join(list(set(import_strings)))
146
152
  init_string = f"\n{tab}".join(init_strings) if len(columns) > 0 else ""
147
- functions_string = """
148
- def get_validators(self) -> list[BenchlingValidator]:
149
- return []"""
150
-
151
- content = f"""{import_string}
153
+ full_content = f"""{import_string}
152
154
 
153
155
 
154
156
  class {classname}(BaseModel, {get_entity_mixin(schema_properties.entity_type)}):
155
157
  __schema_properties__ = {schema_properties.__repr__()}
158
+ {f"__name_template__ = {name_template.__repr__()}" if name_template != NameTemplate() else ""}
156
159
 
157
160
  {columns_string}
158
161
 
@@ -160,7 +163,6 @@ class {classname}(BaseModel, {get_entity_mixin(schema_properties.entity_type)}):
160
163
 
161
164
  {init_string}
162
165
 
163
- {functions_string}
164
166
  """
165
167
  write_directory_path = write_path / get_file_subdirectory(
166
168
  schema_properties.entity_type
@@ -173,7 +175,7 @@ class {classname}(BaseModel, {get_entity_mixin(schema_properties.entity_type)}):
173
175
  )
174
176
  write_directory_path.mkdir(exist_ok=True)
175
177
  with open(write_directory_path / filename, "w") as file:
176
- file.write(content)
178
+ file.write(full_content)
177
179
 
178
180
  for subdir, names in subdirectory_map.items():
179
181
  init_content = (
@@ -2,12 +2,14 @@ from typing import Any, ClassVar
2
2
 
3
3
  from liminal.base.base_operation import BaseOperation
4
4
  from liminal.base.properties.base_field_properties import BaseFieldProperties
5
+ from liminal.base.properties.base_name_template import BaseNameTemplate
5
6
  from liminal.base.properties.base_schema_properties import BaseSchemaProperties
6
7
  from liminal.connection import BenchlingService
7
8
  from liminal.dropdowns.utils import get_benchling_dropdown_id_name_map
8
9
  from liminal.entity_schemas.api import (
9
10
  archive_tag_schemas,
10
11
  create_entity_schema,
12
+ set_tag_schema_name_template,
11
13
  unarchive_tag_schemas,
12
14
  update_tag_schema,
13
15
  )
@@ -21,7 +23,7 @@ from liminal.entity_schemas.utils import (
21
23
  convert_tag_schema_field_to_field_properties,
22
24
  convert_tag_schema_to_internal_schema,
23
25
  )
24
- from liminal.enums.benchling_naming_strategy import BenchlingNamingStrategy
26
+ from liminal.enums import BenchlingNamingStrategy
25
27
  from liminal.orm.schema_properties import SchemaProperties
26
28
  from liminal.utils import to_snake_case
27
29
 
@@ -121,7 +123,7 @@ class CreateEntitySchema(BaseOperation):
121
123
  f"Entity schema {self._validated_schema_properties.warehouse_name} is already active in Benchling."
122
124
  )
123
125
  dropdowns_map = get_benchling_dropdown_id_name_map(benchling_service)
124
- benchling_schema_props, benchling_fields_props = (
126
+ benchling_schema_props, _, benchling_fields_props = (
125
127
  convert_tag_schema_to_internal_schema(schema, dropdowns_map)
126
128
  )
127
129
  if (
@@ -135,7 +137,7 @@ class CreateEntitySchema(BaseOperation):
135
137
 
136
138
 
137
139
  class ArchiveEntitySchema(BaseOperation):
138
- order: ClassVar[int] = 180
140
+ order: ClassVar[int] = 170
139
141
 
140
142
  def __init__(self, wh_schema_name: str) -> None:
141
143
  self.wh_schema_name = wh_schema_name
@@ -160,7 +162,7 @@ class ArchiveEntitySchema(BaseOperation):
160
162
 
161
163
 
162
164
  class UnarchiveEntitySchema(BaseOperation):
163
- order: ClassVar[int] = 110
165
+ order: ClassVar[int] = 100
164
166
 
165
167
  def __init__(self, wh_schema_name: str) -> None:
166
168
  self.wh_schema_name = wh_schema_name
@@ -245,20 +247,43 @@ class UpdateEntitySchema(BaseOperation):
245
247
  raise ValueError(
246
248
  f"Entity schema prefix {self.update_props.prefix} already exists in Benchling."
247
249
  )
248
- if (
249
- self.update_props.naming_strategies
250
- and not BenchlingNamingStrategy.is_valid_set(
251
- self.update_props.naming_strategies, bool(tag_schema.nameTemplateParts)
252
- )
253
- ):
254
- raise ValueError(
255
- "Invalid naming strategies for schema. The name template must be set on the schema through the UI when using template-based naming strategies."
256
- )
257
250
  return tag_schema
258
251
 
259
252
 
253
+ class UpdateEntitySchemaNameTemplate(BaseOperation):
254
+ order: ClassVar[int] = 150
255
+
256
+ def __init__(
257
+ self,
258
+ wh_schema_name: str,
259
+ update_name_template: BaseNameTemplate,
260
+ ) -> None:
261
+ self.wh_schema_name = wh_schema_name
262
+ self.update_name_template = update_name_template
263
+
264
+ def execute(self, benchling_service: BenchlingService) -> dict[str, Any]:
265
+ tag_schema = TagSchemaModel.get_one(benchling_service, self.wh_schema_name)
266
+ updated_schema = tag_schema.update_name_template(self.update_name_template)
267
+ return set_tag_schema_name_template(
268
+ benchling_service,
269
+ tag_schema.id,
270
+ {
271
+ "nameTemplateParts": [
272
+ part.model_dump() for part in updated_schema.nameTemplateParts
273
+ ],
274
+ "shouldOrderNamePartsBySequence": updated_schema.shouldOrderNamePartsBySequence,
275
+ },
276
+ )
277
+
278
+ def describe_operation(self) -> str:
279
+ return f"{self.wh_schema_name}: Updating name template to {str(self.update_name_template)}."
280
+
281
+ def describe(self) -> str:
282
+ return f"{self.wh_schema_name}: Name template is different in code versus Benchling."
283
+
284
+
260
285
  class CreateEntitySchemaField(BaseOperation):
261
- order: ClassVar[int] = 120
286
+ order: ClassVar[int] = 110
262
287
 
263
288
  def __init__(
264
289
  self,
@@ -345,7 +370,7 @@ class CreateEntitySchemaField(BaseOperation):
345
370
 
346
371
 
347
372
  class ArchiveEntitySchemaField(BaseOperation):
348
- order: ClassVar[int] = 160
373
+ order: ClassVar[int] = 140
349
374
 
350
375
  def __init__(
351
376
  self, wh_schema_name: str, wh_field_name: str, index: int | None = None
@@ -396,7 +421,7 @@ class ArchiveEntitySchemaField(BaseOperation):
396
421
 
397
422
 
398
423
  class UnarchiveEntitySchemaField(BaseOperation):
399
- order: ClassVar[int] = 130
424
+ order: ClassVar[int] = 120
400
425
 
401
426
  def __init__(
402
427
  self, wh_schema_name: str, wh_field_name: str, index: int | None = None
@@ -443,7 +468,7 @@ class UnarchiveEntitySchemaField(BaseOperation):
443
468
 
444
469
 
445
470
  class UpdateEntitySchemaField(BaseOperation):
446
- order: ClassVar[int] = 140
471
+ order: ClassVar[int] = 130
447
472
 
448
473
  def __init__(
449
474
  self,
@@ -514,7 +539,7 @@ class UpdateEntitySchemaField(BaseOperation):
514
539
 
515
540
 
516
541
  class ReorderEntitySchemaFields(BaseOperation):
517
- order: ClassVar[int] = 170
542
+ order: ClassVar[int] = 160
518
543
 
519
544
  def __init__(self, wh_schema_name: str, new_order: list[str]) -> None:
520
545
  self.wh_schema_name = wh_schema_name