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.
- liminal/base/base_operation.py +4 -3
- liminal/base/base_validation_filters.py +15 -0
- liminal/base/name_template_parts.py +9 -0
- liminal/base/properties/base_field_properties.py +2 -2
- liminal/base/properties/base_name_template.py +83 -0
- liminal/base/properties/base_schema_properties.py +13 -1
- liminal/dropdowns/compare.py +8 -0
- liminal/dropdowns/operations.py +1 -1
- liminal/entity_schemas/api.py +18 -0
- liminal/entity_schemas/compare.py +62 -8
- liminal/entity_schemas/entity_schema_models.py +43 -0
- liminal/entity_schemas/generate_files.py +13 -11
- liminal/entity_schemas/operations.py +43 -18
- liminal/entity_schemas/tag_schema_models.py +146 -3
- liminal/entity_schemas/utils.py +15 -2
- liminal/enums/__init__.py +0 -1
- liminal/enums/benchling_entity_type.py +8 -0
- liminal/enums/name_template_part_type.py +12 -0
- liminal/external/__init__.py +11 -1
- liminal/migrate/revisions_timeline.py +2 -1
- liminal/orm/base_model.py +90 -29
- liminal/orm/name_template.py +39 -0
- liminal/orm/name_template_parts.py +96 -0
- liminal/orm/schema_properties.py +27 -1
- liminal/tests/conftest.py +18 -9
- liminal/tests/test_entity_schema_compare.py +61 -12
- liminal/utils.py +9 -0
- liminal/validation/__init__.py +84 -108
- liminal/{enums/benchling_report_level.py → validation/validation_severity.py} +2 -2
- {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.1.dist-info}/METADATA +17 -20
- {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.1.dist-info}/RECORD +34 -29
- {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.1.dist-info}/LICENSE.md +0 -0
- {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.1.dist-info}/WHEEL +0 -0
- {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.1.dist-info}/entry_points.txt +0 -0
liminal/base/base_operation.py
CHANGED
@@ -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.
|
23
|
-
16.
|
24
|
-
17.
|
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
|
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()])})"
|
liminal/dropdowns/compare.py
CHANGED
@@ -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]
|
liminal/dropdowns/operations.py
CHANGED
liminal/entity_schemas/api.py
CHANGED
@@ -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.
|
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 =
|
70
|
-
(
|
71
|
-
|
72
|
-
|
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
|
-
|
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(
|
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
|
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] =
|
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] =
|
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] =
|
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] =
|
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] =
|
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] =
|
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] =
|
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
|