liminal-orm 4.5.0__tar.gz → 4.6.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 (88) hide show
  1. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/PKG-INFO +1 -1
  2. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/connection/benchling_service.py +50 -26
  3. liminal_orm-4.6.0/liminal/entity_schemas/api_v3.py +160 -0
  4. liminal_orm-4.6.0/liminal/entity_schemas/entity_schema_models_v3.py +86 -0
  5. liminal_orm-4.6.0/liminal/enums/benchling_api_entity_type.py +18 -0
  6. liminal_orm-4.6.0/liminal/tests/test_benchling_service.py +34 -0
  7. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/pyproject.toml +1 -1
  8. liminal_orm-4.5.0/liminal/.gitignore +0 -1
  9. liminal_orm-4.5.0/liminal/.lock +0 -0
  10. liminal_orm-4.5.0/liminal/CACHEDIR.TAG +0 -1
  11. liminal_orm-4.5.0/liminal/pyvenv.cfg +0 -5
  12. liminal_orm-4.5.0/liminal/share/jupyter/kernels/python3/kernel.json +0 -14
  13. liminal_orm-4.5.0/liminal/share/jupyter/kernels/python3/logo-32x32.png +0 -0
  14. liminal_orm-4.5.0/liminal/share/jupyter/kernels/python3/logo-64x64.png +0 -0
  15. liminal_orm-4.5.0/liminal/share/jupyter/kernels/python3/logo-svg.svg +0 -265
  16. liminal_orm-4.5.0/liminal/share/man/man1/ipython.1 +0 -60
  17. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/.gitignore +0 -0
  18. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/LICENSE.md +0 -0
  19. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/README.md +0 -0
  20. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/__init__.py +0 -0
  21. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/base/base_dropdown.py +0 -0
  22. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/base/base_operation.py +0 -0
  23. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/base/base_validation_filters.py +0 -0
  24. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/base/compare_operation.py +0 -0
  25. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/base/name_template_parts.py +0 -0
  26. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/base/properties/base_field_properties.py +0 -0
  27. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/base/properties/base_name_template.py +0 -0
  28. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/base/properties/base_schema_properties.py +0 -0
  29. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/base/str_enum.py +0 -0
  30. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/cli/cli.py +0 -0
  31. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/cli/controller.py +0 -0
  32. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/cli/live_test_dropdown_migration.py +0 -0
  33. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/cli/live_test_entity_schema_migration.py +0 -0
  34. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/cli/utils.py +0 -0
  35. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/connection/__init__.py +0 -0
  36. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/connection/benchling_connection.py +0 -0
  37. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/dropdowns/api.py +0 -0
  38. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/dropdowns/compare.py +0 -0
  39. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/dropdowns/generate_files.py +0 -0
  40. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/dropdowns/operations.py +0 -0
  41. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/dropdowns/utils.py +0 -0
  42. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/entity_schemas/api.py +0 -0
  43. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/entity_schemas/compare.py +0 -0
  44. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/entity_schemas/entity_schema_models.py +0 -0
  45. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/entity_schemas/generate_files.py +0 -0
  46. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/entity_schemas/operations.py +0 -0
  47. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/entity_schemas/tag_schema_models.py +0 -0
  48. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/entity_schemas/utils.py +0 -0
  49. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/enums/__init__.py +0 -0
  50. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/enums/benchling_api_field_type.py +0 -0
  51. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/enums/benchling_entity_type.py +0 -0
  52. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/enums/benchling_field_type.py +0 -0
  53. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/enums/benchling_folder_item_type.py +0 -0
  54. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/enums/benchling_naming_strategy.py +0 -0
  55. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/enums/benchling_sequence_type.py +0 -0
  56. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/enums/name_template_part_type.py +0 -0
  57. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/enums/sequence_constraint.py +0 -0
  58. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/external/__init__.py +0 -0
  59. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/mappers.py +0 -0
  60. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/migrate/components.py +0 -0
  61. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/migrate/revision.py +0 -0
  62. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/migrate/revisions_timeline.py +0 -0
  63. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/base.py +0 -0
  64. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/base_model.py +0 -0
  65. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/base_results_model.py +0 -0
  66. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/base_tables/registry_entity.py +0 -0
  67. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/base_tables/schema.py +0 -0
  68. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/base_tables/user.py +0 -0
  69. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/column.py +0 -0
  70. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/mixins.py +0 -0
  71. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/name_template.py +0 -0
  72. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/name_template_parts.py +0 -0
  73. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/relationship.py +0 -0
  74. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/results_schema_properties.py +0 -0
  75. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/orm/schema_properties.py +0 -0
  76. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/py.typed +0 -0
  77. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/results_schemas/generate_files.py +0 -0
  78. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/results_schemas/models/results_schema_model.py +0 -0
  79. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/results_schemas/utils.py +0 -0
  80. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/tests/__init__.py +0 -0
  81. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/tests/conftest.py +0 -0
  82. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/tests/from benchling_sdk.py +0 -0
  83. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/tests/test_dropdown_compare.py +0 -0
  84. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/tests/test_entity_schema_compare.py +0 -0
  85. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/unit_dictionary/utils.py +0 -0
  86. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/utils.py +0 -0
  87. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/validation/__init__.py +0 -0
  88. {liminal_orm-4.5.0 → liminal_orm-4.6.0}/liminal/validation/validation_severity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liminal-orm
3
- Version: 4.5.0
3
+ Version: 4.6.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
  Project-URL: Homepage, https://github.com/dynotx/liminal-orm
6
6
  Project-URL: Repository, https://github.com/dynotx/liminal-orm
@@ -2,11 +2,12 @@ import asyncio
2
2
  import logging
3
3
  import os
4
4
  from typing import Any
5
+ from urllib.parse import urlparse
5
6
 
6
- from playwright.async_api import async_playwright
7
+ from playwright.async_api import async_playwright, Request
7
8
  import requests
8
9
  from benchling_sdk.auth.client_credentials_oauth2 import ClientCredentialsOAuth2
9
- from benchling_sdk.benchling import Benchling
10
+ from benchling_sdk.benchling import Benchling, BenchlingApiClientDecorator
10
11
  from benchling_sdk.helpers.retry_helpers import RetryStrategy
11
12
  from bs4 import BeautifulSoup
12
13
  from sqlalchemy import create_engine
@@ -54,6 +55,10 @@ class BenchlingService(Benchling):
54
55
  Whether to connect to the Benchling Postgres database. Requires warehouse_connection_string from the connection object.
55
56
  use_internal_api: bool = False
56
57
  Whether to connect to the Benchling internal API. Requires internal_api_admin_email and internal_api_admin_password from the connection object.
58
+ client_decorator: BenchlingApiClientDecorator | None = None
59
+ An optional function that receives the default BenchlingApiClient and returns a
60
+ customized one. Forwarded to the Benchling SDK. A common use is raising the HTTP
61
+ timeout, which otherwise defaults to 10 seconds: ``lambda c: c.with_timeout(60)``.
57
62
  """
58
63
 
59
64
  def __init__(
@@ -62,6 +67,7 @@ class BenchlingService(Benchling):
62
67
  use_api: bool = True,
63
68
  use_db: bool = False,
64
69
  use_internal_api: bool = False,
70
+ client_decorator: BenchlingApiClientDecorator | None = None,
65
71
  ) -> None:
66
72
  self.connection = connection
67
73
  self._session: Session | None = None
@@ -76,7 +82,10 @@ class BenchlingService(Benchling):
76
82
  )
77
83
  url = f"https://{connection.tenant_name}.benchling.com"
78
84
  super().__init__(
79
- url=url, auth_method=auth_method, retry_strategy=retry_strategy
85
+ url=url,
86
+ auth_method=auth_method,
87
+ retry_strategy=retry_strategy,
88
+ client_decorator=client_decorator,
80
89
  )
81
90
  LOGGER.info(f"Tenant {connection.tenant_name}: Connected to Benchling API.")
82
91
  self.use_db = use_db
@@ -96,28 +105,23 @@ class BenchlingService(Benchling):
96
105
  self.use_internal_api = use_internal_api
97
106
  if use_internal_api:
98
107
  try:
99
- authenticated_session = asyncio.get_event_loop().run_until_complete(
100
- self.autogenerate_auth(
101
- connection.tenant_name,
102
- connection.internal_api_admin_email,
103
- connection.internal_api_admin_password,
104
- connection.playwright_data_dir,
105
- )
108
+ authenticated_session, csrf_token = self.autogenerate_auth(
109
+ connection.tenant_name,
110
+ connection.internal_api_admin_email,
111
+ connection.internal_api_admin_password,
112
+ connection.playwright_data_dir,
106
113
  )
107
114
  except SSODisabledError as e:
108
115
  raise SSODisabledError(
109
116
  f"{e} Please provide `internal_api_admin_email` and `internal_api_admin_password` in your BenchlingConnection."
110
117
  )
111
- except RuntimeError as e:
112
- raise RuntimeError(
113
- f"{e}. If you are running this in a Jupyter notebook, use `nest_asyncio.apply()` to allow the async playwright login to run."
114
- )
115
118
  self.custom_post_cookies = {
116
119
  "session": authenticated_session,
117
120
  }
118
121
  self.custom_post_headers = {
119
122
  "Referer": f"https://{connection.tenant_name}.benchling.com/",
120
123
  "Content-Type": "application/json",
124
+ "x-csrftoken": csrf_token,
121
125
  }
122
126
  LOGGER.info(
123
127
  f"Tenant {connection.tenant_name}: Connected to Benchling internal API."
@@ -260,14 +264,14 @@ class BenchlingService(Benchling):
260
264
  )
261
265
 
262
266
  @classmethod
263
- async def autogenerate_auth(
267
+ def autogenerate_auth(
264
268
  cls,
265
269
  benchling_tenant: str,
266
270
  email: str | None = None,
267
271
  password: str | None = None,
268
272
  playwright_data_dir: str | None = None,
269
- ) -> str:
270
- """Logs in to Benchling using the admin email and password or playwright and returns the session cookie.
273
+ ) -> tuple[str, str]:
274
+ """Logs in to Benchling using the admin email and password or playwright and returns the session cookie and CSRF token.
271
275
  If email and password are not passed in or if SSO is set to required on the Benchling tenant, playwright is used to log in.
272
276
  Otherwise, the admin email and password are used to log in."""
273
277
  with requests.Session() as session:
@@ -291,9 +295,16 @@ class BenchlingService(Benchling):
291
295
  f"admin_email and admin_password not provided when sso is turned off for Benchling tenant {benchling_tenant}."
292
296
  )
293
297
  if signin_page.status_code == 302:
294
- return await cls.get_authenticated_session_sso_login_playwright(
295
- benchling_tenant, playwright_data_dir
296
- )
298
+ try:
299
+ return asyncio.get_event_loop().run_until_complete(
300
+ cls.get_authenticated_session_sso_login_playwright(
301
+ benchling_tenant, playwright_data_dir
302
+ )
303
+ )
304
+ except RuntimeError as e:
305
+ raise RuntimeError(
306
+ f"{e}. If you are running this in a Jupyter notebook, use `nest_asyncio.apply()` to allow the async playwright login to run."
307
+ )
297
308
  else:
298
309
  raise ValueError(
299
310
  f"Unexpected response: Status code {signin_page.status_code}: {signin_page.text}"
@@ -302,8 +313,8 @@ class BenchlingService(Benchling):
302
313
  @classmethod
303
314
  async def get_authenticated_session_sso_login_playwright(
304
315
  cls, benchling_tenant: str, playwright_data_dir: str | None = None
305
- ) -> str:
306
- """Logs in to Benchling using playwright and returns the session cookie.
316
+ ) -> tuple[str, str]:
317
+ """Logs in to Benchling using playwright and returns the session cookie and CSRF token.
307
318
  This can be used when SSO is enabled and required on the Benchling tenant."""
308
319
  LOGGER.info(f"Log into your {benchling_tenant} Benchling tenant...")
309
320
  async with async_playwright() as playwright:
@@ -319,6 +330,15 @@ class BenchlingService(Benchling):
319
330
  )
320
331
  context = await browser.new_context()
321
332
  page = await context.new_page()
333
+
334
+ headers = {}
335
+
336
+ async def async_get_request_headers(request: Request) -> None:
337
+ if urlparse(request.url).netloc == f"{benchling_tenant}.benchling.com":
338
+ headers.update(await request.all_headers())
339
+
340
+ page.on("request", async_get_request_headers)
341
+
322
342
  try:
323
343
  await page.goto(f"https://{benchling_tenant}.benchling.com")
324
344
  except Exception:
@@ -340,7 +360,10 @@ class BenchlingService(Benchling):
340
360
  )
341
361
  if not session_cookie:
342
362
  raise ValueError("No session cookie found.")
343
- return session_cookie
363
+ csrf_token = headers.get("x-csrftoken", None)
364
+ if not csrf_token:
365
+ raise ValueError("No CSRF token found.")
366
+ return session_cookie, csrf_token
344
367
 
345
368
  @classmethod
346
369
  @retry(
@@ -351,8 +374,8 @@ class BenchlingService(Benchling):
351
374
  )
352
375
  def get_authenticated_session_benchling_admin_login(
353
376
  cls, benchling_tenant: str, email: str, password: str
354
- ) -> str:
355
- """Logs in to Benchling using the admin email and password and returns the session cookie.
377
+ ) -> tuple[str, str]:
378
+ """Logs in to Benchling using the admin email and password and returns the session cookie and CSRF token.
356
379
  This can be used when SSO is disabled or optional on the Benchling tenant."""
357
380
  with requests.Session() as session:
358
381
  homepage = session.get(f"https://{benchling_tenant}.benchling.com/signin")
@@ -384,5 +407,6 @@ class BenchlingService(Benchling):
384
407
  return (
385
408
  signin_response.headers["Set-Cookie"]
386
409
  .split("; Secure")[0]
387
- .removeprefix("session=")
410
+ .removeprefix("session="),
411
+ csrf_token,
388
412
  )
@@ -0,0 +1,160 @@
1
+ import json
2
+ from typing import Any
3
+
4
+ import requests
5
+
6
+ from liminal.connection import BenchlingService
7
+ from liminal.enums.benchling_api_entity_type import BenchlingAPIEntityType
8
+ from liminal.utils import await_queued_response
9
+
10
+
11
+ def create_entity_schema(
12
+ benchling_service: BenchlingService,
13
+ api_entity_type: BenchlingAPIEntityType,
14
+ payload: dict[str, Any],
15
+ ) -> dict[str, Any]:
16
+ """
17
+ Create a new entity schema.
18
+ """
19
+ response = benchling_service.api.post_response(
20
+ url=f"/api/v3-alpha/benchling/{api_entity_type.value}", body=payload
21
+ )
22
+ if not (200 <= response.status_code < 300):
23
+ raise Exception("Failed to create entity schema:", response.content)
24
+ return json.loads(response.content)
25
+
26
+
27
+ def get_entity_schema(
28
+ benchling_service: BenchlingService,
29
+ api_entity_type: BenchlingAPIEntityType,
30
+ entity_schema_id: str,
31
+ ) -> dict[str, Any]:
32
+ """
33
+ Get an entity schema.
34
+ """
35
+ response = benchling_service.api.get_response(
36
+ url=f"/api/v3-alpha/benchling/{api_entity_type.value}/{entity_schema_id}"
37
+ )
38
+ if not (200 <= response.status_code < 300):
39
+ raise Exception("Failed to get entity schema:", response.content)
40
+ return json.loads(response.content)
41
+
42
+
43
+ def list_entity_schemas(
44
+ benchling_service: BenchlingService,
45
+ ) -> dict[str, Any]:
46
+ """
47
+ List all entity schemas.
48
+ """
49
+ response = benchling_service.api.get_response(
50
+ url="/api/v3-alpha/benchling/entity-schema/items"
51
+ )
52
+ if not (200 <= response.status_code < 300):
53
+ raise Exception("Failed to get entity schema:", response.content)
54
+ return json.loads(response.content)
55
+
56
+
57
+ def get_entity_schema_field_definitions(
58
+ benchling_service: BenchlingService,
59
+ api_entity_type: BenchlingAPIEntityType,
60
+ entity_schema_id: str,
61
+ ) -> dict[str, Any]:
62
+ """
63
+ Get the field definitions for an entity schema.
64
+ """
65
+ response = benchling_service.api.get_response(
66
+ url=f"/api/v3-alpha/benchling/{api_entity_type.value}/{entity_schema_id}/field-definitions/items"
67
+ )
68
+ if not (200 <= response.status_code < 300):
69
+ raise Exception("Failed to get entity schema:", response.content)
70
+ return json.loads(response.content)
71
+
72
+
73
+ def get_entity_schema_fieldsets(
74
+ benchling_service: BenchlingService,
75
+ api_entity_type: BenchlingAPIEntityType,
76
+ entity_schema_id: str,
77
+ ) -> dict[str, Any]:
78
+ """
79
+ Get the fieldsets for an entity schema.
80
+ """
81
+ response = benchling_service.api.get_response(
82
+ url=f"/api/v3-alpha/benchling/{api_entity_type.value}/{entity_schema_id}/fieldsets/items"
83
+ )
84
+ if not (200 <= response.status_code < 300):
85
+ raise Exception("Failed to get entity schema:", response.content)
86
+ return json.loads(response.content)
87
+
88
+
89
+ # def archive_tag_schemas(
90
+ # benchling_service: BenchlingService, entity_schema_ids: list[str]
91
+ # ) -> dict[str, Any]:
92
+ # """
93
+ # Archive a list of entity schema ids.
94
+ # """
95
+ # with requests.Session() as session:
96
+ # response = session.post(
97
+ # f"https://{benchling_service.benchling_tenant}.benchling.com/1/api/tag-schemas:bulk-archive",
98
+ # data=json.dumps({"ids": entity_schema_ids, "purpose": "Made in error"}),
99
+ # headers=benchling_service.custom_post_headers,
100
+ # cookies=benchling_service.custom_post_cookies,
101
+ # )
102
+ # if not response.ok:
103
+ # raise Exception("Failed to archive tag schemas:", response.content)
104
+ # return response.json()
105
+
106
+
107
+ # def unarchive_tag_schemas(
108
+ # benchling_service: BenchlingService, entity_schema_ids: list[str]
109
+ # ) -> dict[str, Any]:
110
+ # """
111
+ # Unarchive a list of entity schema ids.
112
+ # """
113
+ # with requests.Session() as session:
114
+ # response = session.post(
115
+ # f"https://{benchling_service.benchling_tenant}.benchling.com/1/api/tag-schemas:bulk-unarchive",
116
+ # data=json.dumps({"ids": entity_schema_ids}),
117
+ # headers=benchling_service.custom_post_headers,
118
+ # cookies=benchling_service.custom_post_cookies,
119
+ # )
120
+ # if not response.ok:
121
+ # raise Exception("Failed to unarchive tag schemas:", response.content)
122
+ # return response.json()
123
+
124
+
125
+ # def update_tag_schema(
126
+ # benchling_service: BenchlingService, entity_schema_id: str, payload: dict[str, Any]
127
+ # ) -> dict[str, Any]:
128
+ # """
129
+ # Update the tag schema with a new field.
130
+ # """
131
+ # with requests.Session() as session:
132
+ # queued_response = session.post(
133
+ # f"https://{benchling_service.benchling_tenant}.benchling.com/1/api/tag-schemas/{entity_schema_id}/actions/update",
134
+ # data=json.dumps(payload),
135
+ # headers=benchling_service.custom_post_headers,
136
+ # cookies=benchling_service.custom_post_cookies,
137
+ # )
138
+ # if not queued_response.ok:
139
+ # raise Exception("Failed to update tag schema:", queued_response.content)
140
+ # return await_queued_response(
141
+ # queued_response.json()["status_url"], benchling_service
142
+ # )
143
+
144
+
145
+ # def set_tag_schema_name_template(
146
+ # benchling_service: BenchlingService, entity_schema_id: str, payload: dict[str, Any]
147
+ # ) -> dict[str, Any]:
148
+ # """
149
+ # Update the tag schema name template. Must be in a separate endpoint compared to update_tag_schema.
150
+ # """
151
+ # with requests.Session() as session:
152
+ # response = session.post(
153
+ # f"https://{benchling_service.benchling_tenant}.benchling.com/1/api/tag-schemas/{entity_schema_id}/actions/set-name-template",
154
+ # data=json.dumps(payload),
155
+ # headers=benchling_service.custom_post_headers,
156
+ # cookies=benchling_service.custom_post_cookies,
157
+ # )
158
+ # if not response.ok:
159
+ # raise Exception("Failed to set tag schema name template:", response.content)
160
+ # return response.json()
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+ from liminal.connection import BenchlingService
7
+
8
+
9
+ class EntitySchemaOrganizationRefV2(BaseModel):
10
+ id: str
11
+
12
+
13
+ class EntitySchemaConstraintV2(BaseModel):
14
+ fieldDefinitionNames: list[str] = Field(default_factory=list)
15
+ hasUniqueResidues: bool | None = None
16
+
17
+
18
+ class FieldDefinitionLinkRefV2(BaseModel):
19
+ id: str
20
+
21
+
22
+ class EntitySchemaFieldDefinitionV2(BaseModel):
23
+ id: str
24
+ name: str
25
+ archived: bool
26
+ systemName: str
27
+ isMulti: bool | None = None
28
+ isRequired: bool
29
+ derivationType: str
30
+ isParent: bool | None = None
31
+ archiveReason: str | None = None
32
+ description: str | None = None
33
+ linkDefinition: FieldDefinitionLinkRefV2 | None = None
34
+ __typename: str | None = None
35
+
36
+
37
+ class EntitySchemaModelV2(BaseModel):
38
+ """Pydantic model for Benchling entity schema API payloads (e.g. RnaSequenceSchema)."""
39
+
40
+ model_config = ConfigDict(extra="ignore")
41
+
42
+ id: str
43
+ fieldDefinitions: str
44
+ fields: list[EntitySchemaFieldDefinitionV2] = Field(default_factory=list)
45
+ systemName: str
46
+ itemIdPrefix: str
47
+ containableType: str
48
+ fieldsets: str
49
+ systemCategories: list[Any] = Field(default_factory=list)
50
+ organization: EntitySchemaOrganizationRefV2
51
+ modifiedAt: str
52
+ includeRegistryIdInChips: bool | None = None
53
+ useRegistryIdForItemDisplayLabel: bool | None = None
54
+ availableRegistrationNamingOptions: list[str] = Field(default_factory=list)
55
+ archived: bool | None = None
56
+ name: str
57
+ description: str | None = None
58
+ showResidues: bool | None = None
59
+ createdAt: str
60
+ constraint: EntitySchemaConstraintV2 | None = None
61
+ archiveReason: str | None = None
62
+ __typename: str | None = None
63
+
64
+ @classmethod
65
+ def get_one(
66
+ cls,
67
+ benchling_service: BenchlingService,
68
+ wh_schema_name: str,
69
+ schemas_data: list[dict[str, Any]] | None = None,
70
+ ) -> EntitySchemaModelV2:
71
+ if schemas_data is None:
72
+ schemas_data = cls.get_all_json(benchling_service)
73
+ schema = next(
74
+ (
75
+ schema
76
+ for schema in schemas_data
77
+ if schema["sqlIdentifier"] == wh_schema_name
78
+ and schema["registryId"] == benchling_service.registry_id
79
+ ),
80
+ None,
81
+ )
82
+ if schema is None:
83
+ raise ValueError(
84
+ f"Schema {wh_schema_name} not found in Benchling {benchling_service.benchling_tenant}."
85
+ )
86
+ return cls.model_validate(schema)
@@ -0,0 +1,18 @@
1
+ from liminal.base.str_enum import StrEnum
2
+
3
+
4
+ class BenchlingAPIEntityType(StrEnum):
5
+ """This enum represents the different types a Benchling field can have on an entity schema.
6
+ These are ONLY USED FOR THE INTERNAL BENCHLING API."""
7
+
8
+ CUSTOM_ENTITY_SCHEMA = "custom-entity-schema"
9
+ AA_SEQUENCE_SCHEMA = "aa-sequence-schema"
10
+ DNA_OLIGO_SCHEMA = "dna-oligo-schema"
11
+ DNA_SEQUENCE_SCHEMA = "dna-sequence-schema"
12
+ OLIGO_CONJUGATE_SCHEMA = "oligo-conjugate-schema"
13
+ OLIGO_DUPLEX_SCHEMA = "oligo-duplex-schema"
14
+ RNA_OLIGO_SCHEMA = "rna-oligo-schema"
15
+ RNA_SEQUENCE_SCHEMA = "rna-sequence-schema"
16
+ MIXTURE_SCHEMA = "mixture-schema"
17
+ MOLECULE_SCHEMA = "molecule-schema"
18
+ EQUIPMENT_SCHEMA = "equipment-schema"
@@ -0,0 +1,34 @@
1
+ from typing import cast
2
+
3
+ from benchling_api_client.v2.benchling_client import BenchlingApiClient
4
+
5
+ from liminal.connection.benchling_connection import BenchlingConnection
6
+ from liminal.connection.benchling_service import BenchlingService
7
+
8
+ # The Benchling SDK defaults the per-request HTTP timeout to 10 seconds.
9
+ SDK_DEFAULT_TIMEOUT_SECONDS = 10
10
+
11
+
12
+ def _connection() -> BenchlingConnection:
13
+ return BenchlingConnection(
14
+ tenant_name="test-tenant",
15
+ api_client_id="test-id",
16
+ api_client_secret="test-secret",
17
+ )
18
+
19
+
20
+ class TestBenchlingServiceClientDecorator:
21
+ def test_default_timeout_is_sdk_default(self) -> None:
22
+ service = BenchlingService(_connection(), use_db=False)
23
+ assert service._client.get_timeout() == SDK_DEFAULT_TIMEOUT_SECONDS
24
+
25
+ def test_client_decorator_is_forwarded_to_sdk(self) -> None:
26
+ def raise_timeout(client: BenchlingApiClient) -> BenchlingApiClient:
27
+ # with_timeout is typed to return the base Client but preserves the subclass.
28
+ return cast(BenchlingApiClient, client.with_timeout(60))
29
+
30
+ service = BenchlingService(
31
+ _connection(), use_db=False, client_decorator=raise_timeout
32
+ )
33
+
34
+ assert service._client.get_timeout() == 60
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "liminal-orm"
3
- version = "4.5.0"
3
+ version = "4.6.0"
4
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
5
  authors = [{ name = "DynoTx Open Source", email = "opensource@dynotx.com" }]
6
6
  requires-python = ">=3.9,<4"
@@ -1 +0,0 @@
1
- *
File without changes
@@ -1 +0,0 @@
1
- Signature: 8a477f597d28d172789f06886806bc55
@@ -1,5 +0,0 @@
1
- home = /Users/nirmitdamania/.local/share/uv/python/cpython-3.10.8-macos-aarch64-none/bin
2
- implementation = CPython
3
- uv = 0.8.9
4
- version_info = 3.10.8
5
- include-system-site-packages = false
@@ -1,14 +0,0 @@
1
- {
2
- "argv": [
3
- "python",
4
- "-m",
5
- "ipykernel_launcher",
6
- "-f",
7
- "{connection_file}"
8
- ],
9
- "display_name": "Python 3 (ipykernel)",
10
- "language": "python",
11
- "metadata": {
12
- "debugger": true
13
- }
14
- }
@@ -1,265 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
- <!-- Created with Inkscape (http://www.inkscape.org/) -->
3
-
4
- <svg
5
- version="1.0"
6
- id="svg2"
7
- sodipodi:version="0.32"
8
- inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
9
- sodipodi:docname="python-logo-only.svg"
10
- width="83.371017pt"
11
- height="101.00108pt"
12
- inkscape:export-filename="python-logo-only.png"
13
- inkscape:export-xdpi="232.44"
14
- inkscape:export-ydpi="232.44"
15
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
16
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
17
- xmlns:xlink="http://www.w3.org/1999/xlink"
18
- xmlns="http://www.w3.org/2000/svg"
19
- xmlns:svg="http://www.w3.org/2000/svg"
20
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
21
- xmlns:cc="http://creativecommons.org/ns#"
22
- xmlns:dc="http://purl.org/dc/elements/1.1/">
23
- <metadata
24
- id="metadata371">
25
- <rdf:RDF>
26
- <cc:Work
27
- rdf:about="">
28
- <dc:format>image/svg+xml</dc:format>
29
- <dc:type
30
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
31
- </cc:Work>
32
- </rdf:RDF>
33
- </metadata>
34
- <sodipodi:namedview
35
- inkscape:window-height="2080"
36
- inkscape:window-width="1976"
37
- inkscape:pageshadow="2"
38
- inkscape:pageopacity="0.0"
39
- guidetolerance="10.0"
40
- gridtolerance="10.0"
41
- objecttolerance="10.0"
42
- borderopacity="1.0"
43
- bordercolor="#666666"
44
- pagecolor="#ffffff"
45
- id="base"
46
- inkscape:zoom="2.1461642"
47
- inkscape:cx="91.558698"
48
- inkscape:cy="47.9926"
49
- inkscape:window-x="1092"
50
- inkscape:window-y="72"
51
- inkscape:current-layer="svg2"
52
- width="210mm"
53
- height="40mm"
54
- units="mm"
55
- inkscape:showpageshadow="2"
56
- inkscape:pagecheckerboard="0"
57
- inkscape:deskcolor="#d1d1d1"
58
- inkscape:document-units="pt"
59
- showgrid="false"
60
- inkscape:window-maximized="0" />
61
- <defs
62
- id="defs4">
63
- <linearGradient
64
- id="linearGradient2795">
65
- <stop
66
- style="stop-color:#b8b8b8;stop-opacity:0.49803922;"
67
- offset="0"
68
- id="stop2797" />
69
- <stop
70
- style="stop-color:#7f7f7f;stop-opacity:0;"
71
- offset="1"
72
- id="stop2799" />
73
- </linearGradient>
74
- <linearGradient
75
- id="linearGradient2787">
76
- <stop
77
- style="stop-color:#7f7f7f;stop-opacity:0.5;"
78
- offset="0"
79
- id="stop2789" />
80
- <stop
81
- style="stop-color:#7f7f7f;stop-opacity:0;"
82
- offset="1"
83
- id="stop2791" />
84
- </linearGradient>
85
- <linearGradient
86
- id="linearGradient3676">
87
- <stop
88
- style="stop-color:#b2b2b2;stop-opacity:0.5;"
89
- offset="0"
90
- id="stop3678" />
91
- <stop
92
- style="stop-color:#b3b3b3;stop-opacity:0;"
93
- offset="1"
94
- id="stop3680" />
95
- </linearGradient>
96
- <linearGradient
97
- id="linearGradient3236">
98
- <stop
99
- style="stop-color:#f4f4f4;stop-opacity:1"
100
- offset="0"
101
- id="stop3244" />
102
- <stop
103
- style="stop-color:white;stop-opacity:1"
104
- offset="1"
105
- id="stop3240" />
106
- </linearGradient>
107
- <linearGradient
108
- id="linearGradient4671">
109
- <stop
110
- style="stop-color:#ffd43b;stop-opacity:1;"
111
- offset="0"
112
- id="stop4673" />
113
- <stop
114
- style="stop-color:#ffe873;stop-opacity:1"
115
- offset="1"
116
- id="stop4675" />
117
- </linearGradient>
118
- <linearGradient
119
- id="linearGradient4689">
120
- <stop
121
- style="stop-color:#5a9fd4;stop-opacity:1;"
122
- offset="0"
123
- id="stop4691" />
124
- <stop
125
- style="stop-color:#306998;stop-opacity:1;"
126
- offset="1"
127
- id="stop4693" />
128
- </linearGradient>
129
- <linearGradient
130
- x1="224.23996"
131
- y1="144.75717"
132
- x2="-65.308502"
133
- y2="144.75717"
134
- id="linearGradient2987"
135
- xlink:href="#linearGradient4671"
136
- gradientUnits="userSpaceOnUse"
137
- gradientTransform="translate(100.2702,99.61116)" />
138
- <linearGradient
139
- x1="172.94208"
140
- y1="77.475983"
141
- x2="26.670298"
142
- y2="76.313133"
143
- id="linearGradient2990"
144
- xlink:href="#linearGradient4689"
145
- gradientUnits="userSpaceOnUse"
146
- gradientTransform="translate(100.2702,99.61116)" />
147
- <linearGradient
148
- inkscape:collect="always"
149
- xlink:href="#linearGradient4689"
150
- id="linearGradient2587"
151
- gradientUnits="userSpaceOnUse"
152
- gradientTransform="translate(100.2702,99.61116)"
153
- x1="172.94208"
154
- y1="77.475983"
155
- x2="26.670298"
156
- y2="76.313133" />
157
- <linearGradient
158
- inkscape:collect="always"
159
- xlink:href="#linearGradient4671"
160
- id="linearGradient2589"
161
- gradientUnits="userSpaceOnUse"
162
- gradientTransform="translate(100.2702,99.61116)"
163
- x1="224.23996"
164
- y1="144.75717"
165
- x2="-65.308502"
166
- y2="144.75717" />
167
- <linearGradient
168
- inkscape:collect="always"
169
- xlink:href="#linearGradient4689"
170
- id="linearGradient2248"
171
- gradientUnits="userSpaceOnUse"
172
- gradientTransform="translate(100.2702,99.61116)"
173
- x1="172.94208"
174
- y1="77.475983"
175
- x2="26.670298"
176
- y2="76.313133" />
177
- <linearGradient
178
- inkscape:collect="always"
179
- xlink:href="#linearGradient4671"
180
- id="linearGradient2250"
181
- gradientUnits="userSpaceOnUse"
182
- gradientTransform="translate(100.2702,99.61116)"
183
- x1="224.23996"
184
- y1="144.75717"
185
- x2="-65.308502"
186
- y2="144.75717" />
187
- <linearGradient
188
- inkscape:collect="always"
189
- xlink:href="#linearGradient4671"
190
- id="linearGradient2255"
191
- gradientUnits="userSpaceOnUse"
192
- gradientTransform="matrix(0.562541,0,0,0.567972,-11.5974,-7.60954)"
193
- x1="224.23996"
194
- y1="144.75717"
195
- x2="-65.308502"
196
- y2="144.75717" />
197
- <linearGradient
198
- inkscape:collect="always"
199
- xlink:href="#linearGradient4689"
200
- id="linearGradient2258"
201
- gradientUnits="userSpaceOnUse"
202
- gradientTransform="matrix(0.562541,0,0,0.567972,-11.5974,-7.60954)"
203
- x1="172.94208"
204
- y1="76.176224"
205
- x2="26.670298"
206
- y2="76.313133" />
207
- <radialGradient
208
- inkscape:collect="always"
209
- xlink:href="#linearGradient2795"
210
- id="radialGradient2801"
211
- cx="61.518883"
212
- cy="132.28575"
213
- fx="61.518883"
214
- fy="132.28575"
215
- r="29.036913"
216
- gradientTransform="matrix(1,0,0,0.177966,0,108.7434)"
217
- gradientUnits="userSpaceOnUse" />
218
- <linearGradient
219
- inkscape:collect="always"
220
- xlink:href="#linearGradient4671"
221
- id="linearGradient1475"
222
- gradientUnits="userSpaceOnUse"
223
- gradientTransform="matrix(0.562541,0,0,0.567972,-14.99112,-11.702371)"
224
- x1="150.96111"
225
- y1="192.35176"
226
- x2="112.03144"
227
- y2="137.27299" />
228
- <linearGradient
229
- inkscape:collect="always"
230
- xlink:href="#linearGradient4689"
231
- id="linearGradient1478"
232
- gradientUnits="userSpaceOnUse"
233
- gradientTransform="matrix(0.562541,0,0,0.567972,-14.99112,-11.702371)"
234
- x1="26.648937"
235
- y1="20.603781"
236
- x2="135.66525"
237
- y2="114.39767" />
238
- <radialGradient
239
- inkscape:collect="always"
240
- xlink:href="#linearGradient2795"
241
- id="radialGradient1480"
242
- gradientUnits="userSpaceOnUse"
243
- gradientTransform="matrix(1.7490565e-8,-0.23994696,1.054668,3.7915457e-7,-83.7008,142.46201)"
244
- cx="61.518883"
245
- cy="132.28575"
246
- fx="61.518883"
247
- fy="132.28575"
248
- r="29.036913" />
249
- </defs>
250
- <path
251
- style="fill:url(#linearGradient1478);fill-opacity:1"
252
- d="M 54.918785,9.1927421e-4 C 50.335132,0.02221727 45.957846,0.41313697 42.106285,1.0946693 30.760069,3.0991731 28.700036,7.2947714 28.700035,15.032169 v 10.21875 h 26.8125 v 3.40625 h -26.8125 -10.0625 c -7.792459,0 -14.6157588,4.683717 -16.7499998,13.59375 -2.46181998,10.212966 -2.57101508,16.586023 0,27.25 1.9059283,7.937852 6.4575432,13.593748 14.2499998,13.59375 h 9.21875 v -12.25 c 0,-8.849902 7.657144,-16.656248 16.75,-16.65625 h 26.78125 c 7.454951,0 13.406253,-6.138164 13.40625,-13.625 v -25.53125 c 0,-7.2663386 -6.12998,-12.7247771 -13.40625,-13.9374997 C 64.281548,0.32794397 59.502438,-0.02037903 54.918785,9.1927421e-4 Z m -14.5,8.21875012579 c 2.769547,0 5.03125,2.2986456 5.03125,5.1249996 -2e-6,2.816336 -2.261703,5.09375 -5.03125,5.09375 -2.779476,-1e-6 -5.03125,-2.277415 -5.03125,-5.09375 -10e-7,-2.826353 2.251774,-5.1249996 5.03125,-5.1249996 z"
253
- id="path1948" />
254
- <path
255
- style="fill:url(#linearGradient1475);fill-opacity:1"
256
- d="m 85.637535,28.657169 v 11.90625 c 0,9.230755 -7.825895,16.999999 -16.75,17 h -26.78125 c -7.335833,0 -13.406249,6.278483 -13.40625,13.625 v 25.531247 c 0,7.266344 6.318588,11.540324 13.40625,13.625004 8.487331,2.49561 16.626237,2.94663 26.78125,0 6.750155,-1.95439 13.406253,-5.88761 13.40625,-13.625004 V 86.500919 h -26.78125 v -3.40625 h 26.78125 13.406254 c 7.792461,0 10.696251,-5.435408 13.406241,-13.59375 2.79933,-8.398886 2.68022,-16.475776 0,-27.25 -1.92578,-7.757441 -5.60387,-13.59375 -13.406241,-13.59375 z m -15.0625,64.65625 c 2.779478,3e-6 5.03125,2.277417 5.03125,5.093747 -2e-6,2.826354 -2.251775,5.125004 -5.03125,5.125004 -2.76955,0 -5.03125,-2.29865 -5.03125,-5.125004 2e-6,-2.81633 2.261697,-5.093747 5.03125,-5.093747 z"
257
- id="path1950" />
258
- <ellipse
259
- style="opacity:0.44382;fill:url(#radialGradient1480);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:15.4174;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
260
- id="path1894"
261
- cx="55.816761"
262
- cy="127.70079"
263
- rx="35.930977"
264
- ry="6.9673119" />
265
- </svg>
@@ -1,60 +0,0 @@
1
- .\" Hey, EMACS: -*- nroff -*-
2
- .\" First parameter, NAME, should be all caps
3
- .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection
4
- .\" other parameters are allowed: see man(7), man(1)
5
- .TH IPYTHON 1 "July 15, 2011"
6
- .\" Please adjust this date whenever revising the manpage.
7
- .\"
8
- .\" Some roff macros, for reference:
9
- .\" .nh disable hyphenation
10
- .\" .hy enable hyphenation
11
- .\" .ad l left justify
12
- .\" .ad b justify to both left and right margins
13
- .\" .nf disable filling
14
- .\" .fi enable filling
15
- .\" .br insert line break
16
- .\" .sp <n> insert n+1 empty lines
17
- .\" for manpage-specific macros, see man(7) and groff_man(7)
18
- .\" .SH section heading
19
- .\" .SS secondary section heading
20
- .\"
21
- .\"
22
- .\" To preview this page as plain text: nroff -man ipython.1
23
- .\"
24
- .SH NAME
25
- ipython \- Tools for Interactive Computing in Python.
26
- .SH SYNOPSIS
27
- .B ipython
28
- .RI [ options ] " files" ...
29
-
30
- .B ipython subcommand
31
- .RI [ options ] ...
32
-
33
- .SH DESCRIPTION
34
- An interactive Python shell with automatic history (input and output), dynamic
35
- object introspection, easier configuration, command completion, access to the
36
- system shell, integration with numerical and scientific computing tools,
37
- web notebook, Qt console, and more.
38
-
39
- For more information on how to use IPython, see 'ipython \-\-help',
40
- or 'ipython \-\-help\-all' for all available command\(hyline options.
41
-
42
- .SH "ENVIRONMENT VARIABLES"
43
- .sp
44
- .PP
45
- \fIIPYTHONDIR\fR
46
- .RS 4
47
- This is the location where IPython stores all its configuration files. The default
48
- is $HOME/.ipython if IPYTHONDIR is not defined.
49
-
50
- You can see the computed value of IPYTHONDIR with `ipython locate`.
51
-
52
- .SH FILES
53
-
54
- IPython uses various configuration files stored in profiles within IPYTHONDIR.
55
- To generate the default configuration files and start configuring IPython,
56
- do 'ipython profile create', and edit '*_config.py' files located in
57
- IPYTHONDIR/profile_default.
58
-
59
- .SH AUTHORS
60
- IPython is written by the IPython Development Team <https://github.com/ipython/ipython>.
File without changes
File without changes
File without changes