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 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
- DeprecationWarning,
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 BenchlingService
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
- revision_id = new_revision_file_path.name.split("_")[0]
58
- env_file = f"""# This file is auto-generated by Liminal.
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
- # There must be a revision_id variable defined for each BenchlingConnection instance. The variable name should match the `current_revision_id_var_name` variable passed into the BenchlingConnection instance.
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
- CURRENT_REVISION_ID = "{revision_id}"
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
- current_revision_id, benchling_connection = read_local_liminal_dir(
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 CURRENT_REVISION_ID that your Benchling tenant is on. Reads from liminal/env.py.",
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
- print(
112
- f"[blue]{benchling_connection.current_revision_id_var_name}: {current_revision_id}[/blue]"
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="autogenerate",
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 autogenerate(
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, VERSIONS_DIR_PATH, description, current_revision_id
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 CURRENT_REVISION_ID and the passed in parameters. Runs the upgrade operations of each revision file in order.",
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
- update_env_revision_id(ENV_FILE_PATH, benchling_tenant, upgrade_revision_id)
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 {benchling_tenant}_CURRENT_REVISION_ID to {upgrade_revision_id} in liminal/env.py"
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
- update_env_revision_id(ENV_FILE_PATH, benchling_tenant, downgrade_revision_id)
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 {benchling_tenant}_CURRENT_REVISION_ID to {downgrade_revision_id} in liminal/env.py"
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
- current_revision_id, benchling_connection = read_local_liminal_dir(
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 latest revision ({revision_timeline.get_latest_revision().id}). Please upgrade to the latest revision before generating a new revision."
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
- compare_ops = get_full_migration_operations(benchling_service)
54
- write_path = revision_timeline.write_new_revision(description, compare_ops)
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
- print(f"[bold green]Revision file generated at {write_path}")
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:
@@ -1,2 +1,5 @@
1
- from liminal.connection.benchling_connection import BenchlingConnection # noqa: F401
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 pascalize, to_snake_case
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 = pascalize(dropdown_name)
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 pascalize, to_snake_case
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: pascalize(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: pascalize(sp.name) for sp, _, _ in models
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 = pascalize(schema_properties.name)
70
+ classname = to_pascal_case(schema_properties.name)
71
71
 
72
72
  for schema_properties, name_template, columns in models:
73
- classname = pascalize(schema_properties.name)
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]}", "{classname}", "{col_name}")"""
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.field_props.name)
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.field_props.name)}."
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 | None
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
- self.sqlIdentifier = (
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
 
@@ -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
@@ -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(f"[bold red]Error executing operation {o.__class__.__name__}: {e}]")
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 | None:
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.validate_column_definition(wh_name)
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(f"Invalid field properties: {' '.join(errors)}")
246
+ raise ValueError(
247
+ f"Invalid field properties for schema {cls.__tablename__}: {' '.join(errors)}"
248
+ )
226
249
  return True
227
250
 
228
251
  @classmethod
@@ -1,5 +1,9 @@
1
- from sqlalchemy.orm import RelationshipProperty, relationship
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
- """Wrapper for SQLAlchemy's relationship function. Liminal's recommendation for defining a relationship from
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
- Class name of the current class that this relationship is being defined on.
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
- if target_class_name == current_class_name:
53
- return relationship(
54
- target_class_name,
55
- primaryjoin=f"remote({target_class_name}.id) == any_(foreign({current_class_name}.{entity_link_field_name}))",
56
- uselist=True,
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
- return relationship(
59
- target_class_name,
60
- primaryjoin=f"{target_class_name}.id == any_(foreign({current_class_name}.{entity_link_field_name}))",
61
- uselist=True,
62
- )
112
+ return linked_entities
113
+
114
+ return property(getter)
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 pascalize(input_string: str) -> str:
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
- return "".join(
22
- re.sub(r"[\[\]{}():]", "", word).capitalize()
23
- for word in re.split(r"[ /_\-]", input_string)
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 | None) -> 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
- if input_string is None:
32
- return ""
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) and len(prefix) <= 32
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}'. It should only contain alphabetic characters or underscores."
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
 
@@ -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 pascalize
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 pascalize(func.__name__),
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 pascalize(func.__name__),
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.1.0
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=dJeyrhhhZHDwKXe_p7AtgDlbZlzsnYQ8FoM8FXVF7q0,271
7
- liminal/base/properties/base_field_properties.py,sha256=TqHLJnRx7EK7grzad6MOHpGJeZr0Srm9bPhshHslJRk,5047
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=11QaxtUoHZx25QFv7HP8gQBpknrFthVeyWuMnI_lV5g,5891
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=30ZCkbQUY8p9WLS6OIBWgig0hGM2QlMQweHMGQe1gwQ,9119
12
- liminal/cli/controller.py,sha256=QNj3QO9TMb9hfc6U-VhLuFa0_aohOHZUmvY4XkATPhw,10118
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=hiBDeCDMjXdGnzjA7kwC9pYgNhSyF-oK_IicdO5u3U0,4478
16
- liminal/connection/__init__.py,sha256=3z4pSANIOkc9mh1Xp763oYQuJZDEh4lauN901PU4vqI,166
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=lEYCHF1U8nII8Rn3rMBPTffTFiVFjoFeNmX2Kq36-qE,7170
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=IqnBs-IyLsIZE0NUkdB99zd5EAF-1f9CPBeblz-GzJE,2041
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=u9SoDO9f4qL2nZaddln__2J0zJ3QMFBQhiUabn22aUY,9032
28
- liminal/entity_schemas/operations.py,sha256=cMJ5P1drs-Gv9E3YbsT3jTe48wJswxgPWYdV1qSawq0,26952
29
- liminal/entity_schemas/tag_schema_models.py,sha256=DZQYzlxt3aEHbLy00qEyDZC_mRyi9I325ggkfcNgR1I,24153
30
- liminal/entity_schemas/utils.py,sha256=2ZHyLxnYITVEuyAWxNdsq5hcNSgvN7pN3-uUzyocYSk,6161
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=2HuFp5KDNhofROMRI-BioUoA4CCjhQ_v_F0QmGJzUBU,3480
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=G9VwxPrLhLqKOrIXyxrXyHpujc-72m7omsZjI5-0D0M,14520
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=goJY17riXGqAP3hobHqa-koe8_EijMZ_LgZSXgD69vc,15576
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=Zl4bMHbtDSPx1psGHYnojGGJpA8B8hwcPJdgjB1lmW0,2490
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=radRtRsZmCiNblMvxOX1DH0rcO5TR09kFlp6OONIPBU,2951
64
- liminal/validation/__init__.py,sha256=TVaHrSF3GnSd4mbZrPn8TBHscGWkAPKAUUPq7-symC8,5275
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.1.0.dist-info/LICENSE.md,sha256=oVA877F_D1AV44dpjsv4f-4k690uNGApX1EtzOo3T8U,11353
67
- liminal_orm-3.1.0.dist-info/METADATA,sha256=jC90ZQqaPXCKH8dvJ4oyA5HjPG-dAX3gnPDCEWLfsIk,11032
68
- liminal_orm-3.1.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
69
- liminal_orm-3.1.0.dist-info/entry_points.txt,sha256=atIrU63rrzH81dWC2sjUbFLlc5FWMmYRdMxXEWexIZA,47
70
- liminal_orm-3.1.0.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any