liminal-orm 4.1.1__tar.gz → 4.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. liminal_orm-4.2.0/.gitignore +20 -0
  2. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/PKG-INFO +21 -27
  3. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/base/base_dropdown.py +27 -0
  4. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/cli/cli.py +2 -2
  5. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/dropdowns/generate_files.py +14 -6
  6. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/dropdowns/utils.py +4 -1
  7. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/entity_schemas/generate_files.py +39 -33
  8. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/entity_schemas/utils.py +3 -0
  9. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/base_model.py +29 -0
  10. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/base_results_model.py +29 -0
  11. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/results_schemas/generate_files.py +24 -18
  12. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/results_schemas/utils.py +7 -9
  13. liminal_orm-4.2.0/pyproject.toml +81 -0
  14. liminal_orm-4.1.1/pyproject.toml +0 -66
  15. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/LICENSE.md +0 -0
  16. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/README.md +0 -0
  17. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/__init__.py +0 -0
  18. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/base/base_operation.py +0 -0
  19. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/base/base_validation_filters.py +0 -0
  20. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/base/compare_operation.py +0 -0
  21. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/base/name_template_parts.py +0 -0
  22. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/base/properties/base_field_properties.py +0 -0
  23. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/base/properties/base_name_template.py +0 -0
  24. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/base/properties/base_schema_properties.py +0 -0
  25. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/base/str_enum.py +0 -0
  26. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/cli/controller.py +0 -0
  27. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/cli/live_test_dropdown_migration.py +0 -0
  28. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/cli/live_test_entity_schema_migration.py +0 -0
  29. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/cli/utils.py +0 -0
  30. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/connection/__init__.py +0 -0
  31. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/connection/benchling_connection.py +0 -0
  32. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/connection/benchling_service.py +0 -0
  33. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/dropdowns/api.py +0 -0
  34. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/dropdowns/compare.py +0 -0
  35. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/dropdowns/operations.py +0 -0
  36. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/entity_schemas/api.py +0 -0
  37. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/entity_schemas/compare.py +0 -0
  38. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/entity_schemas/entity_schema_models.py +0 -0
  39. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/entity_schemas/operations.py +0 -0
  40. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/entity_schemas/tag_schema_models.py +0 -0
  41. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/enums/__init__.py +0 -0
  42. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/enums/benchling_api_field_type.py +0 -0
  43. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/enums/benchling_entity_type.py +0 -0
  44. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/enums/benchling_field_type.py +0 -0
  45. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/enums/benchling_folder_item_type.py +0 -0
  46. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/enums/benchling_naming_strategy.py +0 -0
  47. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/enums/benchling_sequence_type.py +0 -0
  48. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/enums/name_template_part_type.py +0 -0
  49. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/enums/sequence_constraint.py +0 -0
  50. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/external/__init__.py +0 -0
  51. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/mappers.py +0 -0
  52. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/migrate/components.py +0 -0
  53. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/migrate/revision.py +0 -0
  54. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/migrate/revisions_timeline.py +0 -0
  55. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/base.py +0 -0
  56. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/base_tables/registry_entity.py +0 -0
  57. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/base_tables/schema.py +0 -0
  58. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/base_tables/user.py +0 -0
  59. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/column.py +0 -0
  60. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/mixins.py +0 -0
  61. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/name_template.py +0 -0
  62. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/name_template_parts.py +0 -0
  63. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/relationship.py +0 -0
  64. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/results_schema_properties.py +0 -0
  65. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/orm/schema_properties.py +0 -0
  66. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/py.typed +0 -0
  67. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/results_schemas/models/results_schema_model.py +0 -0
  68. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/tests/__init__.py +0 -0
  69. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/tests/conftest.py +0 -0
  70. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/tests/from benchling_sdk.py +0 -0
  71. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/tests/test_dropdown_compare.py +0 -0
  72. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/tests/test_entity_schema_compare.py +0 -0
  73. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/unit_dictionary/utils.py +0 -0
  74. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/utils.py +0 -0
  75. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/validation/__init__.py +0 -0
  76. {liminal_orm-4.1.1 → liminal_orm-4.2.0}/liminal/validation/validation_severity.py +0 -0
@@ -0,0 +1,20 @@
1
+ # python generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ .mypy_cache
9
+ .ruff_cache
10
+ # venv
11
+ .venv
12
+
13
+ .DS_store
14
+ **/*.ipynb
15
+
16
+ # mkdocs documentation
17
+ /site
18
+
19
+ # for local testing
20
+ /test_liminal
@@ -1,32 +1,27 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: liminal-orm
3
- Version: 4.1.1
3
+ Version: 4.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
- Home-page: https://github.com/dynotx/liminal-orm
6
- Author: DynoTx Open Source
7
- Author-email: opensource@dynotx.com
8
- Requires-Python: >=3.9,<4
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.9
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Requires-Dist: benchling-sdk (>=1.21.2,<2.0.0)
15
- Requires-Dist: bs4 (>=0.0.2,<0.0.3)
16
- Requires-Dist: lxml (>=5.3.0,<6.0.0)
17
- Requires-Dist: numpy (>=1.23.5,<2.0.0)
18
- Requires-Dist: pandas (>=1.5.3,<2.0.0)
19
- Requires-Dist: psycopg2-binary (>=2.9.10,<3.0.0)
20
- Requires-Dist: pydantic (>=2,<=2.7)
21
- Requires-Dist: requests (>=2.32.3,<3.0.0)
22
- Requires-Dist: rich (>=13.9.2,<14.0.0)
23
- Requires-Dist: sqlalchemy (<2)
24
- Requires-Dist: tenacity (>=8,<10)
25
- Requires-Dist: tornado (==6.5.0)
26
- Requires-Dist: typer (>=0.12.5,<0.13.0)
27
- Project-URL: Bug Tracker, https://github.com/dynotx/liminal-orm/issues
28
- Project-URL: Documentation, https://dynotx.github.io/liminal-orm/
5
+ Project-URL: Homepage, https://github.com/dynotx/liminal-orm
29
6
  Project-URL: Repository, https://github.com/dynotx/liminal-orm
7
+ Project-URL: Documentation, https://dynotx.github.io/liminal-orm/
8
+ Project-URL: Bug Tracker, https://github.com/dynotx/liminal-orm/issues
9
+ Author-email: DynoTx Open Source <opensource@dynotx.com>
10
+ License-File: LICENSE.md
11
+ Requires-Python: <4,>=3.9
12
+ Requires-Dist: benchling-sdk<2,>=1.21.2
13
+ Requires-Dist: bs4<0.0.3,>=0.0.2
14
+ Requires-Dist: lxml<6,>=5.3.0
15
+ Requires-Dist: numpy<2,>=1.23.5
16
+ Requires-Dist: pandas<2,>=1.5.3
17
+ Requires-Dist: psycopg2-binary<3,>=2.9.10
18
+ Requires-Dist: pydantic<=2.7,>=2
19
+ Requires-Dist: requests<3,>=2.32.3
20
+ Requires-Dist: rich<14,>=13.9.2
21
+ Requires-Dist: sqlalchemy<2
22
+ Requires-Dist: tenacity<10,>=8
23
+ Requires-Dist: tornado==6.5.0
24
+ Requires-Dist: typer<0.13,>=0.12.5
30
25
  Description-Content-Type: text/markdown
31
26
 
32
27
  # [Liminal ORM](#liminal-orm)
@@ -152,4 +147,3 @@ ORM<sup>1</sup>: Object-Relational Mapper. An ORM is a piece of software designe
152
147
  LIMS<sup>2</sup>: Laboratory Information Management System. A LIMS is a piece of software that allows you to effectively manage samples and associated data. [Benchling](https://www.benchling.com/) is an industry-leading LIMS software.
153
148
 
154
149
  CLI<sup>3</sup>: Command Line Interface. A CLI is a piece of software that allows you to interact with a software program via the command line. Liminal provides a CLI that allows you to interact with your Liminal environment. This project uses [Typer](https://github.com/fastapi/typer) to construct the CLI
155
-
@@ -1,6 +1,9 @@
1
1
  from abc import ABC
2
2
  from typing import Any
3
3
 
4
+ from liminal.connection.benchling_service import BenchlingService
5
+ from liminal.dropdowns.utils import get_benchling_dropdown_summaries
6
+
4
7
 
5
8
  class BaseDropdown(ABC):
6
9
  """_summary_
@@ -35,6 +38,30 @@ class BaseDropdown(ABC):
35
38
  cls._existing_benchling_names.add(cls.__benchling_name__)
36
39
  return super().__new__(cls, **kwargs)
37
40
 
41
+ @classmethod
42
+ def get_id(cls, benchling_service: BenchlingService) -> str:
43
+ """Connects to Benchling and returns the id of the dropdown using the __benchling_name__.
44
+
45
+ Parameters
46
+ ----------
47
+ benchling_service : BenchlingService
48
+ The Benchling service to use.
49
+
50
+ Returns
51
+ -------
52
+ str
53
+ The id of the dropdown.
54
+ """
55
+ all_dropdowns = get_benchling_dropdown_summaries(benchling_service)
56
+ dropdowns_found_by_name = [
57
+ d for d in all_dropdowns if d.name == cls.__benchling_name__
58
+ ]
59
+ if len(dropdowns_found_by_name) == 0:
60
+ raise ValueError(f"No dropdowns found with name '{cls.__benchling_name__}'")
61
+ else:
62
+ dropdown = dropdowns_found_by_name[0]
63
+ return dropdown.id
64
+
38
65
  @classmethod
39
66
  def validate(cls, *values: str | None) -> None:
40
67
  err_values = []
@@ -83,7 +83,7 @@ connection = BenchlingConnection(
83
83
 
84
84
  @app.command(
85
85
  name="generate-files",
86
- help="Generates the dropdown, entity schema, and results schema files from your Benchling tenant and writes to the given path. By default, this will overwrite any existing files within the {write_path}/dropdowns/, {write_path}/entity_schemas/, and {write_path}/results_schemas/ directories.",
86
+ help="Generates the dropdown, entity schema, and results schema files from your Benchling tenant and writes to the given path. By default, this will not overwrite existing files.",
87
87
  )
88
88
  def generate_files(
89
89
  benchling_tenant: str = typer.Argument(
@@ -117,7 +117,7 @@ def generate_files(
117
117
  False,
118
118
  "-o",
119
119
  "--overwrite",
120
- help="Overwrite existing files within the {write_path}/dropdowns/, {write_path}/entity_schemas/, and {write_path}/results_schemas/ directories.",
120
+ help="Overwrite the whole write directory at the given path.",
121
121
  ),
122
122
  ) -> None:
123
123
  _, benchling_connection = read_local_liminal_dir(LIMINAL_DIR_PATH, benchling_tenant)
@@ -21,7 +21,7 @@ def generate_all_dropdown_files(
21
21
  write_path : Path
22
22
  The path to write the generated files to. dropdowns/ directory will be created within this path.
23
23
  overwrite : bool
24
- Whether to overwrite existing the existing dropdowns/ directory.
24
+ Whether to overwrite existing files in the dropdowns/ directory.
25
25
  """
26
26
  write_path = write_path / "dropdowns"
27
27
  if write_path.exists() and overwrite:
@@ -33,6 +33,7 @@ def generate_all_dropdown_files(
33
33
 
34
34
  dropdowns = get_benchling_dropdowns_dict(benchling_service)
35
35
  file_names_to_classname = []
36
+ num_files_written = 0
36
37
  for dropdown_name, dropdown_options in dropdowns.items():
37
38
  dropdown_values = [option.name for option in dropdown_options.options]
38
39
  options_list = str(dropdown_values).replace("'", '"')
@@ -46,8 +47,10 @@ class {classname}(BaseDropdown):
46
47
  __allowed_values__ = {options_list}
47
48
  """
48
49
  filename = to_snake_case(dropdown_name) + ".py"
49
- with open(write_path / filename, "w") as file:
50
- file.write(dropdown_content)
50
+ if overwrite or not (write_path / filename).exists():
51
+ with open(write_path / filename, "w") as file:
52
+ file.write(dropdown_content)
53
+ num_files_written += 1
51
54
  file_names_to_classname.append((filename, classname))
52
55
 
53
56
  file_names_to_classname.sort(key=lambda x: x[0])
@@ -55,8 +58,13 @@ class {classname}(BaseDropdown):
55
58
  f"from .{filename[:-3]} import {classname}"
56
59
  for filename, classname in file_names_to_classname
57
60
  )
58
- with open(write_path / "__init__.py", "w") as file:
59
- file.write(import_statements)
61
+ if num_files_written > 0:
62
+ with open(write_path / "__init__.py", "w") as file:
63
+ file.write(import_statements)
60
64
  print(
61
- f"[green]Generated {write_path / '__init__.py'} with {len(file_names_to_classname)} dropdown imports."
65
+ f"[green]Generated {write_path / '__init__.py'} with {len(file_names_to_classname)} dropdown imports. {num_files_written} dropdown files written."
66
+ )
67
+ else:
68
+ print(
69
+ "[green dim]No new dropdown files to be written. If you want to overwrite existing files, run with -o flag."
62
70
  )
@@ -1,3 +1,4 @@
1
+ from functools import lru_cache
1
2
  from typing import Any
2
3
 
3
4
  import requests
@@ -6,7 +7,6 @@ from benchling_sdk.models import Dropdown, DropdownOption, DropdownSummary
6
7
  from pydantic import BaseModel
7
8
 
8
9
  from liminal.connection import BenchlingService
9
- from liminal.orm.base_model import BaseModel as BenchlingBaseModel
10
10
 
11
11
 
12
12
  class ArchiveRecord(BaseModel):
@@ -22,6 +22,7 @@ def get_benchling_dropdown_id_name_map(
22
22
  return {d.id: d.name for d in get_benchling_dropdown_summaries(benchling_service)}
23
23
 
24
24
 
25
+ @lru_cache
25
26
  def get_benchling_dropdown_summaries(
26
27
  benchling_service: BenchlingService,
27
28
  ) -> list[DropdownSummary]:
@@ -110,6 +111,8 @@ def dropdown_exists_in_benchling(
110
111
 
111
112
 
112
113
  def get_schemas_with_dropdown(dropdown_name: str) -> list[str]:
114
+ from liminal.orm.base_model import BaseModel as BenchlingBaseModel
115
+
113
116
  schemas_with_dropdown = []
114
117
  for model in BenchlingBaseModel.get_all_subclasses():
115
118
  for props_dict in [
@@ -63,7 +63,7 @@ def generate_all_entity_schema_files(
63
63
  write_path : Path
64
64
  The path to write the generated files to. entity_schemas/ directory will be created within this path.
65
65
  overwrite : bool
66
- Whether to overwrite existing the existing entity_schemas/ directory.
66
+ Whether to overwrite existing files in the entity_schemas/ directory.
67
67
  """
68
68
  write_path = write_path / "entity_schemas"
69
69
  if write_path.exists() and overwrite:
@@ -76,6 +76,7 @@ def generate_all_entity_schema_files(
76
76
  models = get_converted_tag_schemas(benchling_service)
77
77
  has_date = False
78
78
  subdirectory_map: dict[str, list[tuple[str, str]]] = {}
79
+ subdirectory_num_files_written: dict[str, int] = {}
79
80
  dropdown_name_to_classname_map = _get_dropdown_name_to_classname_map(
80
81
  benchling_service
81
82
  )
@@ -188,39 +189,45 @@ class {classname}(BaseModel, {get_entity_mixin(schema_properties.entity_type)}):
188
189
  {init_string}
189
190
 
190
191
  """
191
- write_directory_path = write_path / get_file_subdirectory(
192
- schema_properties.entity_type
193
- )
194
- subdirectory_map[get_file_subdirectory(schema_properties.entity_type)] = (
195
- subdirectory_map.get(
196
- get_file_subdirectory(schema_properties.entity_type), []
197
- )
198
- + [(filename, classname)]
199
- )
192
+ subdirectory_name = get_file_subdirectory(schema_properties.entity_type)
193
+ write_directory_path = write_path / subdirectory_name
194
+ if not subdirectory_map.get(subdirectory_name):
195
+ subdirectory_map[subdirectory_name] = []
196
+ subdirectory_num_files_written[subdirectory_name] = 0
197
+ subdirectory_map[subdirectory_name].append((filename, classname))
200
198
  write_directory_path.mkdir(exist_ok=True)
201
- with open(write_directory_path / filename, "w") as file:
202
- file.write(full_content)
199
+ if overwrite or not (write_directory_path / filename).exists():
200
+ with open(write_directory_path / filename, "w") as file:
201
+ file.write(full_content)
202
+ subdirectory_num_files_written[subdirectory_name] += 1
203
203
 
204
204
  for subdir, names in subdirectory_map.items():
205
- init_content = (
206
- "\n".join(
207
- f"from .{filename[:-3]} import {classname}"
208
- for filename, classname in names
205
+ if subdirectory_num_files_written[subdir] > 0:
206
+ init_content = (
207
+ "\n".join(
208
+ f"from .{filename[:-3]} import {classname}"
209
+ for filename, classname in names
210
+ )
211
+ + "\n"
209
212
  )
210
- + "\n"
211
- )
212
- with open(write_path / subdir / "__init__.py", "w") as file:
213
- file.write(init_content)
214
-
215
- with open(write_path / "__init__.py", "w") as file:
216
- file.write(
217
- "\n".join(
218
- f"from .{subdir} import * # noqa" for subdir in subdirectory_map.keys()
213
+ with open(write_path / subdir / "__init__.py", "w") as file:
214
+ file.write(init_content)
215
+
216
+ if sum(subdirectory_num_files_written.values()) > 0:
217
+ with open(write_path / "__init__.py", "w") as file:
218
+ file.write(
219
+ "\n".join(
220
+ f"from .{subdir} import * # noqa"
221
+ for subdir in subdirectory_map.keys()
222
+ )
223
+ + "\n"
219
224
  )
220
- + "\n"
221
- )
225
+ print(
226
+ f"[green]Generated {write_path / '__init__.py'} with {sum(subdirectory_num_files_written.values())} entity schema files written."
227
+ )
228
+ else:
222
229
  print(
223
- f"[green]Generated {write_path / '__init__.py'} with {len(models)} entity schema imports."
230
+ "[green dim]No new entity schema files to be written. If you want to overwrite existing files, run with -o flag."
224
231
  )
225
232
 
226
233
 
@@ -237,8 +244,7 @@ def _get_dropdown_name_to_classname_map(
237
244
  for dropdown in BaseDropdown.get_all_subclasses()
238
245
  }
239
246
  benchling_dropdowns = get_benchling_dropdowns_dict(benchling_service)
240
- if len(benchling_dropdowns) > 0:
241
- raise Exception(
242
- "No dropdowns found locally. Please ensure your env.py file imports your dropdown classes or generate dropdowns from your Benchling tenant first."
243
- )
244
- return {}
247
+ return {
248
+ dropdown_name: to_pascal_case(dropdown_name)
249
+ for dropdown_name in benchling_dropdowns.keys()
250
+ }
@@ -1,3 +1,5 @@
1
+ from functools import lru_cache
2
+
1
3
  from benchling_sdk.models import EntitySchema
2
4
 
3
5
  from liminal.base.properties.base_field_properties import BaseFieldProperties
@@ -136,6 +138,7 @@ def convert_tag_schema_field_to_field_properties(
136
138
  )
137
139
 
138
140
 
141
+ @lru_cache
139
142
  def get_benchling_entity_schemas(
140
143
  benchling_service: BenchlingService,
141
144
  ) -> list[EntitySchema]:
@@ -13,6 +13,8 @@ from sqlalchemy.orm.decl_api import declared_attr
13
13
 
14
14
  from liminal.base.base_dropdown import BaseDropdown
15
15
  from liminal.base.base_validation_filters import BaseValidatorFilters
16
+ from liminal.connection.benchling_service import BenchlingService
17
+ from liminal.entity_schemas.utils import get_benchling_entity_schemas
16
18
  from liminal.enums import BenchlingNamingStrategy
17
19
  from liminal.enums.benchling_entity_type import BenchlingEntityType
18
20
  from liminal.enums.sequence_constraint import SequenceConstraint
@@ -279,6 +281,33 @@ class BaseModel(Generic[T], Base):
279
281
  query = query.filter(User.name.in_(base_filters.creator_full_names))
280
282
  return query
281
283
 
284
+ @classmethod
285
+ def get_id(cls, benchling_service: BenchlingService) -> str:
286
+ """Connects to Benchling and returns the id of the schema using the __schema_properties__.name.
287
+
288
+ Parameters
289
+ ----------
290
+ benchling_service : BenchlingService
291
+ The Benchling service to use.
292
+
293
+ Returns
294
+ -------
295
+ str
296
+ The id of the schema.
297
+ """
298
+ all_schemas = get_benchling_entity_schemas(benchling_service)
299
+
300
+ schemas_found_by_name = [
301
+ s for s in all_schemas if s.name == cls.__schema_properties__.name
302
+ ]
303
+ if len(schemas_found_by_name) == 0:
304
+ raise ValueError(
305
+ f"No schema found with name '{cls.__schema_properties__.name}'."
306
+ )
307
+ else:
308
+ schema = schemas_found_by_name[0]
309
+ return schema.id
310
+
282
311
  @classmethod
283
312
  def all(cls, session: Session) -> list[T]:
284
313
  """Uses the get_query method to retrieve all entities from the database.
@@ -12,9 +12,11 @@ from sqlalchemy.orm import Query, RelationshipProperty, Session, relationship
12
12
  from sqlalchemy.orm.decl_api import declared_attr
13
13
 
14
14
  from liminal.base.base_validation_filters import BaseValidatorFilters
15
+ from liminal.connection.benchling_service import BenchlingService
15
16
  from liminal.orm.base import Base
16
17
  from liminal.orm.base_tables.user import User
17
18
  from liminal.orm.results_schema_properties import ResultsSchemaProperties
19
+ from liminal.results_schemas.utils import get_benchling_results_schemas
18
20
  from liminal.validation import BenchlingValidatorReport
19
21
 
20
22
  T = TypeVar("T")
@@ -83,6 +85,33 @@ class BaseResultsModel(Generic[T], Base):
83
85
  query = query.filter(User.name.in_(base_filters.creator_full_names))
84
86
  return query
85
87
 
88
+ @classmethod
89
+ def get_id(cls, benchling_service: BenchlingService) -> str:
90
+ """Connects to Benchling and returns the id of the results schema using the __schema_properties__.name.
91
+
92
+ Parameters
93
+ ----------
94
+ benchling_service : BenchlingService
95
+ The Benchling service to use.
96
+
97
+ Returns
98
+ -------
99
+ str
100
+ The id of the results schema.
101
+ """
102
+ all_schemas = get_benchling_results_schemas(benchling_service)
103
+
104
+ schemas_found_by_name = [
105
+ s for s in all_schemas if s.name == cls.__schema_properties__.name
106
+ ]
107
+ if len(schemas_found_by_name) == 0:
108
+ raise ValueError(
109
+ f"No results schema found with name '{cls.__schema_properties__.name}'."
110
+ )
111
+ else:
112
+ schema = schemas_found_by_name[0]
113
+ return schema.id
114
+
86
115
  @classmethod
87
116
  def all(cls, session: Session) -> list[T]:
88
117
  """Uses the get_query method to retrieve all results schema rows from the database.
@@ -29,7 +29,7 @@ def generate_all_results_schema_files(
29
29
  write_path : Path
30
30
  The path to write the generated files to. results_schemas/ directory will be created within this path.
31
31
  overwrite : bool
32
- Whether to overwrite existing the existing results_schemas/ directory.
32
+ Whether to overwrite existing files in the results_schemas/ directory.
33
33
  """
34
34
  write_path = write_path / "results_schemas"
35
35
  if write_path.exists() and overwrite:
@@ -47,6 +47,7 @@ def generate_all_results_schema_files(
47
47
  benchling_service
48
48
  )
49
49
  init_file_imports = []
50
+ num_files_written = 0
50
51
 
51
52
  for schema_properties, field_properties_dict in results_schemas:
52
53
  has_date = False
@@ -147,14 +148,21 @@ class {schema_name}(BaseResultsModel):
147
148
  {init_string}
148
149
  """
149
150
 
150
- with open(write_path / file_name, "w") as file:
151
- file.write(schema_content)
151
+ if overwrite or not (write_path / file_name).exists():
152
+ with open(write_path / file_name, "w") as file:
153
+ file.write(schema_content)
154
+ num_files_written += 1
152
155
 
153
- with open(write_path / "__init__.py", "w") as file:
154
- file.write("\n".join(init_file_imports))
155
- print(
156
- f"[green]Generated {write_path / '__init__.py'} with {len(results_schemas)} entity schema imports."
157
- )
156
+ if num_files_written > 0:
157
+ with open(write_path / "__init__.py", "w") as file:
158
+ file.write("\n".join(init_file_imports))
159
+ print(
160
+ f"[green]Generated {write_path / '__init__.py'} with {len(results_schemas)} entity schema imports. {num_files_written} results schema files written."
161
+ )
162
+ else:
163
+ print(
164
+ "[green dim]No new results schema files to be written. If you want to overwrite existing files, run with -o flag."
165
+ )
158
166
 
159
167
 
160
168
  def _get_dropdown_name_to_classname_map(
@@ -170,11 +178,10 @@ def _get_dropdown_name_to_classname_map(
170
178
  for dropdown in BaseDropdown.get_all_subclasses()
171
179
  }
172
180
  benchling_dropdowns = get_benchling_dropdowns_dict(benchling_service)
173
- if len(benchling_dropdowns) > 0:
174
- raise Exception(
175
- "No dropdowns found locally. Please ensure your env.py file imports your dropdown classes or generate dropdowns from your Benchling tenant first."
176
- )
177
- return {}
181
+ return {
182
+ dropdown_name: to_pascal_case(dropdown_name)
183
+ for dropdown_name in benchling_dropdowns.keys()
184
+ }
178
185
 
179
186
 
180
187
  def _get_entity_schemas_wh_name_to_classname(
@@ -190,8 +197,7 @@ def _get_entity_schemas_wh_name_to_classname(
190
197
  for s in BaseModel.get_all_subclasses()
191
198
  }
192
199
  tag_schemas = get_converted_tag_schemas(benchling_service)
193
- if len(tag_schemas) > 0:
194
- raise Exception(
195
- "No entity schemas found locally. Please ensure your env.py file imports your entity schema classes or generate entity schemas from your Benchling tenant first."
196
- )
197
- return {}
200
+ return {
201
+ schema_props.warehouse_name: to_pascal_case(schema_props.warehouse_name)
202
+ for schema_props, _, _ in tag_schemas
203
+ }
@@ -1,3 +1,5 @@
1
+ from functools import lru_cache
2
+
1
3
  from benchling_api_client.v2.stable.models.assay_result_schema import AssayResultSchema
2
4
 
3
5
  from liminal.base.properties.base_field_properties import BaseFieldProperties
@@ -35,14 +37,10 @@ def get_converted_results_schemas(
35
37
  return results_schemas_list
36
38
 
37
39
 
38
- def get_results_schemas_dict(
40
+ @lru_cache
41
+ def get_benchling_results_schemas(
39
42
  benchling_service: BenchlingService,
40
- ) -> dict[str, AssayResultSchema]:
41
- """This function gets all Results Schema schemas using the Benchling API and returns a dictionary of the schemas by their system name."""
42
- flattened_schemas = [
43
- s
44
- for schemas in list(benchling_service.schemas.list_assay_result_schemas())
45
- for s in schemas
43
+ ) -> list[AssayResultSchema]:
44
+ return [
45
+ s for loe in benchling_service.schemas.list_assay_result_schemas() for s in loe
46
46
  ]
47
- schemas_dict = {s.system_name: s for s in flattened_schemas}
48
- return schemas_dict
@@ -0,0 +1,81 @@
1
+ [project]
2
+ name = "liminal-orm"
3
+ version = "4.2.0"
4
+ description = "An ORM and toolkit that builds on top of Benchling's platform to keep your schemas and downstream code dependencies in sync."
5
+ authors = [{ name = "DynoTx Open Source", email = "opensource@dynotx.com" }]
6
+ requires-python = ">=3.9,<4"
7
+ readme = "README.md"
8
+ dependencies = [
9
+ "benchling-sdk>=1.21.2,<2",
10
+ "bs4>=0.0.2,<0.0.3",
11
+ "numpy>=1.23.5,<2",
12
+ "pandas>=1.5.3,<2",
13
+ "pydantic>=2,<=2.7",
14
+ "requests>=2.32.3,<3",
15
+ "rich>=13.9.2,<14",
16
+ "sqlalchemy<2",
17
+ "tenacity>=8,<10",
18
+ "typer>=0.12.5,<0.13",
19
+ "lxml>=5.3.0,<6",
20
+ "psycopg2-binary>=2.9.10,<3",
21
+ "tornado==6.5.0",
22
+ ]
23
+
24
+ [virtualenvs]
25
+ in-project = true
26
+
27
+ [[tool.uv.index]]
28
+ name = "testpypi"
29
+ url = "https://test.pypi.org/simple/"
30
+ publish-url = "https://test.pypi.org/legacy/"
31
+ explicit = true
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/dynotx/liminal-orm"
35
+ Repository = "https://github.com/dynotx/liminal-orm"
36
+ Documentation = "https://dynotx.github.io/liminal-orm/"
37
+ "Bug Tracker" = "https://github.com/dynotx/liminal-orm/issues"
38
+
39
+ [project.scripts]
40
+ liminal = "liminal.cli.cli:app"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "ipykernel>=6.29.5,<7",
45
+ "mkdocs-material>=9.5.41,<10",
46
+ "mypy>=1.1.1,<2",
47
+ "pre-commit>=4.0.1,<5",
48
+ "pytest>=8.3.3,<9",
49
+ "ruff>=0.6.9,<0.7",
50
+ "sqlalchemy==1.4.49",
51
+ "types-beautifulsoup4>=4.12.0.20240907,<5",
52
+ "types-requests>=2.32.0.20240914,<3",
53
+ ]
54
+
55
+ [tool.hatch.build.targets.sdist]
56
+ include = ["liminal"]
57
+
58
+ [tool.hatch.build.targets.wheel]
59
+ include = ["liminal"]
60
+
61
+ [build-system]
62
+ requires = ["hatchling"]
63
+ build-backend = "hatchling.build"
64
+
65
+ [tool.mypy]
66
+ check_untyped_defs = true
67
+ disallow_untyped_defs = true
68
+ explicit_package_bases = true
69
+ mypy_path = "$MYPY_CONFIG_FILE_DIR/"
70
+ namespace_packages = true
71
+ no_implicit_optional = true
72
+ strict_optional = true
73
+ packages = "liminal"
74
+ plugins = ["pydantic.mypy", "numpy.typing.mypy_plugin"]
75
+ show_error_codes = true
76
+ warn_unused_ignores = true
77
+ disable_error_code=["union-attr"]
78
+
79
+ [[tool.mypy.overrides]]
80
+ module = ["sqlalchemy.*"]
81
+ ignore_missing_imports = true
@@ -1,66 +0,0 @@
1
- [tool.poetry]
2
- name = "liminal-orm"
3
- version = "4.1.1"
4
- description = "An ORM and toolkit that builds on top of Benchling's platform to keep your schemas and downstream code dependencies in sync."
5
- authors = ["DynoTx Open Source <opensource@dynotx.com>"]
6
- readme = "README.md"
7
- homepage = "https://github.com/dynotx/liminal-orm"
8
- repository = "https://github.com/dynotx/liminal-orm"
9
- documentation = "https://dynotx.github.io/liminal-orm/"
10
- packages = [{include = "liminal"}]
11
-
12
- [tool.poetry.urls]
13
- "Bug Tracker" = "https://github.com/dynotx/liminal-orm/issues"
14
-
15
- [tool.poetry.dependencies]
16
- python = ">=3.9,<4"
17
- benchling-sdk = "^1.21.2"
18
- bs4 = "^0.0.2"
19
- numpy = "^1.23.5"
20
- pandas = "^1.5.3"
21
- pydantic = ">=2,<=2.7" # TODO: We need to assess this upgrade
22
- requests = "^2.32.3"
23
- rich = "^13.9.2"
24
- sqlalchemy = "<2" # TODO: We need to assess this upgrade
25
- tenacity = ">=8,<10"
26
- typer = "^0.12.5"
27
- lxml = "^5.3.0"
28
- psycopg2-binary = "^2.9.10"
29
- tornado = "6.5.0"
30
-
31
- [tool.poetry.group.dev.dependencies]
32
- ipykernel = "^6.29.5"
33
- mkdocs-material = "^9.5.41"
34
- mypy = "^1.1.1"
35
- pre-commit = "^4.0.1"
36
- pytest = "^8.3.3"
37
- ruff = "^0.6.9"
38
- sqlalchemy = "1.4.49"
39
- types-beautifulsoup4 = "^4.12.0.20240907"
40
- types-requests = "^2.32.0.20240914"
41
-
42
-
43
- [build-system]
44
- requires = ["poetry-core"]
45
- build-backend = "poetry.core.masonry.api"
46
-
47
- [tool.poetry.scripts]
48
- liminal = "liminal.cli.cli:app"
49
-
50
- [tool.mypy]
51
- check_untyped_defs = true
52
- disallow_untyped_defs = true
53
- explicit_package_bases = true
54
- mypy_path = "$MYPY_CONFIG_FILE_DIR/"
55
- namespace_packages = true
56
- no_implicit_optional = true
57
- strict_optional = true
58
- packages = "liminal"
59
- plugins = ["pydantic.mypy", "numpy.typing.mypy_plugin"]
60
- show_error_codes = true
61
- warn_unused_ignores = true
62
- disable_error_code=["union-attr"]
63
-
64
- [[tool.mypy.overrides]]
65
- module = ["sqlalchemy.*"]
66
- ignore_missing_imports = true
File without changes
File without changes