liminal-orm 3.1.0__py3-none-any.whl → 3.2.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/.DS_Store +0 -0
- liminal/base/name_template_parts.py +2 -3
- liminal/base/properties/base_field_properties.py +0 -22
- liminal/base/properties/base_schema_properties.py +1 -1
- liminal/cli/cli.py +119 -26
- liminal/cli/controller.py +11 -6
- liminal/cli/utils.py +8 -5
- liminal/connection/__init__.py +4 -1
- liminal/connection/benchling_service.py +103 -0
- liminal/dropdowns/generate_files.py +2 -2
- liminal/entity_schemas/generate_files.py +6 -6
- liminal/entity_schemas/operations.py +7 -2
- liminal/entity_schemas/tag_schema_models.py +5 -2
- liminal/entity_schemas/utils.py +1 -0
- liminal/migrate/components.py +3 -1
- liminal/migrate/revisions_timeline.py +1 -3
- liminal/orm/base_model.py +26 -3
- liminal/orm/relationship.py +66 -14
- liminal/tests/.DS_Store +0 -0
- liminal/utils.py +16 -17
- liminal/validation/__init__.py +3 -3
- {liminal_orm-3.1.0.dist-info → liminal_orm-3.2.0.dist-info}/METADATA +1 -2
- {liminal_orm-3.1.0.dist-info → liminal_orm-3.2.0.dist-info}/RECORD +26 -24
- {liminal_orm-3.1.0.dist-info → liminal_orm-3.2.0.dist-info}/WHEEL +1 -1
- {liminal_orm-3.1.0.dist-info → liminal_orm-3.2.0.dist-info}/LICENSE.md +0 -0
- {liminal_orm-3.1.0.dist-info → liminal_orm-3.2.0.dist-info}/entry_points.txt +0 -0
liminal/.DS_Store
ADDED
Binary file
|
@@ -3,7 +3,6 @@ import warnings
|
|
3
3
|
from liminal.orm.name_template_parts import * # noqa: F403
|
4
4
|
|
5
5
|
warnings.warn(
|
6
|
-
"Importing from 'liminal.base.name_template_parts' is deprecated. Please import from 'liminal.orm.name_template_parts' instead.",
|
7
|
-
|
8
|
-
stacklevel=2,
|
6
|
+
"Importing from 'liminal.base.name_template_parts' is deprecated. Please import from 'liminal.orm.name_template_parts' instead. This will be removed in v4.",
|
7
|
+
FutureWarning,
|
9
8
|
)
|
@@ -4,10 +4,7 @@ from typing import Any
|
|
4
4
|
|
5
5
|
from pydantic import BaseModel, ConfigDict, PrivateAttr
|
6
6
|
|
7
|
-
from liminal.base.base_dropdown import BaseDropdown
|
8
7
|
from liminal.enums import BenchlingFieldType
|
9
|
-
from liminal.orm.base_model import BaseModel as BenchlingBaseModel
|
10
|
-
from liminal.utils import is_valid_wh_name
|
11
8
|
|
12
9
|
|
13
10
|
class BaseFieldProperties(BaseModel):
|
@@ -71,25 +68,6 @@ class BaseFieldProperties(BaseModel):
|
|
71
68
|
"""If the Field Properties are meant to represent a column in Benchling,
|
72
69
|
this will validate the properties and ensure that the entity_link and dropdowns are valid names that exist in our code.
|
73
70
|
"""
|
74
|
-
if self.entity_link:
|
75
|
-
if self.entity_link not in [
|
76
|
-
s.__schema_properties__.warehouse_name
|
77
|
-
for s in BenchlingBaseModel.get_all_subclasses()
|
78
|
-
]:
|
79
|
-
raise ValueError(
|
80
|
-
f"Field {wh_name}: could not find entity link {self.entity_link} as a warehouse name for any currently defined schemas."
|
81
|
-
)
|
82
|
-
if self.dropdown_link:
|
83
|
-
if self.dropdown_link not in [
|
84
|
-
d.__benchling_name__ for d in BaseDropdown.get_all_subclasses()
|
85
|
-
]:
|
86
|
-
raise ValueError(
|
87
|
-
f"Field {wh_name}: could not find dropdown link {self.dropdown_link} as a name to any defined dropdowns."
|
88
|
-
)
|
89
|
-
if not is_valid_wh_name(wh_name):
|
90
|
-
raise ValueError(
|
91
|
-
f"Field {wh_name}: invalid warehouse name '{wh_name}'. It should only contain alphanumeric characters and underscores."
|
92
|
-
)
|
93
71
|
return True
|
94
72
|
|
95
73
|
def merge(self, new_props: BaseFieldProperties) -> dict[str, Any]:
|
@@ -91,7 +91,7 @@ class BaseSchemaProperties(BaseModel):
|
|
91
91
|
|
92
92
|
def __init__(self, **data: Any):
|
93
93
|
super().__init__(**data)
|
94
|
-
self._archived = data.get("_archived", None)
|
94
|
+
self._archived = data.get("_archived", None) # TODO: WHY??
|
95
95
|
|
96
96
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
97
97
|
|
liminal/cli/cli.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env python
|
2
|
+
import warnings
|
2
3
|
from pathlib import Path
|
3
4
|
|
4
5
|
import typer
|
@@ -17,7 +18,9 @@ from liminal.cli.live_test_entity_schema_migration import (
|
|
17
18
|
mock_entity_schema_full_migration,
|
18
19
|
)
|
19
20
|
from liminal.cli.utils import read_local_liminal_dir, update_env_revision_id
|
20
|
-
from liminal.connection.benchling_service import
|
21
|
+
from liminal.connection.benchling_service import (
|
22
|
+
BenchlingService,
|
23
|
+
)
|
21
24
|
from liminal.migrate.revisions_timeline import RevisionsTimeline
|
22
25
|
|
23
26
|
|
@@ -54,15 +57,22 @@ def init() -> None:
|
|
54
57
|
new_revision_file_path = RevisionsTimeline(VERSIONS_DIR_PATH).init_versions(
|
55
58
|
VERSIONS_DIR_PATH
|
56
59
|
)
|
57
|
-
|
58
|
-
env_file =
|
60
|
+
|
61
|
+
env_file = """# This file is auto-generated by Liminal.
|
59
62
|
# Import the models and dropdowns you want to keep in sync here.
|
60
63
|
# Instantiate the BenchlingConnection(s) with the correct parameters for your tenant(s).
|
61
|
-
|
62
|
-
# The revision_id variable name can also match the format `<tenant_name>_CURRENT_REVISION_ID` / `<tenant_alias>_CURRENT_REVISION_ID`.
|
63
|
-
from liminal.connection import BenchlingConnection
|
64
|
+
from liminal.connection import BenchlingConnection, TenantConfigFlags
|
64
65
|
|
65
|
-
|
66
|
+
connection = BenchlingConnection(
|
67
|
+
tenant_name="pizzahouse-prod",
|
68
|
+
tenant_alias="prod",
|
69
|
+
api_client_id="my-secret-api-client-id",
|
70
|
+
api_client_secret="my-secret-api-client-secret",
|
71
|
+
warehouse_connection_string="...",
|
72
|
+
internal_api_admin_email="my-secret-internal-api-admin-email",
|
73
|
+
internal_api_admin_password="my-secret-internal-api-admin-password",
|
74
|
+
config_flags=TenantConfigFlags(...)
|
75
|
+
)
|
66
76
|
"""
|
67
77
|
with open(ENV_FILE_PATH, "w") as file:
|
68
78
|
file.write(env_file)
|
@@ -86,9 +96,7 @@ def generate_files(
|
|
86
96
|
help="The path to write the generated files to.",
|
87
97
|
),
|
88
98
|
) -> None:
|
89
|
-
|
90
|
-
LIMINAL_DIR_PATH, benchling_tenant
|
91
|
-
)
|
99
|
+
_, benchling_connection = read_local_liminal_dir(LIMINAL_DIR_PATH, benchling_tenant)
|
92
100
|
benchling_service = BenchlingService(benchling_connection, use_internal_api=True)
|
93
101
|
if not write_path.exists():
|
94
102
|
write_path.mkdir()
|
@@ -98,7 +106,7 @@ def generate_files(
|
|
98
106
|
|
99
107
|
@app.command(
|
100
108
|
name="current",
|
101
|
-
help="Returns the
|
109
|
+
help="Returns the remote revision_id that your Benchling tenant is currently on. Reads this from the name on the '_liminal_remote' schema.",
|
102
110
|
)
|
103
111
|
def current(
|
104
112
|
benchling_tenant: str = typer.Argument(
|
@@ -108,16 +116,34 @@ def current(
|
|
108
116
|
current_revision_id, benchling_connection = read_local_liminal_dir(
|
109
117
|
LIMINAL_DIR_PATH, benchling_tenant
|
110
118
|
)
|
111
|
-
|
112
|
-
|
113
|
-
|
119
|
+
benchling_service = BenchlingService(benchling_connection, use_internal_api=True)
|
120
|
+
try:
|
121
|
+
remote_revision_id = benchling_service.get_remote_revision_id()
|
122
|
+
if current_revision_id is not None:
|
123
|
+
warnings.warn(
|
124
|
+
f"Accessing and using the revision_id variable in {LIMINAL_DIR_PATH/'env.py'} is deprecated. Delete the variable set in the env.py file, the revision_id is now stored in your Benchling tenant within the '_liminal_remote' schema. Support for reading/writing the local revision_id will end with the v4 release.",
|
125
|
+
FutureWarning,
|
126
|
+
)
|
127
|
+
current_revision_id = remote_revision_id
|
128
|
+
except Exception:
|
129
|
+
pass
|
130
|
+
print(f"[blue]Current revision_id: {current_revision_id}.")
|
114
131
|
|
115
132
|
|
116
133
|
@app.command(
|
117
|
-
name="
|
134
|
+
name="head",
|
135
|
+
help="Returns the local heads, or the latest revision_id, in your linear revision timeline.",
|
136
|
+
)
|
137
|
+
def head() -> None:
|
138
|
+
revision_timeline = RevisionsTimeline(VERSIONS_DIR_PATH)
|
139
|
+
print(f"[blue]{revision_timeline.get_latest_revision().id}: (head)[/blue]")
|
140
|
+
|
141
|
+
|
142
|
+
@app.command(
|
143
|
+
name="revision",
|
118
144
|
help="Generates a revision file with a list of operations to bring the given Benchling tenant up to date with the locally defined schemas. Writes revision file to liminal/versions/.",
|
119
145
|
)
|
120
|
-
def
|
146
|
+
def revision(
|
121
147
|
benchling_tenant: str = typer.Argument(
|
122
148
|
..., help="Benchling tenant (or alias) to connect to."
|
123
149
|
),
|
@@ -125,19 +151,56 @@ def autogenerate(
|
|
125
151
|
...,
|
126
152
|
help="A description of the revision being generated. This will also be included in the file name.",
|
127
153
|
),
|
154
|
+
autogenerate: bool = typer.Option(
|
155
|
+
True,
|
156
|
+
"--autogenerate",
|
157
|
+
help="Automatically generate the revision file based on comparisons.",
|
158
|
+
),
|
128
159
|
) -> None:
|
129
160
|
current_revision_id, benchling_connection = read_local_liminal_dir(
|
130
161
|
LIMINAL_DIR_PATH, benchling_tenant
|
131
162
|
)
|
132
163
|
benchling_service = BenchlingService(benchling_connection, use_internal_api=True)
|
164
|
+
try:
|
165
|
+
remote_revision_id = benchling_service.get_remote_revision_id()
|
166
|
+
if current_revision_id is not None:
|
167
|
+
warnings.warn(
|
168
|
+
f"Accessing and using the revision_id variable in {LIMINAL_DIR_PATH/'env.py'} is deprecated. Delete the variable set in the env.py file, the revision_id is now stored in your Benchling tenant within the '_liminal_remote' schema. Support for reading/writing the local revision_id will end with the v4 release.",
|
169
|
+
FutureWarning,
|
170
|
+
)
|
171
|
+
current_revision_id = remote_revision_id
|
172
|
+
except Exception:
|
173
|
+
assert current_revision_id is not None
|
133
174
|
autogenerate_revision_file(
|
134
|
-
benchling_service,
|
175
|
+
benchling_service,
|
176
|
+
VERSIONS_DIR_PATH,
|
177
|
+
description,
|
178
|
+
current_revision_id,
|
179
|
+
autogenerate,
|
180
|
+
)
|
181
|
+
|
182
|
+
|
183
|
+
@app.command(
|
184
|
+
name="autogenerate",
|
185
|
+
hidden=True,
|
186
|
+
)
|
187
|
+
def autogenerate(
|
188
|
+
benchling_tenant: str = typer.Argument(
|
189
|
+
..., help="Benchling tenant (or alias) to connect to."
|
190
|
+
),
|
191
|
+
description: str = typer.Argument(
|
192
|
+
...,
|
193
|
+
help="A description of the revision being generated. This will also be included in the file name.",
|
194
|
+
),
|
195
|
+
) -> None:
|
196
|
+
raise DeprecationWarning(
|
197
|
+
"CLI command `liminal autogenerate ...` is deprecated and will be removed in v4. Please use `liminal revision ...` instead."
|
135
198
|
)
|
136
199
|
|
137
200
|
|
138
201
|
@app.command(
|
139
202
|
name="upgrade",
|
140
|
-
help="Upgrades the Benchling tenant by running revision file(s) based on the
|
203
|
+
help="Upgrades the Benchling tenant by running revision file(s) based on the passed in parameters. Uses the remote revision_id in Benchling as the starting point, and upgrades to the given revision by running the operations in the revision file(s).",
|
141
204
|
context_settings={"ignore_unknown_options": True},
|
142
205
|
)
|
143
206
|
def upgrade(
|
@@ -146,19 +209,35 @@ def upgrade(
|
|
146
209
|
),
|
147
210
|
upgrade_descriptor: str = typer.Argument(
|
148
211
|
...,
|
149
|
-
help="Determines the revision files that get run. Pass in the 'revision_id' to upgrade to that revision. Pass in 'head' to upgrade to the latest revision. Pass in '+n' to make a relative revision based on the current revision id.",
|
212
|
+
help="Determines the revision files that get run. Pass in the 'revision_id' to upgrade to that revision. Pass in 'head' to upgrade to the latest revision. Pass in '+n' to make a relative revision based on the current remote revision id.",
|
150
213
|
),
|
151
214
|
) -> None:
|
152
215
|
current_revision_id, benchling_connection = read_local_liminal_dir(
|
153
216
|
LIMINAL_DIR_PATH, benchling_tenant
|
154
217
|
)
|
218
|
+
local_revision_id_exists = current_revision_id is not None
|
155
219
|
benchling_service = BenchlingService(benchling_connection, use_internal_api=True)
|
220
|
+
try:
|
221
|
+
remote_revision_id = benchling_service.get_remote_revision_id()
|
222
|
+
if current_revision_id is not None:
|
223
|
+
warnings.warn(
|
224
|
+
f"Accessing and using the revision_id variable in {LIMINAL_DIR_PATH/'env.py'} is deprecated. Delete the variable set in the env.py file, the revision_id is now stored in your Benchling tenant within the '_liminal_remote' schema. Support for reading/writing the local revision_id will end with the v4 release.",
|
225
|
+
FutureWarning,
|
226
|
+
)
|
227
|
+
current_revision_id = remote_revision_id
|
228
|
+
except Exception:
|
229
|
+
assert current_revision_id is not None
|
156
230
|
upgrade_revision_id = upgrade_benchling_tenant(
|
157
231
|
benchling_service, VERSIONS_DIR_PATH, current_revision_id, upgrade_descriptor
|
158
232
|
)
|
159
|
-
|
233
|
+
benchling_service.upsert_remote_revision_id(upgrade_revision_id)
|
234
|
+
if local_revision_id_exists:
|
235
|
+
update_env_revision_id(ENV_FILE_PATH, benchling_tenant, upgrade_revision_id)
|
236
|
+
print(
|
237
|
+
f"[dim red]Set local {benchling_tenant}_CURRENT_REVISION_ID to {upgrade_revision_id} in liminal/env.py"
|
238
|
+
)
|
160
239
|
print(
|
161
|
-
f"[dim]Set
|
240
|
+
f"[dim]Set revision_id to {upgrade_revision_id} withinn '_liminal_remote' schema."
|
162
241
|
)
|
163
242
|
print("[bold green]Migration complete")
|
164
243
|
|
@@ -180,13 +259,29 @@ def downgrade(
|
|
180
259
|
current_revision_id, benchling_connection = read_local_liminal_dir(
|
181
260
|
LIMINAL_DIR_PATH, benchling_tenant
|
182
261
|
)
|
262
|
+
local_revision_id_exists = current_revision_id is not None
|
183
263
|
benchling_service = BenchlingService(benchling_connection, use_internal_api=True)
|
264
|
+
try:
|
265
|
+
remote_revision_id = benchling_service.get_remote_revision_id()
|
266
|
+
if current_revision_id is not None:
|
267
|
+
warnings.warn(
|
268
|
+
f"Accessing and using the revision_id variable in {LIMINAL_DIR_PATH/'env.py'} is deprecated. Delete the variable set in the env.py file, the revision_id is now stored in your Benchling tenant within the '_liminal_remote' schema. Support for reading/writing the local revision_id will end with the v4 release.",
|
269
|
+
FutureWarning,
|
270
|
+
)
|
271
|
+
current_revision_id = remote_revision_id
|
272
|
+
except Exception:
|
273
|
+
assert current_revision_id is not None
|
184
274
|
downgrade_revision_id = downgrade_benchling_tenant(
|
185
275
|
benchling_service, VERSIONS_DIR_PATH, current_revision_id, downgrade_descriptor
|
186
276
|
)
|
187
|
-
|
277
|
+
benchling_service.upsert_remote_revision_id(downgrade_revision_id)
|
278
|
+
if local_revision_id_exists:
|
279
|
+
update_env_revision_id(ENV_FILE_PATH, benchling_tenant, downgrade_revision_id)
|
280
|
+
print(
|
281
|
+
f"[dim red]Set local {benchling_tenant}_CURRENT_REVISION_ID to {downgrade_revision_id} in liminal/env.py"
|
282
|
+
)
|
188
283
|
print(
|
189
|
-
f"[dim]Set
|
284
|
+
f"[dim]Set revision_id to {downgrade_revision_id} withinn '_liminal_remote' schema."
|
190
285
|
)
|
191
286
|
print("[bold green]Migration complete")
|
192
287
|
|
@@ -217,9 +312,7 @@ def live_test(
|
|
217
312
|
raise ValueError(
|
218
313
|
"Only one of --entity-schema-migration or --dropdown-migration can be set."
|
219
314
|
)
|
220
|
-
|
221
|
-
LIMINAL_DIR_PATH, benchling_tenant
|
222
|
-
)
|
315
|
+
_, benchling_connection = read_local_liminal_dir(LIMINAL_DIR_PATH, benchling_tenant)
|
223
316
|
benchling_service = BenchlingService(benchling_connection, use_internal_api=True)
|
224
317
|
if test_entity_schema_migration:
|
225
318
|
mock_entity_schema_full_migration(
|
liminal/cli/controller.py
CHANGED
@@ -30,6 +30,7 @@ def autogenerate_revision_file(
|
|
30
30
|
write_dir_path: Path,
|
31
31
|
description: str,
|
32
32
|
current_revision_id: str,
|
33
|
+
compare: bool = True,
|
33
34
|
) -> None:
|
34
35
|
"""Generates a revision file by comparing locally defined schemas to the given Benchling tenant.
|
35
36
|
The revision file contains a unique revision id, the down_revision_id (which is the latest revision id that this revision is based on),
|
@@ -46,16 +47,20 @@ def autogenerate_revision_file(
|
|
46
47
|
A description of the revision being generated. This will also be included in the file name.
|
47
48
|
"""
|
48
49
|
revision_timeline = RevisionsTimeline(write_dir_path)
|
50
|
+
if current_revision_id not in revision_timeline.revisions_map.keys():
|
51
|
+
raise Exception(
|
52
|
+
f"Your target Benchling tenant is currently at revision_id {current_revision_id}. This does not exist within your revision timeline in any of your revision files. Ensure your current revision_id for your tenant is correct. The current local head revision is {revision_timeline.get_latest_revision().id}"
|
53
|
+
)
|
49
54
|
if current_revision_id != revision_timeline.get_latest_revision().id:
|
50
55
|
raise Exception(
|
51
|
-
f"Your target Benchling tenant is not up to date with the
|
56
|
+
f"Your target Benchling tenant is currently at revision_id {current_revision_id}, which is not up to date with the local head revision ({revision_timeline.get_latest_revision().id}). Please upgrade your tenant to the latest revision before generating a new revision."
|
52
57
|
)
|
53
|
-
|
54
|
-
|
55
|
-
if write_path is None:
|
56
|
-
print("[bold green]No changes needed. Skipping revision file generation.")
|
58
|
+
if compare:
|
59
|
+
compare_ops = get_full_migration_operations(benchling_service)
|
57
60
|
else:
|
58
|
-
|
61
|
+
compare_ops = []
|
62
|
+
write_path = revision_timeline.write_new_revision(description, compare_ops)
|
63
|
+
print(f"[bold green]Revision file generated at {write_path}")
|
59
64
|
|
60
65
|
|
61
66
|
def upgrade_benchling_tenant(
|
liminal/cli/utils.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
import importlib.util
|
2
|
+
from logging import Logger
|
2
3
|
from pathlib import Path
|
3
4
|
|
4
5
|
from liminal.connection.benchling_connection import BenchlingConnection
|
5
6
|
|
7
|
+
LOGGER = Logger(name=__name__)
|
8
|
+
|
6
9
|
|
7
10
|
def _check_liminal_directory_initialized(liminal_dir_path: Path) -> None:
|
8
11
|
"""Raises an exception if the liminal directory does not exist at the given path."""
|
@@ -19,7 +22,7 @@ def _check_liminal_directory_initialized(liminal_dir_path: Path) -> None:
|
|
19
22
|
|
20
23
|
def read_local_liminal_dir(
|
21
24
|
liminal_dir_path: Path, benchling_tenant: str
|
22
|
-
) -> tuple[str, BenchlingConnection]:
|
25
|
+
) -> tuple[str | None, BenchlingConnection]:
|
23
26
|
"""Imports the env.py file from /liminal/env.py and returns the CURRENT_REVISION_ID variable along with the BenchlingConnection object.
|
24
27
|
The env.py file is expected to have the CURRENT_REVISION_ID variable set to the revision id you are currently on.
|
25
28
|
The BenchlingConnection object is expected to be defined and have connection information for the Benchling API client and internal API.
|
@@ -34,7 +37,7 @@ def read_local_liminal_dir(
|
|
34
37
|
|
35
38
|
def _read_local_env_file(
|
36
39
|
env_file_path: Path, benchling_tenant: str
|
37
|
-
) -> tuple[str, BenchlingConnection]:
|
40
|
+
) -> tuple[str | None, BenchlingConnection]:
|
38
41
|
"""Imports the env.py file from the current working directory and returns the CURRENT_REVISION_ID variable along with the BenchlingConnection object.
|
39
42
|
The env.py file is expected to have the CURRENT_REVISION_ID variable set to the revision id you are currently on.
|
40
43
|
The BenchlingConnection object is expected to be defined and have connection information for the Benchling API client and internal API.
|
@@ -62,8 +65,8 @@ def _read_local_env_file(
|
|
62
65
|
"internal_api_admin_email and internal_api_admin_password must be provided in BenchlingConnection in liminal/env.py. This is necessary for the migration service."
|
63
66
|
)
|
64
67
|
try:
|
65
|
-
current_revision_id: str = getattr(
|
66
|
-
module, bc.current_revision_id_var_name
|
68
|
+
current_revision_id: str | None = getattr(
|
69
|
+
module, bc.current_revision_id_var_name, None
|
67
70
|
)
|
68
71
|
return current_revision_id, bc
|
69
72
|
except Exception as e:
|
@@ -78,7 +81,7 @@ def _read_local_env_file(
|
|
78
81
|
def update_env_revision_id(
|
79
82
|
env_file_path: Path, benchling_env: str, revision_id: str
|
80
83
|
) -> None:
|
81
|
-
"""Updates the CURRENT_REVISION_ID variable in the env.py file to the given revision id."""
|
84
|
+
"""Updates the CURRENT_REVISION_ID variable in the env.py file to the given revision id. REMOVE WITH v4 release."""
|
82
85
|
env_file_content = env_file_path.read_text().split("\n")
|
83
86
|
for i, line in enumerate(env_file_content):
|
84
87
|
if f"{benchling_env}_CURRENT_REVISION_ID =" in line:
|
liminal/connection/__init__.py
CHANGED
@@ -1,2 +1,5 @@
|
|
1
|
-
from liminal.connection.benchling_connection import
|
1
|
+
from liminal.connection.benchling_connection import (
|
2
|
+
BenchlingConnection, # noqa: F401
|
3
|
+
TenantConfigFlags, # noqa: F401
|
4
|
+
)
|
2
5
|
from liminal.connection.benchling_service import BenchlingService # noqa: F401
|
@@ -16,10 +16,20 @@ from tenacity import (
|
|
16
16
|
wait_exponential,
|
17
17
|
)
|
18
18
|
|
19
|
+
from liminal.base.properties.base_field_properties import BaseFieldProperties
|
20
|
+
from liminal.base.properties.base_schema_properties import BaseSchemaProperties
|
19
21
|
from liminal.connection.benchling_connection import BenchlingConnection
|
22
|
+
from liminal.enums import (
|
23
|
+
BenchlingEntityType,
|
24
|
+
BenchlingFieldType,
|
25
|
+
BenchlingNamingStrategy,
|
26
|
+
)
|
20
27
|
|
21
28
|
logger = logging.getLogger(__name__)
|
22
29
|
|
30
|
+
REMOTE_LIMINAL_SCHEMA_NAME = "_liminal_remote"
|
31
|
+
REMOTE_REVISION_ID_FIELD_WH_NAME = "revision_id"
|
32
|
+
|
23
33
|
|
24
34
|
class BenchlingService(Benchling):
|
25
35
|
"""
|
@@ -138,6 +148,99 @@ class BenchlingService(Benchling):
|
|
138
148
|
"""Closes all sessions and cleans up engine"""
|
139
149
|
self.engine.dispose()
|
140
150
|
|
151
|
+
def get_remote_revision_id(self) -> str:
|
152
|
+
"""
|
153
|
+
Uses internal API to to search for the _liminal_remote schema, where the revision_id is stored.
|
154
|
+
This schema contains the remote revision_id in the name of the revision_id field.
|
155
|
+
|
156
|
+
Returns the remote revision_id stored on the entity.
|
157
|
+
"""
|
158
|
+
from liminal.entity_schemas.tag_schema_models import TagSchemaModel
|
159
|
+
|
160
|
+
try:
|
161
|
+
liminal_schema = TagSchemaModel.get_one(self, REMOTE_LIMINAL_SCHEMA_NAME)
|
162
|
+
except Exception:
|
163
|
+
raise ValueError(
|
164
|
+
f"Did not find any schema name '{REMOTE_LIMINAL_SCHEMA_NAME}'. Run a liminal migration to populate your registry with the Liminal entity that stores the remote revision_id."
|
165
|
+
)
|
166
|
+
revision_id_fields = [
|
167
|
+
f
|
168
|
+
for f in liminal_schema.fields
|
169
|
+
if f.systemName == REMOTE_REVISION_ID_FIELD_WH_NAME
|
170
|
+
]
|
171
|
+
if len(revision_id_fields) == 1:
|
172
|
+
revision_id = revision_id_fields[0].name
|
173
|
+
assert revision_id is not None, "No revision_id set in field name."
|
174
|
+
return revision_id
|
175
|
+
else:
|
176
|
+
raise ValueError(
|
177
|
+
f"Error finding field on {REMOTE_LIMINAL_SCHEMA_NAME} schema with warehouse_name {REMOTE_REVISION_ID_FIELD_WH_NAME}. Check schema fields to ensure this field exists and is defined according to documentation."
|
178
|
+
)
|
179
|
+
|
180
|
+
def upsert_remote_revision_id(self, revision_id: str) -> None:
|
181
|
+
"""Updates or inserts a remote Liminal schema into your tenant with the given revision_id stored in the name of a field.
|
182
|
+
If the '_liminal_remote' schema is found, check and make sure a field with warehouse_name 'revision_id' is present. If both are present, update the revision_id stored within the name.
|
183
|
+
If no schema is found, create the _liminal_remote entity schema.
|
184
|
+
Upsert is needed to migrate users from using the CURRENT_REVISION_ID stored in the env.py file smoothly to storing in Benchling itself.
|
185
|
+
|
186
|
+
Parameters
|
187
|
+
----------
|
188
|
+
revision_id : str
|
189
|
+
revision_id of migration file to set in Benchling on remote liminal entity.
|
190
|
+
|
191
|
+
Returns
|
192
|
+
-------
|
193
|
+
CustomEntity
|
194
|
+
remote liminal entity with updated revision_id field.
|
195
|
+
"""
|
196
|
+
from liminal.entity_schemas.tag_schema_models import TagSchemaModel
|
197
|
+
|
198
|
+
try:
|
199
|
+
liminal_schema = TagSchemaModel.get_one(self, REMOTE_LIMINAL_SCHEMA_NAME)
|
200
|
+
except Exception:
|
201
|
+
# No _liminal_remote schema found. Create schema.
|
202
|
+
from liminal.entity_schemas.operations import CreateEntitySchema
|
203
|
+
|
204
|
+
CreateEntitySchema(
|
205
|
+
schema_properties=BaseSchemaProperties(
|
206
|
+
name={REMOTE_LIMINAL_SCHEMA_NAME},
|
207
|
+
warehouse_name={REMOTE_LIMINAL_SCHEMA_NAME},
|
208
|
+
prefix={REMOTE_LIMINAL_SCHEMA_NAME},
|
209
|
+
entity_type=BenchlingEntityType.CUSTOM_ENTITY,
|
210
|
+
naming_strategies={BenchlingNamingStrategy.NEW_IDS},
|
211
|
+
),
|
212
|
+
fields=[
|
213
|
+
BaseFieldProperties(
|
214
|
+
name={revision_id},
|
215
|
+
warehouse_name=REMOTE_REVISION_ID_FIELD_WH_NAME,
|
216
|
+
type=BenchlingFieldType.TEXT,
|
217
|
+
parent_link=False,
|
218
|
+
is_multi=False,
|
219
|
+
required=True,
|
220
|
+
)
|
221
|
+
],
|
222
|
+
).execute(self)
|
223
|
+
# _liminal_remote schema found. Check if revision_id field exists on it.
|
224
|
+
revision_id_fields = [
|
225
|
+
f
|
226
|
+
for f in liminal_schema.fields
|
227
|
+
if f.systemName == REMOTE_REVISION_ID_FIELD_WH_NAME
|
228
|
+
]
|
229
|
+
if len(revision_id_fields) == 1:
|
230
|
+
# _liminal_remote schema found, revision_id field found. Update revision_id field on it with given revision_id.
|
231
|
+
from liminal.entity_schemas.operations import UpdateEntitySchemaField
|
232
|
+
|
233
|
+
revision_id_field = revision_id_fields[0]
|
234
|
+
UpdateEntitySchemaField(
|
235
|
+
liminal_schema.sqlIdentifier,
|
236
|
+
revision_id_field.systemName,
|
237
|
+
BaseFieldProperties(name=revision_id),
|
238
|
+
).execute(self)
|
239
|
+
else:
|
240
|
+
raise ValueError(
|
241
|
+
f"Error finding field on {REMOTE_LIMINAL_SCHEMA_NAME} schema with warehouse_name {REMOTE_REVISION_ID_FIELD_WH_NAME}. Check schema fields to ensure this field exists and is defined according to documentation."
|
242
|
+
)
|
243
|
+
|
141
244
|
@classmethod
|
142
245
|
@retry(
|
143
246
|
stop=stop_after_attempt(3),
|
@@ -4,7 +4,7 @@ from rich import print
|
|
4
4
|
|
5
5
|
from liminal.connection import BenchlingService
|
6
6
|
from liminal.dropdowns.utils import get_benchling_dropdowns_dict
|
7
|
-
from liminal.utils import
|
7
|
+
from liminal.utils import to_pascal_case, to_snake_case
|
8
8
|
|
9
9
|
|
10
10
|
def generate_all_dropdown_files(
|
@@ -24,7 +24,7 @@ def generate_all_dropdown_files(
|
|
24
24
|
for dropdown_name, dropdown_options in dropdowns.items():
|
25
25
|
dropdown_values = [option.name for option in dropdown_options.options]
|
26
26
|
options_list = str(dropdown_values).replace("'", '"')
|
27
|
-
classname =
|
27
|
+
classname = to_pascal_case(dropdown_name)
|
28
28
|
dropdown_content = f"""
|
29
29
|
from liminal.base.base_dropdown import BaseDropdown
|
30
30
|
|
@@ -8,7 +8,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
10
|
from liminal.orm.name_template import NameTemplate
|
11
|
-
from liminal.utils import
|
11
|
+
from liminal.utils import to_pascal_case, to_snake_case
|
12
12
|
|
13
13
|
|
14
14
|
def get_entity_mixin(entity_type: BenchlingEntityType) -> str:
|
@@ -59,18 +59,18 @@ def generate_all_entity_schema_files(
|
|
59
59
|
subdirectory_map: dict[str, list[tuple[str, str]]] = {}
|
60
60
|
benchling_dropdowns = get_benchling_dropdowns_dict(benchling_service)
|
61
61
|
dropdown_name_to_classname_map: dict[str, str] = {
|
62
|
-
dropdown_name:
|
62
|
+
dropdown_name: to_pascal_case(dropdown_name)
|
63
63
|
for dropdown_name in benchling_dropdowns.keys()
|
64
64
|
}
|
65
65
|
wh_name_to_classname: dict[str, str] = {
|
66
|
-
sp.warehouse_name:
|
66
|
+
sp.warehouse_name: to_pascal_case(sp.name) for sp, _, _ in models
|
67
67
|
}
|
68
68
|
|
69
69
|
for schema_properties, name_template, columns in models:
|
70
|
-
classname =
|
70
|
+
classname = to_pascal_case(schema_properties.name)
|
71
71
|
|
72
72
|
for schema_properties, name_template, columns in models:
|
73
|
-
classname =
|
73
|
+
classname = to_pascal_case(schema_properties.name)
|
74
74
|
filename = to_snake_case(schema_properties.name) + ".py"
|
75
75
|
columns = {key: columns[key] for key in columns}
|
76
76
|
import_strings = [
|
@@ -117,7 +117,7 @@ def generate_all_entity_schema_files(
|
|
117
117
|
)
|
118
118
|
else:
|
119
119
|
relationship_strings.append(
|
120
|
-
f"""{tab}{col_name}_entities = multi_relationship("{wh_name_to_classname[col.entity_link]}",
|
120
|
+
f"""{tab}{col_name}_entities = multi_relationship("{wh_name_to_classname[col.entity_link]}", {col_name})"""
|
121
121
|
)
|
122
122
|
import_strings.append(
|
123
123
|
"from liminal.orm.relationship import multi_relationship"
|
@@ -313,10 +313,15 @@ class CreateEntitySchemaField(BaseOperation):
|
|
313
313
|
self.index = index
|
314
314
|
|
315
315
|
self._wh_field_name: str
|
316
|
+
self._field_name: str
|
316
317
|
if field_props.warehouse_name:
|
317
318
|
self._wh_field_name = field_props.warehouse_name
|
318
319
|
else:
|
319
320
|
raise ValueError("Field warehouse name is required.")
|
321
|
+
if field_props.name:
|
322
|
+
self._field_name = field_props.name
|
323
|
+
else:
|
324
|
+
raise ValueError("Field name is required.")
|
320
325
|
|
321
326
|
def execute(self, benchling_service: BenchlingService) -> dict[str, Any]:
|
322
327
|
try:
|
@@ -385,10 +390,10 @@ class CreateEntitySchemaField(BaseOperation):
|
|
385
390
|
if (
|
386
391
|
benchling_service.connection.config_flags.schemas_enable_change_warehouse_name
|
387
392
|
is False
|
388
|
-
and self.field_props.warehouse_name != to_snake_case(self.
|
393
|
+
and self.field_props.warehouse_name != to_snake_case(self._field_name)
|
389
394
|
):
|
390
395
|
raise ValueError(
|
391
|
-
f"{self.wh_schema_name}: Tenant config flag SCHEMAS_ENABLE_CHANGE_WAREHOUSE_NAME is required to set a custom field warehouse name. Reach out to Benchling support to turn this config flag to True and then set the flag to True in BenchlingConnection.config_flags. Otherwise, define the field warehouse_name in code to be the given Benchling warehouse name: {to_snake_case(self.
|
396
|
+
f"{self.wh_schema_name}: Tenant config flag SCHEMAS_ENABLE_CHANGE_WAREHOUSE_NAME is required to set a custom field warehouse name. Reach out to Benchling support to turn this config flag to True and then set the flag to True in BenchlingConnection.config_flags. Otherwise, define the field warehouse_name in code to be the given Benchling warehouse name: {to_snake_case(self._field_name)}."
|
392
397
|
)
|
393
398
|
if (
|
394
399
|
self.field_props.unit_name
|
@@ -388,7 +388,7 @@ class TagSchemaModel(BaseModel):
|
|
388
388
|
shouldCreateAsOligo: bool | None
|
389
389
|
shouldOrderNamePartsBySequence: bool | None
|
390
390
|
showResidues: bool | None
|
391
|
-
sqlIdentifier: str
|
391
|
+
sqlIdentifier: str
|
392
392
|
useOrganizationCollectionAliasForDisplayLabel: bool | None
|
393
393
|
useRandomOrgAlias: bool | None
|
394
394
|
|
@@ -526,11 +526,14 @@ class TagSchemaModel(BaseModel):
|
|
526
526
|
self.prefix = (
|
527
527
|
update_props.prefix if "prefix" in update_diff_names else self.prefix
|
528
528
|
)
|
529
|
-
|
529
|
+
|
530
|
+
set_sql_identifier = (
|
530
531
|
update_props.warehouse_name
|
531
532
|
if "warehouse_name" in update_diff_names
|
532
533
|
else self.sqlIdentifier
|
533
534
|
)
|
535
|
+
assert type(set_sql_identifier) is str
|
536
|
+
self.sqlIdentifier = set_sql_identifier
|
534
537
|
self.name = update_props.name if "name" in update_diff_names else self.name
|
535
538
|
return self
|
536
539
|
|
liminal/entity_schemas/utils.py
CHANGED
@@ -33,6 +33,7 @@ def get_converted_tag_schemas(
|
|
33
33
|
if include_archived
|
34
34
|
else [s for s in all_schemas if not s.archiveRecord]
|
35
35
|
)
|
36
|
+
all_schemas = [s for s in all_schemas if s.sqlIdentifier != "_liminal_remote"]
|
36
37
|
return [
|
37
38
|
convert_tag_schema_to_internal_schema(
|
38
39
|
tag_schema, dropdowns_map, unit_id_to_name_map, include_archived
|
liminal/migrate/components.py
CHANGED
@@ -76,7 +76,9 @@ def execute_operations(
|
|
76
76
|
o.execute(benchling_service)
|
77
77
|
except Exception as e:
|
78
78
|
traceback.print_exc()
|
79
|
-
print(
|
79
|
+
print(
|
80
|
+
f"[bold red]Error at step {index} executing operation {o.__class__.__name__}: {e}]"
|
81
|
+
)
|
80
82
|
return False
|
81
83
|
index += 1
|
82
84
|
return True
|
@@ -222,7 +222,7 @@ class RevisionsTimeline:
|
|
222
222
|
|
223
223
|
def write_new_revision(
|
224
224
|
self, message: str, operations: list[CompareOperation]
|
225
|
-
) -> str
|
225
|
+
) -> str:
|
226
226
|
"""Adds a new revision to the revisions map and writes the revision file to the versions directory if write is True.
|
227
227
|
The added revision will be the latest revision.
|
228
228
|
"""
|
@@ -234,8 +234,6 @@ class RevisionsTimeline:
|
|
234
234
|
upgrade_operations=[o.op for o in operations],
|
235
235
|
downgrade_operations=reversed([o.reverse_op for o in operations]),
|
236
236
|
)
|
237
|
-
if len(operations) == 0:
|
238
|
-
return None
|
239
237
|
new_revision.write_revision_file(self.versions_dir_path)
|
240
238
|
self.revisions_map[new_revision.id] = new_revision
|
241
239
|
self.get_revision(
|
liminal/orm/base_model.py
CHANGED
@@ -11,6 +11,7 @@ from sqlalchemy import Column as SqlColumn
|
|
11
11
|
from sqlalchemy.orm import Query, RelationshipProperty, Session, relationship
|
12
12
|
from sqlalchemy.orm.decl_api import declared_attr
|
13
13
|
|
14
|
+
from liminal.base.base_dropdown import BaseDropdown
|
14
15
|
from liminal.base.base_validation_filters import BaseValidatorFilters
|
15
16
|
from liminal.enums import BenchlingNamingStrategy
|
16
17
|
from liminal.enums.benchling_entity_type import BenchlingEntityType
|
@@ -20,6 +21,7 @@ from liminal.orm.base_tables.user import User
|
|
20
21
|
from liminal.orm.name_template import NameTemplate
|
21
22
|
from liminal.orm.name_template_parts import FieldPart
|
22
23
|
from liminal.orm.schema_properties import SchemaProperties
|
24
|
+
from liminal.utils import is_valid_wh_name
|
23
25
|
from liminal.validation import BenchlingValidatorReport
|
24
26
|
|
25
27
|
if TYPE_CHECKING:
|
@@ -175,7 +177,7 @@ class BaseModel(Generic[T], Base):
|
|
175
177
|
if len(models.keys()) != len(set(names)):
|
176
178
|
missing_models = set(names) - set(models.keys())
|
177
179
|
raise ValueError(
|
178
|
-
f"No model subclass found for the following class names or warehouse names: {', '.join(missing_models)}"
|
180
|
+
f"No model subclass found for the following class names or warehouse names: {', '.join(missing_models)}. Please ensure the entity schema model(s) are imported or defined."
|
179
181
|
)
|
180
182
|
return list(models.values())
|
181
183
|
|
@@ -218,11 +220,32 @@ class BaseModel(Generic[T], Base):
|
|
218
220
|
)
|
219
221
|
for wh_name, field in properties.items():
|
220
222
|
try:
|
221
|
-
field.
|
223
|
+
if field.entity_link:
|
224
|
+
if field.entity_link not in [
|
225
|
+
s.__schema_properties__.warehouse_name
|
226
|
+
for s in cls.__base__.get_all_subclasses()
|
227
|
+
]:
|
228
|
+
breakpoint()
|
229
|
+
raise ValueError(
|
230
|
+
f"Field {wh_name}: could not find entity link {field.entity_link} as a warehouse name for any currently defined schemas."
|
231
|
+
)
|
232
|
+
if field.dropdown_link:
|
233
|
+
if field.dropdown_link not in [
|
234
|
+
d.__benchling_name__ for d in BaseDropdown.get_all_subclasses()
|
235
|
+
]:
|
236
|
+
raise ValueError(
|
237
|
+
f"Field {wh_name}: could not find dropdown link {field.dropdown_link} as a name to any defined dropdowns."
|
238
|
+
)
|
239
|
+
if not is_valid_wh_name(wh_name):
|
240
|
+
raise ValueError(
|
241
|
+
f"Field {wh_name}: invalid warehouse name '{wh_name}'. It should only contain alphanumeric characters and underscores."
|
242
|
+
)
|
222
243
|
except ValueError as e:
|
223
244
|
errors.append(str(e))
|
224
245
|
if errors:
|
225
|
-
raise ValueError(
|
246
|
+
raise ValueError(
|
247
|
+
f"Invalid field properties for schema {cls.__tablename__}: {' '.join(errors)}"
|
248
|
+
)
|
226
249
|
return True
|
227
250
|
|
228
251
|
@classmethod
|
liminal/orm/relationship.py
CHANGED
@@ -1,5 +1,9 @@
|
|
1
|
-
|
1
|
+
import warnings
|
2
|
+
from typing import Any
|
2
3
|
|
4
|
+
from sqlalchemy.orm import RelationshipProperty, object_session, relationship
|
5
|
+
|
6
|
+
from liminal.orm.base_model import BaseModel
|
3
7
|
from liminal.orm.column import Column
|
4
8
|
|
5
9
|
|
@@ -31,32 +35,80 @@ def single_relationship(
|
|
31
35
|
)
|
32
36
|
|
33
37
|
|
34
|
-
def multi_relationship(
|
38
|
+
def multi_relationship(*args: Any, **kwargs: Any) -> RelationshipProperty:
|
39
|
+
"""Wrapper for generating a multi-relationship. Supporting the usage of a deprecated signature until v4 release."""
|
40
|
+
if len(args) == 2 and isinstance(args[1], Column):
|
41
|
+
return multi_relationship_v2(*args, **kwargs)
|
42
|
+
else:
|
43
|
+
return multi_relationship_deprecated(*args, **kwargs)
|
44
|
+
|
45
|
+
|
46
|
+
def multi_relationship_deprecated(
|
35
47
|
target_class_name: str, current_class_name: str, entity_link_field_name: str
|
36
48
|
) -> RelationshipProperty:
|
37
|
-
"""
|
49
|
+
"""
|
50
|
+
DEPRECATED: USE THE FUNCTION BELOW INSTEAD.
|
51
|
+
Wrapper for SQLAlchemy's relationship function. Liminal's recommendation for defining a relationship from
|
38
52
|
a class to a linked entity field that has is_multi=True. This means the representation of that field is a list of entity_ids.
|
39
53
|
Parameters
|
40
54
|
----------
|
41
55
|
target_class_name : str
|
42
56
|
Class name of the entity schema class that is being linked.
|
43
57
|
current_class_name : str
|
44
|
-
|
58
|
+
Name of the current class that is linking to the target class. This is not used.
|
45
59
|
entity_link_field_name : str
|
60
|
+
Name of the column on the current class that links to the target class.
|
61
|
+
|
62
|
+
Returns
|
63
|
+
-------
|
64
|
+
SQLAlchemy RelationshipProperty
|
65
|
+
"""
|
66
|
+
warnings.warn(
|
67
|
+
"This version of multi_relationship is deprecated. New function signature is multi_relationship(target_class_name: str, entity_link_field: Column). Support for this signature will end with the v4 release.",
|
68
|
+
FutureWarning,
|
69
|
+
stacklevel=2,
|
70
|
+
)
|
71
|
+
|
72
|
+
def getter(self: Any) -> list[Any]:
|
73
|
+
target_table = BaseModel.get_all_subclasses(names={target_class_name})[0]
|
74
|
+
session = object_session(self)
|
75
|
+
|
76
|
+
linked_entities = (
|
77
|
+
session.query(target_table)
|
78
|
+
.filter(target_table.id.in_(getattr(self, entity_link_field_name)))
|
79
|
+
.all()
|
80
|
+
)
|
81
|
+
return linked_entities
|
82
|
+
|
83
|
+
return property(getter)
|
84
|
+
|
85
|
+
|
86
|
+
def multi_relationship_v2(
|
87
|
+
target_class_name: str, entity_link_field: Column
|
88
|
+
) -> RelationshipProperty:
|
89
|
+
"""Wrapper for SQLAlchemy's relationship function. Liminal's recommendation for defining a relationship from
|
90
|
+
a class to a linked entity field that has is_multi=True. This means the representation of that field is a list of entity_ids.
|
91
|
+
Parameters
|
92
|
+
----------
|
93
|
+
target_class_name : str
|
94
|
+
Class name of the entity schema class that is being linked.
|
95
|
+
entity_link_field : Column
|
46
96
|
Column on the current class that links to the target class.
|
47
97
|
|
48
98
|
Returns
|
49
99
|
-------
|
50
100
|
SQLAlchemy RelationshipProperty
|
51
101
|
"""
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
102
|
+
|
103
|
+
def getter(self: Any) -> list[Any]:
|
104
|
+
target_table = BaseModel.get_all_subclasses(names={target_class_name})[0]
|
105
|
+
session = object_session(self)
|
106
|
+
|
107
|
+
linked_entities = (
|
108
|
+
session.query(target_table)
|
109
|
+
.filter(target_table.id.in_(getattr(self, entity_link_field.name)))
|
110
|
+
.all()
|
57
111
|
)
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
uselist=True,
|
62
|
-
)
|
112
|
+
return linked_entities
|
113
|
+
|
114
|
+
return property(getter)
|
liminal/tests/.DS_Store
ADDED
Binary file
|
liminal/utils.py
CHANGED
@@ -10,30 +10,26 @@ from liminal.connection.benchling_service import BenchlingService
|
|
10
10
|
|
11
11
|
|
12
12
|
def generate_random_id(length: int = 8) -> str:
|
13
|
-
"""Generate a random ID with only lowercase letters."""
|
13
|
+
"""Generate a pseudo-random ID with only lowercase letters."""
|
14
14
|
return "".join(random.choices(string.ascii_lowercase, k=length))
|
15
15
|
|
16
16
|
|
17
|
-
def
|
17
|
+
def to_pascal_case(input_string: str) -> str:
|
18
18
|
"""
|
19
|
-
Convert a string to PascalCase.
|
19
|
+
Convert a string to PascalCase. Filters out any non-alphanumeric characters.
|
20
20
|
"""
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
)
|
21
|
+
words = re.split(r"[ /_\-]", input_string)
|
22
|
+
# Then remove any non-alphanumeric characters and capitalize each word
|
23
|
+
return "".join(re.sub(r"[^a-zA-Z0-9]", "", word).capitalize() for word in words)
|
25
24
|
|
26
25
|
|
27
|
-
def to_snake_case(input_string: str
|
26
|
+
def to_snake_case(input_string: str) -> str:
|
28
27
|
"""
|
29
|
-
Convert a string to snake_case.
|
28
|
+
Convert a string to snake_case. Filters out any non-alphanumeric characters.
|
30
29
|
"""
|
31
|
-
|
32
|
-
|
33
|
-
return "_".join(
|
34
|
-
re.sub(r"[\[\]{}():]", "", word).lower()
|
35
|
-
for word in re.split(r"[ /_\-]", input_string)
|
36
|
-
)
|
30
|
+
words = re.split(r"[ /_\-]", input_string)
|
31
|
+
words = [word for word in words if word]
|
32
|
+
return "_".join(re.sub(r"[^a-zA-Z0-9]", "", word).lower() for word in words)
|
37
33
|
|
38
34
|
|
39
35
|
def to_string_val(input_val: Any) -> str:
|
@@ -64,11 +60,14 @@ def is_valid_prefix(prefix: str) -> bool:
|
|
64
60
|
It must be contain only alphanumeric characters and underscores, be less than 33 characters, and end with an alphabetic character.
|
65
61
|
"""
|
66
62
|
valid = (
|
67
|
-
all(c.isalnum() or c == "_" or c == "-" for c in prefix)
|
63
|
+
all(c.isalnum() or c == "_" or c == "-" for c in prefix)
|
64
|
+
and len(prefix) <= 32
|
65
|
+
and not prefix[-1].isdigit()
|
66
|
+
and " " not in prefix
|
68
67
|
)
|
69
68
|
if not valid:
|
70
69
|
raise ValueError(
|
71
|
-
f"Invalid prefix '{prefix}'.
|
70
|
+
f"Invalid prefix '{prefix}'. The prefix should only contain alphabetic characters or underscores, not end end in a digit, and not contain whitespace."
|
72
71
|
)
|
73
72
|
return valid
|
74
73
|
|
liminal/validation/__init__.py
CHANGED
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Callable
|
|
4
4
|
|
5
5
|
from pydantic import BaseModel, ConfigDict
|
6
6
|
|
7
|
-
from liminal.utils import
|
7
|
+
from liminal.utils import to_pascal_case
|
8
8
|
from liminal.validation.validation_severity import ValidationSeverity
|
9
9
|
|
10
10
|
if TYPE_CHECKING:
|
@@ -137,14 +137,14 @@ def liminal_validator(
|
|
137
137
|
valid=False,
|
138
138
|
level=validator_level,
|
139
139
|
entity=self,
|
140
|
-
validator_name=validator_name or
|
140
|
+
validator_name=validator_name or to_pascal_case(func.__name__),
|
141
141
|
message=str(e),
|
142
142
|
)
|
143
143
|
return BenchlingValidatorReport.create_validation_report(
|
144
144
|
valid=True,
|
145
145
|
level=validator_level,
|
146
146
|
entity=self,
|
147
|
-
validator_name=validator_name or
|
147
|
+
validator_name=validator_name or to_pascal_case(func.__name__),
|
148
148
|
)
|
149
149
|
|
150
150
|
setattr(wrapper, "_is_liminal_validator", True)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: liminal-orm
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.2.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
|
@@ -11,7 +11,6 @@ Classifier: Programming Language :: Python :: 3.9
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
14
|
-
Classifier: Programming Language :: Python :: 3.13
|
15
14
|
Requires-Dist: benchling-sdk (>=1.8.0)
|
16
15
|
Requires-Dist: bs4 (>=0.0.2,<0.0.3)
|
17
16
|
Requires-Dist: lxml (>=5.3.0,<6.0.0)
|
@@ -1,33 +1,34 @@
|
|
1
|
+
liminal/.DS_Store,sha256=s_ehSI1aIzOjVRnFlcSzhtWS3irmEDSGHyS6l0QRcus,8196
|
1
2
|
liminal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
3
|
liminal/base/base_dropdown.py,sha256=Unk4l_5Y8rj_eSWYqzFi2BAFSQToQDWW2qdXwiCHTg8,2523
|
3
4
|
liminal/base/base_operation.py,sha256=opQfFZeC49YAFkg5ahE6CFpeSUNPh1ootWZxXyEXfFI,3128
|
4
5
|
liminal/base/base_validation_filters.py,sha256=kHG3G5gXkuNHQosMTrxRc57OTmczcaoSx0DmkrScIr4,1043
|
5
6
|
liminal/base/compare_operation.py,sha256=hkpv4ewHhxy4dlTPKgJuzBjsAqO6Km7OrrKB44pRA_o,352
|
6
|
-
liminal/base/name_template_parts.py,sha256=
|
7
|
-
liminal/base/properties/base_field_properties.py,sha256=
|
7
|
+
liminal/base/name_template_parts.py,sha256=uWKRIqlhxI6eMllRHBX2UDRyZ8xQFdi1fKAFoYBub3I,276
|
8
|
+
liminal/base/properties/base_field_properties.py,sha256=3n5tg3SqXellBQk-phDnN3qIXUnMP0iR5sC-IqD6_dY,3918
|
8
9
|
liminal/base/properties/base_name_template.py,sha256=AOtaW4QEDRC-mjZOZk6jgc_mopUMsHS2Fj6VVsO07WY,3150
|
9
|
-
liminal/base/properties/base_schema_properties.py,sha256=
|
10
|
+
liminal/base/properties/base_schema_properties.py,sha256=jm7dG218gzHITOPdnILY4BUnOCW-KA8yiQjgz5FgQUI,5906
|
10
11
|
liminal/base/str_enum.py,sha256=jF3d-Lo8zsHUe6GsctX2L-TSj92Y3qCYDrTD-saeJoc,210
|
11
|
-
liminal/cli/cli.py,sha256=
|
12
|
-
liminal/cli/controller.py,sha256=
|
12
|
+
liminal/cli/cli.py,sha256=F4JK-SrsC62-ufOJgaf8BMhgiuVMGv7pdUdTBmHg3aQ,13389
|
13
|
+
liminal/cli/controller.py,sha256=iIoyIq091vbS1hkjAG1Mc0K033lPnOjxlyXgFBBknK0,10573
|
13
14
|
liminal/cli/live_test_dropdown_migration.py,sha256=87JwxGD4A5ExjfsEEev9jgBNuldUj4CLoaFwsZ-BjEI,2889
|
14
15
|
liminal/cli/live_test_entity_schema_migration.py,sha256=_JawaiJNHkAjIrtwe9_K15OoJSFq_4vaTK5vrmz-b6s,5576
|
15
|
-
liminal/cli/utils.py,sha256=
|
16
|
-
liminal/connection/__init__.py,sha256=
|
16
|
+
liminal/cli/utils.py,sha256=vm35SuYapw7Kl8vZI5Kc9taE_WNvpsf7FF8QnVsR7yQ,4588
|
17
|
+
liminal/connection/__init__.py,sha256=60SyUzDxiTv9wn7cNyoqB4b9BuI3E6susqanYGvLnG8,212
|
17
18
|
liminal/connection/benchling_connection.py,sha256=PLiaI4v9EcJDNBEO5ZF3jERuI6AYo8NAYpqPYLxVYdQ,3237
|
18
|
-
liminal/connection/benchling_service.py,sha256=
|
19
|
+
liminal/connection/benchling_service.py,sha256=v9sKYvwW6NX53a4OBpKJAmb4fZWaUN5AvcY2N_oArZU,12070
|
19
20
|
liminal/dropdowns/api.py,sha256=n5oxi1EhkmpmPpNi1LOI4xcIQmk1C069XFaGP5XSBx8,6959
|
20
21
|
liminal/dropdowns/compare.py,sha256=-UbCkeTKx3THwvjMTUubyYVXBkhmvyhEKzwrIzBkthY,7141
|
21
|
-
liminal/dropdowns/generate_files.py,sha256=
|
22
|
+
liminal/dropdowns/generate_files.py,sha256=Y6biJOrt1XtWcTMoIV_eC2Yqwg6JYQxyF85U78s6M7U,2051
|
22
23
|
liminal/dropdowns/operations.py,sha256=-TRIsxqnUtrIUjhrt5k_PdiBCDUXsXDzsOUmznJE-6Q,13516
|
23
24
|
liminal/dropdowns/utils.py,sha256=1-H7bTszCUeqeRBpiYXjRjreDzhn1Fd1MFwIsrEI-o4,4109
|
24
25
|
liminal/entity_schemas/api.py,sha256=Emn_Y95cAG9Wis6tpchw6QBVKQh4If86LOdgKk0Ndjw,3575
|
25
26
|
liminal/entity_schemas/compare.py,sha256=t6tl67GWaMoNNcPxyLpCuNAlN3OWNqURTo3EUEMtETE,17549
|
26
27
|
liminal/entity_schemas/entity_schema_models.py,sha256=v5A1ELaiuBnUSl1HkUNAeMuIRQeQnIKzfpFxmsiKWh0,8349
|
27
|
-
liminal/entity_schemas/generate_files.py,sha256=
|
28
|
-
liminal/entity_schemas/operations.py,sha256=
|
29
|
-
liminal/entity_schemas/tag_schema_models.py,sha256=
|
30
|
-
liminal/entity_schemas/utils.py,sha256=
|
28
|
+
liminal/entity_schemas/generate_files.py,sha256=X-o6B2kSgVgJG9Nkc0fCEO60yfqveJepsXgEWvVQI2A,9040
|
29
|
+
liminal/entity_schemas/operations.py,sha256=N58H8LP1fuRCjRecG6G43ZdAX7MEL2lxPLQPmc23Wlw,27119
|
30
|
+
liminal/entity_schemas/tag_schema_models.py,sha256=HCtiJPMsPksX3wOBpfDdyIElNH3XybM7w25OUpIYoWM,24242
|
31
|
+
liminal/entity_schemas/utils.py,sha256=ngWqBq9RKVjHAODok85yeuReHXaqmrexHiSmyVx30Vg,6244
|
31
32
|
liminal/enums/__init__.py,sha256=-szuqAwMED4ai0NaPVUfgihQJAJ27wPu_nDnj4cEgTk,518
|
32
33
|
liminal/enums/benchling_api_field_type.py,sha256=0QamSWEMnxZtedZXlh6zNhSRogS9ZqvWskdHHN19xJo,633
|
33
34
|
liminal/enums/benchling_entity_type.py,sha256=H_6ZlHJsiVNMpezPBrNKo2eP0pDrt--HU-P7PgznaMA,846
|
@@ -39,11 +40,11 @@ liminal/enums/name_template_part_type.py,sha256=Z3Zv5PpzoUrIj_EvwPVgDDkY2G0kO-wE
|
|
39
40
|
liminal/enums/sequence_constraint.py,sha256=CT3msm8qzJpcivfbQZ3NOWNRsedH4mSlfhzvQBLrHWA,407
|
40
41
|
liminal/external/__init__.py,sha256=EundQBe68_ZIhcsuSOhc-CznzYauNDYlNG1CjRDui_Y,1348
|
41
42
|
liminal/mappers.py,sha256=TgPMQsLrESAI6D7KBl0UoBBpnxYgcgGOT7a2faWsuhY,9587
|
42
|
-
liminal/migrate/components.py,sha256=
|
43
|
+
liminal/migrate/components.py,sha256=bzt-5eJbhWVNs_zk9WieKmTkGgQrn58sm9GOuHUlM3Q,3526
|
43
44
|
liminal/migrate/revision.py,sha256=KppU0u-d0JsfPsXsmncxy9Q_XBJyf-o4e16wNZAJODM,7774
|
44
|
-
liminal/migrate/revisions_timeline.py,sha256=
|
45
|
+
liminal/migrate/revisions_timeline.py,sha256=FSKzjUnZwnD3v2o7qoilBUkmKmJVvLVqozo25WnTo-w,14456
|
45
46
|
liminal/orm/base.py,sha256=fFSpiNRYgK5UG7lbXdQGV8KgO8pwjMqt0pycM3rWJ2o,615
|
46
|
-
liminal/orm/base_model.py,sha256=
|
47
|
+
liminal/orm/base_model.py,sha256=ydUk_tno-iqPHL4N_yJLLxhbkZKE8_jy6hA7vdAz8uk,16897
|
47
48
|
liminal/orm/base_tables/registry_entity.py,sha256=4ET1cepTGjZ3AMFI5q-iMYxMObzXwuUDBD0jNNqCipE,2126
|
48
49
|
liminal/orm/base_tables/schema.py,sha256=7_btCVSUJxjVdGcKVRKL8sKcNw7-_gazTpfEh1jru3o,921
|
49
50
|
liminal/orm/base_tables/user.py,sha256=elRAHj7HgO3iVLK_pNCIwf_9Rl_9k6vkBgaYazoJSQc,818
|
@@ -51,20 +52,21 @@ liminal/orm/column.py,sha256=aK-MrKabOK5tf3UFPpRACq83YVVrjXITZF_rcOU-wPQ,6207
|
|
51
52
|
liminal/orm/mixins.py,sha256=yEeUDF1qEBLP523q8bZra4KtNVK0gwZN9mXJSNe3GEE,4802
|
52
53
|
liminal/orm/name_template.py,sha256=ftXZOiRR6gGGvGaZkFVDXKOboIHFWauhQENRguBGWMI,1739
|
53
54
|
liminal/orm/name_template_parts.py,sha256=iI4S9ZGvckQbYjhgFdn6xeJ3rS90lzCvjH0863HnAi8,3201
|
54
|
-
liminal/orm/relationship.py,sha256=
|
55
|
+
liminal/orm/relationship.py,sha256=Rn35E1tr4ZW6_-BINpdL20Sdvhb-hsUGzCQa9rv-E_I,4324
|
55
56
|
liminal/orm/schema_properties.py,sha256=vqqjnxbh7AYh9ZvSmhCsl69BqSBPpQutNKImb-TBCGg,4167
|
56
57
|
liminal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
58
|
+
liminal/tests/.DS_Store,sha256=0sTLf7flLKL2_3KGceYriAB8_gXTcYwn0c2RVSYmLZk,6148
|
57
59
|
liminal/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
58
60
|
liminal/tests/conftest.py,sha256=B463eOfe1uCHDJsUNvG-6tY8Qx8FJMByGDOtuyM87lA,17669
|
59
61
|
liminal/tests/from benchling_sdk.py,sha256=CjRUHFB3iaa4rUPLGOqDiBq5EPKldm-Fd8aQQr92zF4,147
|
60
62
|
liminal/tests/test_dropdown_compare.py,sha256=yHB0ovQlBLRu8-qYkqIPd8VtYEOmOft_93FQM86g_z8,8198
|
61
63
|
liminal/tests/test_entity_schema_compare.py,sha256=-26Bu5eYIuHRswB5kYjGDo5Wed5LUWjm1e6IRI1Q-lE,18952
|
62
64
|
liminal/unit_dictionary/utils.py,sha256=o3K06Yyt33iIUSMHPT8f1vSuUSgWjZLf51p78lx4SZs,1817
|
63
|
-
liminal/utils.py,sha256=
|
64
|
-
liminal/validation/__init__.py,sha256=
|
65
|
+
liminal/utils.py,sha256=Eu_o5qfx1Thy26UaDOL-QnrB67FeJf3kOrTavGNRSo0,3248
|
66
|
+
liminal/validation/__init__.py,sha256=NsjzmivSRwGki1k9ykJgjtcYNcILR_PHSf6RkI1w2n0,5290
|
65
67
|
liminal/validation/validation_severity.py,sha256=ib03PTZCQHcbBDc01v4gJF53YtA-ANY6QSFnhTV-FbU,259
|
66
|
-
liminal_orm-3.
|
67
|
-
liminal_orm-3.
|
68
|
-
liminal_orm-3.
|
69
|
-
liminal_orm-3.
|
70
|
-
liminal_orm-3.
|
68
|
+
liminal_orm-3.2.0.dist-info/LICENSE.md,sha256=oVA877F_D1AV44dpjsv4f-4k690uNGApX1EtzOo3T8U,11353
|
69
|
+
liminal_orm-3.2.0.dist-info/METADATA,sha256=040mqvMPY3eJJ2Rrkb6ZaVdIjtMjyor44wQjdcwGJdE,10981
|
70
|
+
liminal_orm-3.2.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
71
|
+
liminal_orm-3.2.0.dist-info/entry_points.txt,sha256=atIrU63rrzH81dWC2sjUbFLlc5FWMmYRdMxXEWexIZA,47
|
72
|
+
liminal_orm-3.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|