benchling-sdk 1.9.0a4__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.
- benchling_sdk/apps/canvas/__init__.py +0 -0
- benchling_sdk/apps/canvas/errors.py +14 -0
- benchling_sdk/apps/{helpers/canvas_helpers.py → canvas/framework.py} +129 -188
- benchling_sdk/apps/canvas/types.py +125 -0
- benchling_sdk/apps/config/__init__.py +0 -3
- benchling_sdk/apps/config/decryption_provider.py +1 -1
- benchling_sdk/apps/config/errors.py +38 -0
- benchling_sdk/apps/config/framework.py +343 -0
- benchling_sdk/apps/config/helpers.py +157 -0
- benchling_sdk/apps/config/{mock_dependencies.py → mock_config.py} +78 -99
- benchling_sdk/apps/config/types.py +36 -0
- benchling_sdk/apps/framework.py +49 -338
- benchling_sdk/apps/helpers/webhook_helpers.py +2 -2
- benchling_sdk/apps/status/__init__.py +0 -0
- benchling_sdk/apps/status/errors.py +85 -0
- benchling_sdk/apps/{helpers/session_helpers.py → status/framework.py} +58 -167
- benchling_sdk/apps/status/helpers.py +20 -0
- benchling_sdk/apps/status/types.py +45 -0
- benchling_sdk/apps/types.py +3 -0
- benchling_sdk/errors.py +4 -4
- benchling_sdk/helpers/retry_helpers.py +3 -1
- benchling_sdk/models/__init__.py +44 -0
- benchling_sdk/services/v2/beta/{v2_beta_dataset_service.py → v2_beta_data_frame_service.py} +126 -116
- benchling_sdk/services/v2/stable/assay_result_service.py +18 -0
- benchling_sdk/services/v2/v2_beta_service.py +11 -11
- {benchling_sdk-1.9.0a4.dist-info → benchling_sdk-1.10.0.dist-info}/METADATA +4 -4
- {benchling_sdk-1.9.0a4.dist-info → benchling_sdk-1.10.0.dist-info}/RECORD +30 -21
- benchling_sdk/apps/config/dependencies.py +0 -1085
- benchling_sdk/apps/config/scalars.py +0 -226
- benchling_sdk/apps/helpers/config_helpers.py +0 -409
- /benchling_sdk/apps/{helpers → config}/cryptography_helpers.py +0 -0
- {benchling_sdk-1.9.0a4.dist-info → benchling_sdk-1.10.0.dist-info}/LICENSE +0 -0
- {benchling_sdk-1.9.0a4.dist-info → benchling_sdk-1.10.0.dist-info}/WHEEL +0 -0
benchling_sdk/apps/framework.py
CHANGED
@@ -1,137 +1,19 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
4
|
-
|
5
|
-
|
6
|
-
import
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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.
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
160
|
-
"""
|
161
|
-
|
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
|
65
|
+
self,
|
199
66
|
name: str,
|
200
67
|
timeout_seconds: int,
|
201
|
-
context_enter_handler: Optional[
|
202
|
-
context_exit_handler: Optional[
|
203
|
-
) ->
|
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
|
79
|
+
self,
|
216
80
|
session_id: str,
|
217
|
-
context_enter_handler: Optional[
|
218
|
-
context_exit_handler: Optional[
|
219
|
-
) ->
|
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.
|
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
|
-
|
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 ""
|