liminal-orm 1.1.4__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.
Files changed (67) hide show
  1. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/PKG-INFO +17 -20
  2. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/README.md +16 -19
  3. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/base/base_operation.py +4 -3
  4. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/base/base_validation_filters.py +15 -0
  5. liminal_orm-2.0.0/liminal/base/name_template_parts.py +96 -0
  6. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/base/properties/base_field_properties.py +2 -2
  7. liminal_orm-2.0.0/liminal/base/properties/base_name_template.py +83 -0
  8. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/base/properties/base_schema_properties.py +13 -1
  9. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/dropdowns/compare.py +8 -0
  10. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/dropdowns/operations.py +1 -1
  11. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/entity_schemas/api.py +18 -0
  12. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/entity_schemas/compare.py +62 -8
  13. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/entity_schemas/entity_schema_models.py +43 -0
  14. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/entity_schemas/generate_files.py +13 -11
  15. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/entity_schemas/operations.py +43 -18
  16. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/entity_schemas/tag_schema_models.py +146 -3
  17. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/entity_schemas/utils.py +15 -2
  18. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/enums/__init__.py +0 -1
  19. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/enums/benchling_entity_type.py +8 -0
  20. liminal_orm-2.0.0/liminal/enums/name_template_part_type.py +12 -0
  21. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/external/__init__.py +11 -1
  22. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/migrate/revisions_timeline.py +2 -1
  23. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/orm/base_model.py +90 -29
  24. liminal_orm-2.0.0/liminal/orm/name_template.py +39 -0
  25. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/orm/schema_properties.py +27 -1
  26. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/tests/conftest.py +18 -9
  27. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/tests/test_entity_schema_compare.py +61 -12
  28. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/utils.py +9 -0
  29. liminal_orm-2.0.0/liminal/validation/__init__.py +154 -0
  30. liminal_orm-1.1.4/liminal/enums/benchling_report_level.py → liminal_orm-2.0.0/liminal/validation/validation_severity.py +2 -2
  31. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/pyproject.toml +1 -1
  32. liminal_orm-1.1.4/liminal/validation/__init__.py +0 -178
  33. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/LICENSE.md +0 -0
  34. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/__init__.py +0 -0
  35. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/base/base_dropdown.py +0 -0
  36. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/base/compare_operation.py +0 -0
  37. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/base/str_enum.py +0 -0
  38. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/cli/cli.py +0 -0
  39. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/cli/controller.py +0 -0
  40. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/cli/live_test_dropdown_migration.py +0 -0
  41. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/cli/live_test_entity_schema_migration.py +0 -0
  42. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/connection/__init__.py +0 -0
  43. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/connection/benchling_connection.py +0 -0
  44. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/connection/benchling_service.py +0 -0
  45. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/dropdowns/api.py +0 -0
  46. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/dropdowns/generate_files.py +0 -0
  47. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/dropdowns/utils.py +0 -0
  48. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/enums/benchling_api_field_type.py +0 -0
  49. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/enums/benchling_field_type.py +0 -0
  50. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/enums/benchling_folder_item_type.py +0 -0
  51. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/enums/benchling_naming_strategy.py +0 -0
  52. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/enums/benchling_sequence_type.py +0 -0
  53. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/mappers.py +0 -0
  54. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/migrate/components.py +0 -0
  55. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/migrate/revision.py +0 -0
  56. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/migrate/utils.py +0 -0
  57. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/orm/base.py +0 -0
  58. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/orm/base_tables/registry_entity.py +0 -0
  59. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/orm/base_tables/schema.py +0 -0
  60. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/orm/base_tables/user.py +0 -0
  61. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/orm/column.py +0 -0
  62. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/orm/mixins.py +0 -0
  63. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/orm/relationship.py +0 -0
  64. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/py.typed +0 -0
  65. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/tests/__init__.py +0 -0
  66. {liminal_orm-1.1.4 → liminal_orm-2.0.0}/liminal/tests/from benchling_sdk.py +0 -0
  67. {liminal_orm-1.1.4 → 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: 1.1.4
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 [Discussions](https://github.com/dynotx/liminal-orm/discussions) forum 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 (contact information below). You can expect responses within 48 hours :)
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 BenchlingValidator, BenchlingValidatorReport, BenchlingReportLevel
87
- from liminal.orm.base_model import BaseModel
88
-
89
- class CookTempValidator(BenchlingValidator):
90
- """Validates that a field value is a valid enum value for a Benchling entity"""
91
-
92
- def validate(self, entity: type[BaseModel]) -> BenchlingValidatorReport:
93
- valid = True
94
- message = None
95
- if entity.cook_time is not None and entity.cook_temp is None:
96
- valid = False
97
- message = "Cook temp is required if cook time is set"
98
- if entity.cook_time is None and entity.cook_temp is not None:
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 [Discussions](https://github.com/dynotx/liminal-orm/discussions) forum 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 (contact information below). You can expect responses within 48 hours :)
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 BenchlingValidator, BenchlingValidatorReport, BenchlingReportLevel
56
- from liminal.orm.base_model import BaseModel
57
-
58
- class CookTempValidator(BenchlingValidator):
59
- """Validates that a field value is a valid enum value for a Benchling entity"""
60
-
61
- def validate(self, entity: type[BaseModel]) -> BenchlingValidatorReport:
62
- valid = True
63
- message = None
64
- if entity.cook_time is not None and entity.cook_temp is None:
65
- valid = False
66
- message = "Cook temp is required if cook time is set"
67
- if entity.cook_time is None and entity.cook_temp is not None:
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. ReorderFields
23
- 16. ArchiveSchema
24
- 17. ArchiveDropdown
22
+ 15. UpdateEntitySchemaNameTemplate
23
+ 16. ReorderFields
24
+ 17. ArchiveSchema
25
+ 18. ArchiveDropdown
25
26
  """
26
27
 
27
28
 
@@ -7,6 +7,21 @@ class BaseValidatorFilters(BaseModel):
7
7
  """
8
8
  This class is used to pass base filters to benchling warehouse database queries.
9
9
  These columns are found on all tables in the benchling warehouse database.
10
+
11
+ Parameters
12
+ ----------
13
+ created_date_start: date | None
14
+ Start date for created date filter.
15
+ created_date_end: date | None
16
+ End date for created date filter.
17
+ updated_date_start: date | None
18
+ Start date for updated date filter.
19
+ updated_date_end: date | None
20
+ End date for updated date filter.
21
+ entity_ids: list[str] | None
22
+ List of entity IDs to filter by.
23
+ creator_full_names: list[str] | None
24
+ List of creator full names to filter by.
10
25
  """
11
26
 
12
27
  created_date_start: date | None = None
@@ -0,0 +1,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 validate_column(self, wh_name: str) -> bool:
68
+ def validate_column_definition(self, wh_name: str) -> bool:
69
69
  """If the Field Properties are meant to represent a column in Benchling,
70
70
  this will validate the properties and ensure that the entity_link and dropdowns are valid names that exist in our code.
71
71
  """
@@ -122,4 +122,4 @@ class BaseFieldProperties(BaseModel):
122
122
 
123
123
  def __repr__(self) -> str:
124
124
  """Generates a string representation of the class so that it can be executed."""
125
- return f"{self.__class__.__name__}({', '.join([f'{k}={v.__repr__()}' for k, v in self.model_dump(exclude_defaults=True).items()])})"
125
+ return f"{self.__class__.__name__}({', '.join([f'{k}={v.__repr__()}' for k, v in self.model_dump(exclude_unset=True, exclude_defaults=True).items()])})"
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ from liminal.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]
@@ -76,7 +76,7 @@ class CreateDropdown(BaseOperation):
76
76
 
77
77
 
78
78
  class ArchiveDropdown(BaseOperation):
79
- order: ClassVar[int] = 190
79
+ order: ClassVar[int] = 180
80
80
 
81
81
  def __init__(self, dropdown_name: str) -> None:
82
82
  self.dropdown_name = dropdown_name
@@ -75,3 +75,21 @@ def update_tag_schema(
75
75
  return await_queued_response(
76
76
  queued_response.json()["status_url"], benchling_service
77
77
  )
78
+
79
+
80
+ def set_tag_schema_name_template(
81
+ benchling_service: BenchlingService, entity_schema_id: str, payload: dict[str, Any]
82
+ ) -> dict[str, Any]:
83
+ """
84
+ Update the tag schema name template. Must be in a separate endpoint compared to update_tag_schema.
85
+ """
86
+ with requests.Session() as session:
87
+ response = session.post(
88
+ f"https://{benchling_service.benchling_tenant}.benchling.com/1/api/tag-schemas/{entity_schema_id}/actions/set-name-template",
89
+ data=json.dumps(payload),
90
+ headers=benchling_service.custom_post_headers,
91
+ cookies=benchling_service.custom_post_cookies,
92
+ )
93
+ if not response.ok:
94
+ raise Exception("Failed to set tag schema name template:", response.content)
95
+ return response.json()
@@ -1,5 +1,8 @@
1
+ import logging
2
+
1
3
  from liminal.base.compare_operation import CompareOperation
2
4
  from liminal.base.properties.base_field_properties import BaseFieldProperties
5
+ from liminal.base.properties.base_name_template import BaseNameTemplate
3
6
  from liminal.base.properties.base_schema_properties import BaseSchemaProperties
4
7
  from liminal.connection import BenchlingService
5
8
  from liminal.entity_schemas.operations import (
@@ -12,12 +15,15 @@ from liminal.entity_schemas.operations import (
12
15
  UnarchiveEntitySchemaField,
13
16
  UpdateEntitySchema,
14
17
  UpdateEntitySchemaField,
18
+ UpdateEntitySchemaNameTemplate,
15
19
  )
16
20
  from liminal.entity_schemas.utils import get_converted_tag_schemas
17
21
  from liminal.orm.base_model import BaseModel
18
22
  from liminal.orm.column import Column
19
23
  from liminal.utils import to_snake_case
20
24
 
25
+ LOGGER = logging.getLogger(__name__)
26
+
21
27
 
22
28
  def compare_entity_schemas(
23
29
  benchling_service: BenchlingService, schema_names: set[str] | None = None
@@ -46,13 +52,18 @@ def compare_entity_schemas(
46
52
  for m in BaseModel.get_all_subclasses(schema_names)
47
53
  if not m.__schema_properties__._archived
48
54
  ]
55
+ if len(models) == 0 and len(benchling_schemas) > 0:
56
+ LOGGER.warning(
57
+ "WARNING: No model classes found that inherit from BaseModel. Ensure that the model classes are defined and imported correctly."
58
+ )
59
+
49
60
  archived_benchling_schema_wh_names = [
50
- s.warehouse_name for s, _ in benchling_schemas if s._archived is True
61
+ s.warehouse_name for s, _, _ in benchling_schemas if s._archived is True
51
62
  ]
52
63
  # Running list of schema names from benchling. As each model is checked, remove the schema name from this list.
53
64
  # This is used at the end to check if there are any schemas left (schemas that exist in benchling but not in code) and archive them if they are.
54
65
  running_benchling_schema_names = list(
55
- [s.warehouse_name for s, _ in benchling_schemas]
66
+ [s.warehouse_name for s, _, _ in benchling_schemas]
56
67
  )
57
68
  # Iterate through each benchling model defined in code.
58
69
  for model in models:
@@ -61,15 +72,17 @@ def compare_entity_schemas(
61
72
  exclude_base_columns=True
62
73
  )
63
74
  # Validate the entity_link and dropdown_link reference an entity_schema or dropdown that exists in code.
64
- model.validate_model()
75
+ model.validate_model_definition()
65
76
  # if the model table_name is found in the benchling schemas, check for changes...
66
77
  if (model_wh_name := model.__schema_properties__.warehouse_name) in [
67
- s.warehouse_name for s, _ in benchling_schemas
78
+ s.warehouse_name for s, _, _ in benchling_schemas
68
79
  ]:
69
- benchling_schema_props, benchling_schema_fields = next(
70
- (s, lof)
71
- for s, lof in benchling_schemas
72
- if s.warehouse_name == model_wh_name
80
+ benchling_schema_props, benchling_name_template, benchling_schema_fields = (
81
+ next(
82
+ (s, nt, lof)
83
+ for s, nt, lof in benchling_schemas
84
+ if s.warehouse_name == model_wh_name
85
+ )
73
86
  )
74
87
  archived_benchling_schema_fields = {
75
88
  k: v for k, v in benchling_schema_fields.items() if v._archived is True
@@ -237,6 +250,23 @@ def compare_entity_schemas(
237
250
  ),
238
251
  ),
239
252
  )
253
+ if benchling_name_template != model.__name_template__:
254
+ ops.append(
255
+ CompareOperation(
256
+ op=UpdateEntitySchemaNameTemplate(
257
+ model.__schema_properties__.warehouse_name,
258
+ BaseNameTemplate(
259
+ **benchling_name_template.merge(model.__name_template__)
260
+ ),
261
+ ),
262
+ reverse_op=UpdateEntitySchemaNameTemplate(
263
+ model.__schema_properties__.warehouse_name,
264
+ BaseNameTemplate(
265
+ **model.__name_template__.merge(benchling_name_template)
266
+ ),
267
+ ),
268
+ )
269
+ )
240
270
  # If the model is not found as the benchling schema, Create.
241
271
  # Benchling api does not allow for setting a custom warehouse_name,
242
272
  # so we need to run another UpdateEntitySchema to set the warehouse_name if it is different from the snakecase version of the model name.
@@ -276,6 +306,30 @@ def compare_entity_schemas(
276
306
  ),
277
307
  )
278
308
  )
309
+ benchling_given_name_template = BaseNameTemplate(
310
+ parts=[], order_name_parts_by_sequence=False
311
+ )
312
+ if benchling_name_template != model.__name_template__:
313
+ ops.append(
314
+ CompareOperation(
315
+ op=UpdateEntitySchemaNameTemplate(
316
+ model.__schema_properties__.warehouse_name,
317
+ BaseNameTemplate(
318
+ **benchling_given_name_template.merge(
319
+ model.__name_template__
320
+ )
321
+ ),
322
+ ),
323
+ reverse_op=UpdateEntitySchemaNameTemplate(
324
+ model.__schema_properties__.warehouse_name,
325
+ BaseNameTemplate(
326
+ **benchling_given_name_template.merge(
327
+ model.__name_template__
328
+ )
329
+ ),
330
+ ),
331
+ )
332
+ )
279
333
 
280
334
  model_operations[model.__schema_properties__.warehouse_name] = ops
281
335
  running_benchling_schema_names = [