liminal-orm 1.1.3__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- liminal/base/base_operation.py +4 -3
- liminal/base/base_validation_filters.py +15 -0
- liminal/base/name_template_parts.py +96 -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/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.3.dist-info → liminal_orm-2.0.0.dist-info}/METADATA +17 -20
- {liminal_orm-1.1.3.dist-info → liminal_orm-2.0.0.dist-info}/RECORD +33 -29
- {liminal_orm-1.1.3.dist-info → liminal_orm-2.0.0.dist-info}/LICENSE.md +0 -0
- {liminal_orm-1.1.3.dist-info → liminal_orm-2.0.0.dist-info}/WHEEL +0 -0
- {liminal_orm-1.1.3.dist-info → liminal_orm-2.0.0.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,96 @@
|
|
1
|
+
from typing import Any, ClassVar
|
2
|
+
|
3
|
+
from pydantic import BaseModel, ConfigDict, field_validator
|
4
|
+
|
5
|
+
from liminal.enums.name_template_part_type import NameTemplatePartType
|
6
|
+
|
7
|
+
|
8
|
+
class NameTemplatePart(BaseModel):
|
9
|
+
"""Base class for all name template parts. These are put together in a list (where order matters) to form a name template.
|
10
|
+
|
11
|
+
Parameters
|
12
|
+
----------
|
13
|
+
component_type : NameTemplatePartType
|
14
|
+
The type of the component. One of the values in the NameTemplatePartType enum.
|
15
|
+
|
16
|
+
"""
|
17
|
+
|
18
|
+
component_type: ClassVar[NameTemplatePartType]
|
19
|
+
|
20
|
+
_type_map: ClassVar[dict[NameTemplatePartType, type["NameTemplatePart"]]] = {}
|
21
|
+
|
22
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
23
|
+
|
24
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
25
|
+
super().__init_subclass__(**kwargs)
|
26
|
+
cls._type_map[cls.component_type] = cls
|
27
|
+
|
28
|
+
@classmethod
|
29
|
+
def resolve_type(cls, type: NameTemplatePartType) -> type["NameTemplatePart"]:
|
30
|
+
if type not in cls._type_map:
|
31
|
+
raise ValueError(f"Invalid name template part type: {type}")
|
32
|
+
return cls._type_map[type]
|
33
|
+
|
34
|
+
|
35
|
+
class SeparatorPart(NameTemplatePart):
|
36
|
+
component_type: ClassVar[NameTemplatePartType] = NameTemplatePartType.SEPARATOR
|
37
|
+
value: str
|
38
|
+
|
39
|
+
@field_validator("value")
|
40
|
+
def validate_value(cls, v: str) -> str:
|
41
|
+
if not v:
|
42
|
+
raise ValueError("value cannot be empty")
|
43
|
+
return v
|
44
|
+
|
45
|
+
|
46
|
+
class TextPart(NameTemplatePart):
|
47
|
+
component_type: ClassVar[NameTemplatePartType] = NameTemplatePartType.TEXT
|
48
|
+
value: str
|
49
|
+
|
50
|
+
@field_validator("value")
|
51
|
+
def validate_value(cls, v: str) -> str:
|
52
|
+
if not v:
|
53
|
+
raise ValueError("value cannot be empty")
|
54
|
+
return v
|
55
|
+
|
56
|
+
|
57
|
+
class CreationYearPart(NameTemplatePart):
|
58
|
+
component_type: ClassVar[NameTemplatePartType] = NameTemplatePartType.CREATION_YEAR
|
59
|
+
|
60
|
+
|
61
|
+
class CreationDatePart(NameTemplatePart):
|
62
|
+
component_type: ClassVar[NameTemplatePartType] = NameTemplatePartType.CREATION_DATE
|
63
|
+
|
64
|
+
|
65
|
+
class FieldPart(NameTemplatePart):
|
66
|
+
component_type: ClassVar[NameTemplatePartType] = NameTemplatePartType.FIELD
|
67
|
+
wh_field_name: str
|
68
|
+
|
69
|
+
|
70
|
+
class ParentLotNumberPart(NameTemplatePart):
|
71
|
+
component_type: ClassVar[NameTemplatePartType] = (
|
72
|
+
NameTemplatePartType.CHILD_ENTITY_LOT_NUMBER
|
73
|
+
)
|
74
|
+
wh_field_name: str
|
75
|
+
|
76
|
+
|
77
|
+
class RegistryIdentifierNumberPart(NameTemplatePart):
|
78
|
+
component_type: ClassVar[NameTemplatePartType] = (
|
79
|
+
NameTemplatePartType.REGISTRY_IDENTIFIER_NUMBER
|
80
|
+
)
|
81
|
+
|
82
|
+
|
83
|
+
class ProjectPart(NameTemplatePart):
|
84
|
+
component_type: ClassVar[NameTemplatePartType] = NameTemplatePartType.PROJECT
|
85
|
+
|
86
|
+
|
87
|
+
NameTemplateParts = (
|
88
|
+
SeparatorPart
|
89
|
+
| TextPart
|
90
|
+
| CreationYearPart
|
91
|
+
| CreationDatePart
|
92
|
+
| FieldPart
|
93
|
+
| RegistryIdentifierNumberPart
|
94
|
+
| ProjectPart
|
95
|
+
| ParentLotNumberPart
|
96
|
+
)
|
@@ -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.base.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.base.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 = (
|