benchling-sdk 1.9.0a5__py3-none-any.whl → 1.10.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.
Files changed (32) hide show
  1. benchling_sdk/apps/canvas/__init__.py +0 -0
  2. benchling_sdk/apps/canvas/errors.py +14 -0
  3. benchling_sdk/apps/{helpers/canvas_helpers.py → canvas/framework.py} +129 -188
  4. benchling_sdk/apps/canvas/types.py +125 -0
  5. benchling_sdk/apps/config/__init__.py +0 -3
  6. benchling_sdk/apps/config/decryption_provider.py +1 -1
  7. benchling_sdk/apps/config/errors.py +38 -0
  8. benchling_sdk/apps/config/framework.py +343 -0
  9. benchling_sdk/apps/config/helpers.py +157 -0
  10. benchling_sdk/apps/config/{mock_dependencies.py → mock_config.py} +78 -99
  11. benchling_sdk/apps/config/types.py +36 -0
  12. benchling_sdk/apps/framework.py +49 -338
  13. benchling_sdk/apps/helpers/webhook_helpers.py +2 -2
  14. benchling_sdk/apps/status/__init__.py +0 -0
  15. benchling_sdk/apps/status/errors.py +85 -0
  16. benchling_sdk/apps/{helpers/session_helpers.py → status/framework.py} +58 -167
  17. benchling_sdk/apps/status/helpers.py +20 -0
  18. benchling_sdk/apps/status/types.py +45 -0
  19. benchling_sdk/apps/types.py +3 -0
  20. benchling_sdk/errors.py +4 -4
  21. benchling_sdk/models/__init__.py +44 -0
  22. benchling_sdk/services/v2/beta/{v2_beta_dataset_service.py → v2_beta_data_frame_service.py} +126 -116
  23. benchling_sdk/services/v2/stable/assay_result_service.py +18 -0
  24. benchling_sdk/services/v2/v2_beta_service.py +11 -11
  25. {benchling_sdk-1.9.0a5.dist-info → benchling_sdk-1.10.0.dist-info}/METADATA +4 -4
  26. {benchling_sdk-1.9.0a5.dist-info → benchling_sdk-1.10.0.dist-info}/RECORD +29 -20
  27. benchling_sdk/apps/config/dependencies.py +0 -1085
  28. benchling_sdk/apps/config/scalars.py +0 -226
  29. benchling_sdk/apps/helpers/config_helpers.py +0 -409
  30. /benchling_sdk/apps/{helpers → config}/cryptography_helpers.py +0 -0
  31. {benchling_sdk-1.9.0a5.dist-info → benchling_sdk-1.10.0.dist-info}/LICENSE +0 -0
  32. {benchling_sdk-1.9.0a5.dist-info → benchling_sdk-1.10.0.dist-info}/WHEEL +0 -0
@@ -1,137 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
- from abc import ABC, abstractmethod
4
- from typing import Generic, Optional, Protocol, Type, TypeVar, Union
5
-
6
- import attr
7
- from benchling_api_client.v2.benchling_client import AuthorizationMethod
8
- import httpx
9
-
10
- from benchling_sdk.apps import helpers
11
- from benchling_sdk.apps.config.decryption_provider import BaseDecryptionProvider
12
- from benchling_sdk.apps.config.dependencies import BaseDependencies
13
- from benchling_sdk.benchling import (
14
- _DEFAULT_BASE_PATH,
15
- _DEFAULT_RETRY_STRATEGY,
16
- Benchling,
17
- BenchlingApiClientDecorator,
3
+ from typing import Optional
4
+
5
+ from benchling_sdk.apps.config.framework import BenchlingConfigProvider, ConfigItemStore
6
+ from benchling_sdk.apps.status.framework import (
7
+ continue_session_context,
8
+ new_session_context,
9
+ SessionContextEnterHandler,
10
+ SessionContextExitHandler,
11
+ SessionContextManager,
18
12
  )
19
- from benchling_sdk.helpers.logging_helpers import log_stability_warning, StabilityLevel
20
- from benchling_sdk.helpers.retry_helpers import RetryStrategy
21
- from benchling_sdk.models.webhooks.v0 import WebhookEnvelopeV0
22
-
23
- log_stability_warning(StabilityLevel.ALPHA)
24
-
25
-
26
- ConfigType = TypeVar("ConfigType", bound=BaseDependencies)
27
- AppType = TypeVar("AppType", bound="App")
28
- AppWebhookType = WebhookEnvelopeV0
29
-
30
-
31
- class MissingTenantUrlProviderError(Exception):
32
- """Error when a base URL is expected but unspecified."""
33
-
34
- pass
35
-
36
-
37
- class MissingAppConfigTypeError(Exception):
38
- """Error when app config is expected but unspecified."""
39
-
40
- pass
41
-
42
-
43
- class MalformedAppWebhookError(Exception):
44
- """Error when a webhook cannot be read by an app."""
45
-
46
- pass
47
-
48
-
49
- class TenantUrlProvider(Protocol):
50
- """Return a base URL."""
13
+ from benchling_sdk.benchling import Benchling
51
14
 
52
- def __call__(self) -> str:
53
- """Return a base URL."""
54
- pass
55
15
 
56
-
57
- def tenant_url_provider_static(tenant_url: str) -> TenantUrlProvider:
58
- """Create a provider function that always returns a static tenant URL."""
59
-
60
- def _url() -> str:
61
- return tenant_url
62
-
63
- return _url
64
-
65
-
66
- def tenant_url_provider_lazy() -> TenantUrlProvider:
67
- """
68
- Create a provider function for app that will be initialized at runtime, such as from a webhook.
69
-
70
- Useful for when a base_url for Benchling is not known in advance but can be supplied at runtime.
71
- """
72
-
73
- def _deferred() -> str:
74
- raise MissingTenantUrlProviderError(
75
- "Unable to initialize base URL for tenant. Expected a URL to "
76
- "be provided at runtime but none was specified. Either specify "
77
- "a url provider or use TenantUrlProvider.static_url"
78
- )
79
-
80
- return _deferred
81
-
82
-
83
- class BenchlingProvider(Protocol):
84
- """Return a Benchling instance."""
85
-
86
- def __call__(self, tenant_url_provider: TenantUrlProvider) -> Benchling:
87
- """Return a Benchling instance."""
88
- pass
89
-
90
-
91
- class ConfigProvider(Protocol[ConfigType]):
92
- """Return a ConfigType instance."""
93
-
94
- def __call__(self, app: App[ConfigType]) -> ConfigType:
95
- """Return a ConfigType instance."""
96
- pass
97
-
98
-
99
- def config_provider_static(config: ConfigType) -> ConfigProvider[ConfigType]:
100
- """Create a provider function that always returns a static app config."""
101
-
102
- def _static_config(app: App[ConfigType]) -> ConfigType:
103
- return config
104
-
105
- return _static_config
106
-
107
-
108
- def config_provider_error_on_call() -> ConfigProvider[ConfigType]:
109
- """
110
- Create a provider function that raises an error.
111
-
112
- Used as a ConfigProvider for apps which don't support config and don't expect to invoke it.
113
- """
114
-
115
- def _error_on_call(app: App[ConfigType]) -> ConfigType:
116
- raise MissingAppConfigTypeError(
117
- "No app config class was defined for this app. "
118
- "Initialize an app with a ConfigProvider to use config."
119
- )
120
-
121
- return _error_on_call
122
-
123
-
124
- def benchling_provider_static(benchling: Benchling) -> BenchlingProvider:
125
- """Create a provider function that always returns a static Benchling."""
126
-
127
- def _static_benchling(tenant_url_provider: TenantUrlProvider) -> Benchling:
128
- return benchling
129
-
130
- return _static_benchling
131
-
132
-
133
- @attr.s(auto_attribs=True)
134
- class App(Generic[ConfigType]):
16
+ class App:
135
17
  """
136
18
  App.
137
19
 
@@ -141,238 +23,67 @@ class App(Generic[ConfigType]):
141
23
  known until runtime. Also allows for easier mocking in tests.
142
24
  """
143
25
 
144
- id: str
145
- _benchling_provider: BenchlingProvider
146
- _tenant_url_provider: TenantUrlProvider
147
- _config_provider: ConfigProvider[ConfigType] = attr.ib(default=config_provider_error_on_call)
148
- _benchling: Optional[Benchling] = attr.ib(default=None, init=False)
149
- _config: Optional[ConfigType] = attr.ib(default=None, init=False)
26
+ _app_id: str
27
+ _benchling: Benchling
28
+ _config_store: ConfigItemStore
29
+
30
+ def __init__(
31
+ self, app_id: str, benchling: Benchling, config_store: Optional[ConfigItemStore] = None
32
+ ) -> None:
33
+ """
34
+ Initialize a Benchling App.
35
+
36
+ :param app_id: An id representing a tenanted app installation (e.g., "app_Uh3BZ55aYcXGFJVb")
37
+ :param benchling: A Benchling object for making API calls. The auth_method should be valid for the specified App.
38
+ Commonly this is ClientCredentialsOAuth2 using the app's client ID and client secret.
39
+ :param config_store: The configuration item store for accessing an App's tenanted app config items.
40
+ If unspecified, will default to retrieving app config from the tenant referenced by Benchling.
41
+ Apps that don't use app configuration can safely ignore this.
42
+ """
43
+ self._app_id = app_id
44
+ self._benchling = benchling
45
+ self._config_store = (
46
+ config_store if config_store else ConfigItemStore(BenchlingConfigProvider(benchling, app_id))
47
+ )
48
+
49
+ @property
50
+ def id(self) -> str:
51
+ """Return the app tenanted installation id."""
52
+ return self._app_id
150
53
 
151
54
  @property
152
55
  def benchling(self) -> Benchling:
153
56
  """Return a Benchling instance for the App."""
154
- if self._benchling is None:
155
- self._benchling = self._benchling_provider(self._tenant_url_provider)
156
57
  return self._benchling
157
58
 
158
59
  @property
159
- def config(self) -> ConfigType:
160
- """
161
- Return config for the app.
162
-
163
- Apps which do not have config will raise MissingAppConfigTypeError.
164
- """
165
- if self._config is None:
166
- self._config = self._config_provider(self)
167
- return self._config
168
-
169
- def reset(self) -> None:
170
- """
171
- Reset the app.
172
-
173
- Generally clears all states and internal caches, which may cause subsequent invocations of the App
174
- to be expensive.
175
- """
176
- self._benchling = None
177
- if self._config is not None:
178
- self._config.invalidate_cache()
179
-
180
- def with_base_url(self: AppType, base_url: str) -> AppType:
181
- """Create a new copy of the app with a different base URL."""
182
- updated_tenant_url_provider = tenant_url_provider_static(base_url)
183
- modified_app = attr.evolve(self, tenant_url_provider=updated_tenant_url_provider)
184
- modified_app.reset()
185
- return modified_app
186
-
187
- def with_webhook(self: AppType, webhook: Union[dict, AppWebhookType]) -> AppType:
188
- """Create a new copy of the app with a different base URL provided by a webhook."""
189
- if isinstance(webhook, dict):
190
- if "baseUrl" not in webhook:
191
- raise MalformedAppWebhookError("The webhook specified did not contain a baseUrl")
192
- base_url = webhook["baseUrl"]
193
- else:
194
- base_url = webhook.base_url
195
- return self.with_base_url(base_url)
60
+ def config_store(self) -> ConfigItemStore:
61
+ """Return a ConfigItemStore instance for the App."""
62
+ return self._config_store
196
63
 
197
64
  def create_session_context(
198
- self: AppType,
65
+ self,
199
66
  name: str,
200
67
  timeout_seconds: int,
201
- context_enter_handler: Optional[helpers.session_helpers.SessionContextEnterHandler[AppType]] = None,
202
- context_exit_handler: Optional[helpers.session_helpers.SessionContextExitHandler[AppType]] = None,
203
- ) -> helpers.session_helpers.SessionContextManager[AppType]:
68
+ context_enter_handler: Optional[SessionContextEnterHandler] = None,
69
+ context_exit_handler: Optional[SessionContextExitHandler] = None,
70
+ ) -> SessionContextManager:
204
71
  """
205
72
  Create Session Context.
206
73
 
207
74
  Create a new app session in Benchling.
208
75
  """
209
- # Avoid circular import + MyPy "is not defined" if using relative like above
210
- from benchling_sdk.apps.helpers.session_helpers import new_session_context
211
-
212
76
  return new_session_context(self, name, timeout_seconds, context_enter_handler, context_exit_handler)
213
77
 
214
78
  def continue_session_context(
215
- self: AppType,
79
+ self,
216
80
  session_id: str,
217
- context_enter_handler: Optional[helpers.session_helpers.SessionContextEnterHandler[AppType]] = None,
218
- context_exit_handler: Optional[helpers.session_helpers.SessionContextExitHandler[AppType]] = None,
219
- ) -> helpers.session_helpers.SessionContextManager[AppType]:
81
+ context_enter_handler: Optional[SessionContextEnterHandler] = None,
82
+ context_exit_handler: Optional[SessionContextExitHandler] = None,
83
+ ) -> SessionContextManager:
220
84
  """
221
85
  Continue Session Context.
222
86
 
223
87
  Fetch an existing app session from Benchling and enter a context with it.
224
88
  """
225
- # Avoid circular import + MyPy "is not defined" if using relative like above
226
- from benchling_sdk.apps.helpers.session_helpers import continue_session_context
227
-
228
89
  return continue_session_context(self, session_id, context_enter_handler, context_exit_handler)
229
-
230
- @classmethod
231
- def init(
232
- cls: Type[AppType],
233
- id: str,
234
- benchling_provider: BenchlingProvider,
235
- tenant_url_provider: TenantUrlProvider,
236
- config_provider: Optional[ConfigProvider] = None,
237
- ) -> AppType:
238
- """
239
- Init.
240
-
241
- Initialize an app from its class.
242
- """
243
- required_config_provider: ConfigProvider[ConfigType] = (
244
- config_provider_error_on_call() if config_provider is None else config_provider
245
- )
246
- return cls(id, benchling_provider, tenant_url_provider, required_config_provider)
247
-
248
-
249
- class BaseAppFactory(ABC, Generic[AppType, ConfigType]):
250
- """
251
- Base App Factory.
252
-
253
- Can be used as an alternative to init_app() for those who prefer to import a pre-defined app instance
254
- globally. Call create() on the factory to initialize an App.
255
-
256
- Users must subclass AppFactory and implement its abstract methods to create a subclass of App.
257
- """
258
-
259
- _app_type: Type[AppType]
260
- app_id: str
261
- benchling_provider: BenchlingProvider
262
- config_provider: ConfigProvider[ConfigType]
263
-
264
- def __init__(self, app_type: Type[AppType], app_id: str, config_type: Optional[Type[ConfigType]] = None):
265
- """Initialize App Factory."""
266
- self._app_type = app_type
267
- self.app_id = app_id
268
-
269
- # Initialize providers here to fail fast if there is a problem assembling them from the factory
270
-
271
- def benchling_provider(tenant_url_provider):
272
- tenant_url = tenant_url_provider()
273
- return Benchling(
274
- url=tenant_url,
275
- auth_method=self.auth_method,
276
- base_path=self.base_path,
277
- retry_strategy=self.retry_strategy,
278
- client_decorator=self.client_decorator,
279
- httpx_client=self.httpx_client,
280
- )
281
-
282
- if config_type is None:
283
- config_provider: ConfigProvider[ConfigType] = config_provider_error_on_call()
284
- else:
285
-
286
- def _config_provider(app: App[ConfigType]) -> ConfigType:
287
- # MyPy believes config_type can be None despite the conditional
288
- return config_type.from_app(app.benchling, app.id, self.decryption_provider) # type: ignore
289
-
290
- config_provider = _config_provider
291
-
292
- self.benchling_provider = benchling_provider
293
- self.config_provider = config_provider
294
-
295
- def create(self) -> AppType:
296
- """Create an App instance from the factory."""
297
- return self._app_type.init(
298
- self.app_id, self.benchling_provider, self.tenant_url_provider, self.config_provider
299
- )
300
-
301
- @property
302
- @abstractmethod
303
- def auth_method(self) -> AuthorizationMethod:
304
- """
305
- Get an auth method to pass to Benchling.
306
-
307
- Must be implemented on all subclasses.
308
- """
309
- pass
310
-
311
- @property
312
- def tenant_url_provider(self) -> TenantUrlProvider:
313
- """
314
- Get a tenant URL provider that will provide a base URL for Benchling at runtime.
315
-
316
- By default, assumes that the App has no base_url and will be provided one later (e.g., from a webhook).
317
- Invoking app.benchling on an App in this state without setting a URL will raise an error.
318
-
319
- Use tenant_url_provider_static("https://myurl...") to specify a single URL.
320
- """
321
- return tenant_url_provider_lazy()
322
-
323
- # Benchling overrides
324
-
325
- @property
326
- def base_path(self) -> Optional[str]:
327
- """Get a base_path for Benchling."""
328
- return _DEFAULT_BASE_PATH
329
-
330
- @property
331
- def client_decorator(self) -> Optional[BenchlingApiClientDecorator]:
332
- """Get a BenchlingApiClientDecorator for Benchling."""
333
- return None
334
-
335
- @property
336
- def httpx_client(self) -> Optional[httpx.Client]:
337
- """Get a custom httpx Client for Benchling."""
338
- return None
339
-
340
- @property
341
- def retry_strategy(self) -> RetryStrategy:
342
- """Get a RetryStrategy for Benchling."""
343
- return _DEFAULT_RETRY_STRATEGY
344
-
345
- @property
346
- def decryption_provider(self) -> Optional[BaseDecryptionProvider]:
347
- """Get a decryption provider for decryption app config secrets."""
348
- return None
349
-
350
-
351
- def init_app(
352
- app_id: str,
353
- benchling_provider: BenchlingProvider,
354
- tenant_url_provider: TenantUrlProvider,
355
- config_provider: Optional[ConfigProvider[ConfigType]] = None,
356
- ) -> App[ConfigType]:
357
- """
358
- Init App.
359
-
360
- Initializes a Benchling App with a series of functions to provide App dependencies at runtime.
361
- """
362
- if config_provider is None:
363
- config_provider = config_provider_error_on_call()
364
- return App(app_id, benchling_provider, tenant_url_provider, config_provider)
365
-
366
-
367
- def init_static_app(
368
- app_id: str, benchling: Benchling, config: Optional[ConfigType] = None
369
- ) -> App[ConfigType]:
370
- """
371
- Init Static App.
372
-
373
- Initializes a Benchling App with static values. Suitable for apps that communicate with a single URL.
374
- """
375
- tenant_url_provider = tenant_url_provider_static(benchling.client.base_url)
376
- benchling_provider = benchling_provider_static(benchling)
377
- config_provider = config_provider_error_on_call() if config is None else config_provider_static(config)
378
- return init_app(app_id, benchling_provider, tenant_url_provider, config_provider)
@@ -11,7 +11,7 @@ from benchling_sdk.helpers.logging_helpers import log_stability_warning, Stabili
11
11
  from benchling_sdk.helpers.package_helpers import _required_packages_context, ExtrasPackage
12
12
  from benchling_sdk.services.v2.stable.api_service import build_json_response
13
13
 
14
- log_stability_warning(StabilityLevel.ALPHA)
14
+ log_stability_warning(StabilityLevel.BETA)
15
15
 
16
16
 
17
17
  class WebhookVerificationError(Exception):
@@ -104,7 +104,7 @@ def verify_app_installation(
104
104
  Resolves JWKs from Benchling with default settings. Pass jwk_function for customization.
105
105
 
106
106
  This method will eventually be replaced with verify(app_definition_id) once Benchling Apps
107
- are migrated to global.
107
+ have global JWKs available for their app definition id.
108
108
  """
109
109
  _verify_headers_present(headers)
110
110
  _verify_timestamp(headers["webhook-timestamp"])
File without changes
@@ -0,0 +1,85 @@
1
+ from typing import List, Union
2
+
3
+ from benchling_sdk.models import AppSessionMessageCreate, AppSessionMessageStyle
4
+
5
+
6
+ class SessionClosedError(Exception):
7
+ """
8
+ Session Closed Error.
9
+
10
+ A session was inoperable because its status in Benchling was terminal.
11
+ """
12
+
13
+ pass
14
+
15
+
16
+ class SessionContextClosedError(Exception):
17
+ """
18
+ Session Context Closed Error.
19
+
20
+ An operation was attempted using the session context manager after it was closed.
21
+ """
22
+
23
+ pass
24
+
25
+
26
+ class InvalidSessionTimeoutError(Exception):
27
+ """
28
+ Invalid Session Timeout Error.
29
+
30
+ A session's timeout value was set to an invalid value.
31
+ """
32
+
33
+ pass
34
+
35
+
36
+ class MissingAttachedCanvasError(Exception):
37
+ """
38
+ Missing Attached Canvas Error.
39
+
40
+ A canvas operation was requested, but a session context has no attached canvas.
41
+ """
42
+
43
+ pass
44
+
45
+
46
+ class AppUserFacingError(Exception):
47
+ """
48
+ App User Facing Error.
49
+
50
+ Extend this class with custom exceptions you want to be written back to the user as a SessionMessage.
51
+
52
+ SessionClosingContextExitHandler will invoke messages() and write them to a user. Callers choosing to
53
+ write their own SessionContextExitHandler may need to replicate this behavior themselves.
54
+
55
+ This is useful for control flow where an app wants to terminate with an error state that is resolvable
56
+ by the user.
57
+
58
+ For example:
59
+
60
+ class InvalidUserInputError(AppUserFacingError):
61
+ pass
62
+
63
+ raise InvalidUserInputError("Please enter a number between 1 and 10")
64
+
65
+ This would create a message shown to the user like:
66
+
67
+ AppSessionMessageCreate("Please enter a number between 1 and 10", style=AppSessionMessageStyle.ERROR)
68
+ """
69
+
70
+ _messages: List[str]
71
+
72
+ def __init__(self, messages: Union[str, List[str]], *args) -> None:
73
+ """Initialize an AppUserFacingError with one message or a list."""
74
+ self._messages = [messages] if isinstance(messages, str) else messages
75
+ super().__init__(args)
76
+
77
+ def messages(self) -> List[AppSessionMessageCreate]:
78
+ """Create a series of AppSessionMessageCreate to write to a Session and displayed to the user."""
79
+ return [
80
+ AppSessionMessageCreate(content=message, style=AppSessionMessageStyle.ERROR)
81
+ for message in self._messages
82
+ ]
83
+
84
+ def __str__(self) -> str:
85
+ return "\n".join(self._messages) if self._messages else ""