liminal-orm 1.1.3__tar.gz → 2.0.0__tar.gz
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_orm-1.1.3 → liminal_orm-2.0.0}/PKG-INFO +17 -20
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/README.md +16 -19
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/base/base_operation.py +4 -3
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/base/base_validation_filters.py +15 -0
- liminal_orm-2.0.0/liminal/base/name_template_parts.py +96 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/base/properties/base_field_properties.py +2 -2
- liminal_orm-2.0.0/liminal/base/properties/base_name_template.py +83 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/base/properties/base_schema_properties.py +13 -1
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/dropdowns/compare.py +8 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/dropdowns/operations.py +1 -1
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/entity_schemas/api.py +18 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/entity_schemas/compare.py +62 -8
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/entity_schemas/entity_schema_models.py +43 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/entity_schemas/generate_files.py +13 -11
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/entity_schemas/operations.py +43 -18
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/entity_schemas/tag_schema_models.py +146 -3
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/entity_schemas/utils.py +15 -2
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/enums/__init__.py +0 -1
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/enums/benchling_entity_type.py +8 -0
- liminal_orm-2.0.0/liminal/enums/name_template_part_type.py +12 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/external/__init__.py +11 -1
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/migrate/revisions_timeline.py +2 -1
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/orm/base_model.py +90 -29
- liminal_orm-2.0.0/liminal/orm/name_template.py +39 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/orm/schema_properties.py +27 -1
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/tests/conftest.py +18 -9
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/tests/test_entity_schema_compare.py +61 -12
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/utils.py +9 -0
- liminal_orm-2.0.0/liminal/validation/__init__.py +154 -0
- liminal_orm-1.1.3/liminal/enums/benchling_report_level.py → liminal_orm-2.0.0/liminal/validation/validation_severity.py +2 -2
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/pyproject.toml +1 -1
- liminal_orm-1.1.3/liminal/validation/__init__.py +0 -178
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/LICENSE.md +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/__init__.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/base/base_dropdown.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/base/compare_operation.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/base/str_enum.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/cli/cli.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/cli/controller.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/cli/live_test_dropdown_migration.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/cli/live_test_entity_schema_migration.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/connection/__init__.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/connection/benchling_connection.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/connection/benchling_service.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/dropdowns/api.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/dropdowns/generate_files.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/dropdowns/utils.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/enums/benchling_api_field_type.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/enums/benchling_field_type.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/enums/benchling_folder_item_type.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/enums/benchling_naming_strategy.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/enums/benchling_sequence_type.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/mappers.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/migrate/components.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/migrate/revision.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/migrate/utils.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/orm/base.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/orm/base_tables/registry_entity.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/orm/base_tables/schema.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/orm/base_tables/user.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/orm/column.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/orm/mixins.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/orm/relationship.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/py.typed +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/tests/__init__.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/tests/from benchling_sdk.py +0 -0
- {liminal_orm-1.1.3 → liminal_orm-2.0.0}/liminal/tests/test_dropdown_compare.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: liminal-orm
|
3
|
-
Version:
|
3
|
+
Version: 2.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
|
@@ -41,13 +41,12 @@ Liminal ORM<sup>1</sup> is an open-source Python package that builds on [Benchli
|
|
41
41
|
Liminal provides an ORM framework using [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy) along with a schema migration service inspired by [Alembic](https://alembic.sqlalchemy.org/en/latest/). This allows you to define your Benchling schemas in code and create a *single source of truth* that synchronizes between your upstream Benchling tenant(s) and downstream dependencies. By creating a standard interface and through using one-line CLI<sup>3</sup> commands, Liminal enables a code-first approach for managing Benchling tenants and accessing Benchling data. With the schemas defined in code, you can also take advantage of the additional capabilities that the Liminal toolkit provides. This includes:
|
42
42
|
|
43
43
|
- The ability to run migrations to your Benchling tenant(s) through an easy to use CLI.
|
44
|
-
- One source of truth defined in code for your Benchling schema model that your many Benchling tenants can stay in sync with.
|
45
44
|
- Easy to implement validation rules to reflect business logic for all of your Benchling entities.
|
46
45
|
- Strongly typed queries for all your Benchling entities.
|
47
46
|
- CI/CD integration with GitHub Actions to ensure that your Benchling schemas and code are always in sync.
|
48
47
|
- And more based on community contributions/feedback :)
|
49
48
|
|
50
|
-
If you are a Benchling user, try out Liminal by following the [**Quick Start Guide**](https://dynotx.github.io/liminal-orm/getting-started/prerequisites/)! Reach out in the [
|
49
|
+
If you are a Benchling user, try out Liminal by following the [**Quick Start Guide**](https://dynotx.github.io/liminal-orm/getting-started/prerequisites/)! Reach out in the [Slack community](https://join.slack.com/t/liminalorm/shared_invite/zt-2ujrp07s3-bctook4e~cAjn1LgOLVY~Q) (preferred method) with any questions or to simply introduce yourself! If there is something blocking you from using Liminal or you're having trouble setting Liminal up, please share in [Issues](https://github.com/dynotx/liminal-orm/issues) or reach out directly (info below).
|
51
50
|
|
52
51
|
Benchling is an industry standard cloud platform for life sciences R&D. Liminal builds on top of Benchling's platform and assumes that you already have a Benchling tenant set up and have (or have access to) an admin user account. If not, learn more about getting started with Benchling [here](https://www.benchling.com/explore-benchling)!
|
53
52
|
|
@@ -67,6 +66,7 @@ If you or your organization use Liminal, please consider adding yourself or your
|
|
67
66
|
- [Community](#community)
|
68
67
|
- [Contributing](#contributing)
|
69
68
|
- [License](#license)
|
69
|
+
- [Direct Contact](#direct-contact)
|
70
70
|
- [Acknowledgements](#acknowledgements)
|
71
71
|
- [Footnotes](#footnotes)
|
72
72
|
|
@@ -83,22 +83,19 @@ With your schemas defined in code, you can now take advantage of the additional
|
|
83
83
|
1. Entity validation: Easily create custom validation rules for your Benchling entities.
|
84
84
|
|
85
85
|
```python
|
86
|
-
from liminal.validation import
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
if
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
valid = False
|
100
|
-
message = "Cook time is required if cook temp is set"
|
101
|
-
return self.create_report(valid, BenchlingReportLevel.MED, entity, message)
|
86
|
+
from liminal.validation import ValidationSeverity, liminal_validator
|
87
|
+
|
88
|
+
class Pizza(BaseModel, CustomEntityMixin):
|
89
|
+
...
|
90
|
+
|
91
|
+
@liminal_validator(ValidationSeverity.MED)
|
92
|
+
def cook_time_and_temp_validator(self) -> None:
|
93
|
+
if self.cook_time is not None and self.cook_temp is None:
|
94
|
+
raise ValueError("Cook temp is required if cook time is set")
|
95
|
+
if self.cook_time is None and self.cook_temp is not None:
|
96
|
+
raise ValueError("Cook time is required if cook temp is set")
|
97
|
+
|
98
|
+
validation_reports = Pizza.validate(session)
|
102
99
|
```
|
103
100
|
|
104
101
|
2. Strongly typed queries: Write type-safe queries using SQLAlchemy to access your Benchling entities.
|
@@ -106,7 +103,6 @@ With your schemas defined in code, you can now take advantage of the additional
|
|
106
103
|
```python
|
107
104
|
with BenchlingSession(benchling_connection, with_db=True) as session:
|
108
105
|
pizza = session.query(Pizza).filter(Pizza.name == "Margherita").first()
|
109
|
-
print(pizza)
|
110
106
|
```
|
111
107
|
|
112
108
|
3. CI/CD integration: Use Liminal to automatically generate and apply your revision files to your Benchling tenant(s) as part of your CI/CD pipeline.
|
@@ -136,6 +132,7 @@ Liminal ORM is distributed under the [Apache License, Version 2.0](./LICENSE.md)
|
|
136
132
|
|
137
133
|
## [Direct Contact](#direct-contact)
|
138
134
|
|
135
|
+
- Liminal Community Slack group: [Join here](https://join.slack.com/t/liminalorm/shared_invite/zt-2ujrp07s3-bctook4e~cAjn1LgOLVY~Q)
|
139
136
|
- Email: <opensource@dynotx.com>
|
140
137
|
- LinkedIn: [Nirmit Damania](https://www.linkedin.com/in/nirmit-damania/)
|
141
138
|
|
@@ -10,13 +10,12 @@ Liminal ORM<sup>1</sup> is an open-source Python package that builds on [Benchli
|
|
10
10
|
Liminal provides an ORM framework using [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy) along with a schema migration service inspired by [Alembic](https://alembic.sqlalchemy.org/en/latest/). This allows you to define your Benchling schemas in code and create a *single source of truth* that synchronizes between your upstream Benchling tenant(s) and downstream dependencies. By creating a standard interface and through using one-line CLI<sup>3</sup> commands, Liminal enables a code-first approach for managing Benchling tenants and accessing Benchling data. With the schemas defined in code, you can also take advantage of the additional capabilities that the Liminal toolkit provides. This includes:
|
11
11
|
|
12
12
|
- The ability to run migrations to your Benchling tenant(s) through an easy to use CLI.
|
13
|
-
- One source of truth defined in code for your Benchling schema model that your many Benchling tenants can stay in sync with.
|
14
13
|
- Easy to implement validation rules to reflect business logic for all of your Benchling entities.
|
15
14
|
- Strongly typed queries for all your Benchling entities.
|
16
15
|
- CI/CD integration with GitHub Actions to ensure that your Benchling schemas and code are always in sync.
|
17
16
|
- And more based on community contributions/feedback :)
|
18
17
|
|
19
|
-
If you are a Benchling user, try out Liminal by following the [**Quick Start Guide**](https://dynotx.github.io/liminal-orm/getting-started/prerequisites/)! Reach out in the [
|
18
|
+
If you are a Benchling user, try out Liminal by following the [**Quick Start Guide**](https://dynotx.github.io/liminal-orm/getting-started/prerequisites/)! Reach out in the [Slack community](https://join.slack.com/t/liminalorm/shared_invite/zt-2ujrp07s3-bctook4e~cAjn1LgOLVY~Q) (preferred method) with any questions or to simply introduce yourself! If there is something blocking you from using Liminal or you're having trouble setting Liminal up, please share in [Issues](https://github.com/dynotx/liminal-orm/issues) or reach out directly (info below).
|
20
19
|
|
21
20
|
Benchling is an industry standard cloud platform for life sciences R&D. Liminal builds on top of Benchling's platform and assumes that you already have a Benchling tenant set up and have (or have access to) an admin user account. If not, learn more about getting started with Benchling [here](https://www.benchling.com/explore-benchling)!
|
22
21
|
|
@@ -36,6 +35,7 @@ If you or your organization use Liminal, please consider adding yourself or your
|
|
36
35
|
- [Community](#community)
|
37
36
|
- [Contributing](#contributing)
|
38
37
|
- [License](#license)
|
38
|
+
- [Direct Contact](#direct-contact)
|
39
39
|
- [Acknowledgements](#acknowledgements)
|
40
40
|
- [Footnotes](#footnotes)
|
41
41
|
|
@@ -52,22 +52,19 @@ With your schemas defined in code, you can now take advantage of the additional
|
|
52
52
|
1. Entity validation: Easily create custom validation rules for your Benchling entities.
|
53
53
|
|
54
54
|
```python
|
55
|
-
from liminal.validation import
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
def
|
62
|
-
|
63
|
-
|
64
|
-
if
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
valid = False
|
69
|
-
message = "Cook time is required if cook temp is set"
|
70
|
-
return self.create_report(valid, BenchlingReportLevel.MED, entity, message)
|
55
|
+
from liminal.validation import ValidationSeverity, liminal_validator
|
56
|
+
|
57
|
+
class Pizza(BaseModel, CustomEntityMixin):
|
58
|
+
...
|
59
|
+
|
60
|
+
@liminal_validator(ValidationSeverity.MED)
|
61
|
+
def cook_time_and_temp_validator(self) -> None:
|
62
|
+
if self.cook_time is not None and self.cook_temp is None:
|
63
|
+
raise ValueError("Cook temp is required if cook time is set")
|
64
|
+
if self.cook_time is None and self.cook_temp is not None:
|
65
|
+
raise ValueError("Cook time is required if cook temp is set")
|
66
|
+
|
67
|
+
validation_reports = Pizza.validate(session)
|
71
68
|
```
|
72
69
|
|
73
70
|
2. Strongly typed queries: Write type-safe queries using SQLAlchemy to access your Benchling entities.
|
@@ -75,7 +72,6 @@ With your schemas defined in code, you can now take advantage of the additional
|
|
75
72
|
```python
|
76
73
|
with BenchlingSession(benchling_connection, with_db=True) as session:
|
77
74
|
pizza = session.query(Pizza).filter(Pizza.name == "Margherita").first()
|
78
|
-
print(pizza)
|
79
75
|
```
|
80
76
|
|
81
77
|
3. CI/CD integration: Use Liminal to automatically generate and apply your revision files to your Benchling tenant(s) as part of your CI/CD pipeline.
|
@@ -105,6 +101,7 @@ Liminal ORM is distributed under the [Apache License, Version 2.0](./LICENSE.md)
|
|
105
101
|
|
106
102
|
## [Direct Contact](#direct-contact)
|
107
103
|
|
104
|
+
- Liminal Community Slack group: [Join here](https://join.slack.com/t/liminalorm/shared_invite/zt-2ujrp07s3-bctook4e~cAjn1LgOLVY~Q)
|
108
105
|
- Email: <opensource@dynotx.com>
|
109
106
|
- LinkedIn: [Nirmit Damania](https://www.linkedin.com/in/nirmit-damania/)
|
110
107
|
|
@@ -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()])})"
|
@@ -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]
|
@@ -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 = [
|