benchling-sdk 1.10.0a4__py3-none-any.whl → 1.10.0a6__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.
@@ -1,13 +1,38 @@
1
- import collections
2
- from typing import Dict, List, Optional, OrderedDict, Protocol, Tuple
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, List, Optional, OrderedDict, Protocol, Tuple, Union
3
4
 
4
5
  from benchling_api_client.v2.extensions import UnknownType
5
6
  from ordered_set import OrderedSet
6
7
 
7
- from benchling_sdk.apps.config.errors import UnsupportedConfigItemError
8
+ from benchling_sdk.apps.config.errors import (
9
+ ConfigItemLinkedResourceError,
10
+ InaccessibleConfigItemError,
11
+ MissingRequiredConfigItemError,
12
+ UnsupportedConfigItemError,
13
+ )
8
14
  from benchling_sdk.apps.config.types import ConfigItemPath, ConfigurationReference
9
15
  from benchling_sdk.benchling import Benchling
10
- from benchling_sdk.models import AppConfigItem, ListAppConfigurationItemsSort
16
+ from benchling_sdk.models import (
17
+ AppConfigItem,
18
+ EntitySchemaAppConfigItem,
19
+ FieldAppConfigItem,
20
+ GenericApiIdentifiedAppConfigItem,
21
+ InaccessibleResource,
22
+ LinkedAppConfigResourceSummary,
23
+ ListAppConfigurationItemsSort,
24
+ )
25
+
26
+
27
+ class _DefaultOrderedDict(OrderedDict):
28
+ _root_path: List[str]
29
+
30
+ def __init__(self, root_path: List[str]) -> None:
31
+ super().__init__()
32
+ self._root_path = root_path
33
+
34
+ def __missing__(self, key):
35
+ return ConfigItemWrapper(None, [*self._root_path, key])
11
36
 
12
37
 
13
38
  class ConfigProvider(Protocol):
@@ -52,7 +77,8 @@ class BenchlingConfigProvider(ConfigProvider):
52
77
 
53
78
  # Eager load all config items for now since we don't yet have a way of lazily querying by path
54
79
  all_config_pages = list(app_pages)
55
- # Punt on UnknownType for now as apps using manifests with new types + older client could lead to unpredictable results
80
+ # Punt on UnknownType for now as apps using manifests with new types +
81
+ # older client could lead to unpredictable results
56
82
  all_config_items = [
57
83
  _supported_config_item(config_item) for page in all_config_pages for config_item in page
58
84
  ]
@@ -82,17 +108,126 @@ class StaticConfigProvider(ConfigProvider):
82
108
  return self._configuration_items
83
109
 
84
110
 
111
+ class ConfigItemWrapper:
112
+ """
113
+ Config Item Wrapper.
114
+
115
+ A decorator class for AppConfigItem to assist with typesafe access to its values.
116
+ Access the `item` attribute for the original config item, if present.
117
+ """
118
+
119
+ item: Optional[ConfigurationReference]
120
+ path: List[str]
121
+
122
+ def __init__(self, item: Optional[ConfigurationReference], path: List[str]) -> None:
123
+ """Init Pathed Config Item."""
124
+ self.item = item
125
+ self.path = path
126
+
127
+ def required(self) -> RequiredConfigItemWrapper:
128
+ """Return a `RequiredPathedConfigItem` to enforce that config item is not optional."""
129
+ if self.item is None:
130
+ raise MissingRequiredConfigItemError(
131
+ f"Required config item {self.path} is missing from the config store"
132
+ )
133
+ return RequiredConfigItemWrapper(self.item, self.path)
134
+
135
+ def linked_resource(self) -> Optional[LinkedAppConfigResourceSummary]:
136
+ """
137
+ Return an optional LinkedAppConfigResourceSummary.
138
+
139
+ Raises exceptions if the config item is not of the type that supports linked resources,
140
+ or the linked resource is inaccessible.
141
+ """
142
+ if self.item is not None:
143
+ # Python can't compare isinstance for Union types until Python 3.10 so just do this for now
144
+ # from: https://github.com/python/typing/discussions/1132#discussioncomment-2560441
145
+ # elif isinstance(self.item, get_args(LinkedResourceConfigurationReference)):
146
+ if isinstance(
147
+ self.item, (EntitySchemaAppConfigItem, FieldAppConfigItem, GenericApiIdentifiedAppConfigItem)
148
+ ):
149
+ if self.item.linked_resource is None:
150
+ return None
151
+ elif isinstance(self.item.linked_resource, LinkedAppConfigResourceSummary):
152
+ return self.item.linked_resource
153
+ elif isinstance(self.item.linked_resource, InaccessibleResource):
154
+ raise InaccessibleConfigItemError(
155
+ f"The linked resource for config item {self.path} was inaccessible. "
156
+ f"The caller does not have permissions to the resource."
157
+ )
158
+ raise UnsupportedConfigItemError(
159
+ f"Unable to read app configuration with unsupported type: {self.item}"
160
+ )
161
+ raise ConfigItemLinkedResourceError(
162
+ f"Type mismatch: The config item {self.item} type never has a linked resource"
163
+ )
164
+ return None
165
+
166
+ def value(self) -> Union[str, float, int, bool, None]:
167
+ """Return the value of the config item, if present."""
168
+ return self.item.value if self.item else None
169
+
170
+ def value_str(self) -> Optional[str]:
171
+ """Return the value of the config item as a string, if present."""
172
+ return str(self.value()) if self.value() else None
173
+
174
+ def __bool__(self) -> bool:
175
+ return self.value() is not None
176
+
177
+
178
+ class RequiredConfigItemWrapper(ConfigItemWrapper):
179
+ """
180
+ Required Config Item Wrapper.
181
+
182
+ A decorator class for AppConfigItem to assist with typesafe access to its values.
183
+ Enforces that a config item is present, and that it's value is not None.
184
+ Access the `item` attribute for the original config item.
185
+ """
186
+
187
+ item: ConfigurationReference
188
+
189
+ def __init__(self, item: ConfigurationReference, path: List[str]) -> None:
190
+ """Init Required Pathed Config Item."""
191
+ super().__init__(item, path)
192
+
193
+ def linked_resource(self) -> LinkedAppConfigResourceSummary:
194
+ """
195
+ Return a LinkedAppConfigResourceSummary.
196
+
197
+ Raises exceptions if the config item is not of the type that supports linked resources,
198
+ or the linked resource is inaccessible.
199
+ """
200
+ linked_resource = super().linked_resource()
201
+ if linked_resource is None:
202
+ raise MissingRequiredConfigItemError(
203
+ f"Required config item {self.path} is missing a linked resource"
204
+ )
205
+ return linked_resource
206
+
207
+ def value(self) -> Union[str, float, int, bool]:
208
+ """Return the value of the config item."""
209
+ if self.item.value is None:
210
+ raise MissingRequiredConfigItemError(
211
+ f"Required config item {self.path} is missing from the config store"
212
+ )
213
+ return self.item.value
214
+
215
+ def value_str(self) -> str:
216
+ """Return the value of the config item as a string."""
217
+ return str(self.value())
218
+
219
+
85
220
  class ConfigItemStore:
86
221
  """
87
222
  Dependency Link Store.
88
223
 
89
- Marshalls an app configuration from the configuration provider into an indexable structure.
224
+ Marshals an app configuration from the configuration provider into an indexed structure.
90
225
  Only retrieves app configuration once unless its cache is invalidated.
91
226
  """
92
227
 
93
228
  _configuration_provider: ConfigProvider
94
229
  _configuration: Optional[List[ConfigurationReference]] = None
95
- _configuration_map: Optional[Dict[ConfigItemPath, ConfigurationReference]] = None
230
+ _configuration_dict: Optional[Dict[ConfigItemPath, ConfigItemWrapper]] = None
96
231
  _array_path_row_names: Dict[Tuple[str, ...], OrderedSet[str]] = dict()
97
232
 
98
233
  def __init__(self, configuration_provider: ConfigProvider):
@@ -118,17 +253,19 @@ class ConfigItemStore:
118
253
  return self._configuration
119
254
 
120
255
  @property
121
- def configuration_path_map(self) -> Dict[ConfigItemPath, ConfigurationReference]:
256
+ def configuration_path_dict(self) -> Dict[ConfigItemPath, ConfigItemWrapper]:
122
257
  """
123
258
  Config links.
124
259
 
125
- Return a map of configuration item paths to their corresponding configuration items.
260
+ Return a dict of configuration item paths to their corresponding configuration items.
126
261
  """
127
- if not self._configuration_map:
128
- self._configuration_map = {tuple(item.path): item for item in self.configuration}
129
- return self._configuration_map
262
+ if not self._configuration_dict:
263
+ self._configuration_dict = {
264
+ tuple(item.path): ConfigItemWrapper(item, item.path) for item in self.configuration
265
+ }
266
+ return self._configuration_dict
130
267
 
131
- def config_by_path(self, path: List[str]) -> Optional[ConfigurationReference]:
268
+ def config_by_path(self, path: List[str]) -> ConfigItemWrapper:
132
269
  """
133
270
  Config by path.
134
271
 
@@ -136,7 +273,7 @@ class ConfigItemStore:
136
273
  """
137
274
  # Since we eager load all config now, we know that missing path means it's not configured in Benchling
138
275
  # Later if we support lazy loading, we'll need to differentiate what's in our cache versus missing
139
- return self.configuration_path_map.get(tuple(path))
276
+ return self.configuration_path_dict.get(tuple(path), ConfigItemWrapper(None, path))
140
277
 
141
278
  def config_keys_by_path(self, path: List[str]) -> OrderedSet[str]:
142
279
  """
@@ -160,7 +297,7 @@ class ConfigItemStore:
160
297
  self._array_path_row_names[path_tuple] = OrderedSet(
161
298
  [
162
299
  config_item.path[len(path)]
163
- # Use the list instead of configuration_map to preserve order
300
+ # Use the list instead of configuration_dict to preserve order
164
301
  for config_item in self.configuration
165
302
  # The +1 is the name of the array row
166
303
  if len(config_item.path) >= len(path) + 1
@@ -171,22 +308,21 @@ class ConfigItemStore:
171
308
  )
172
309
  return self._array_path_row_names[path_tuple]
173
310
 
174
- def array_rows_to_dict(self, path: List[str]) -> OrderedDict[str, Dict[str, ConfigurationReference]]:
311
+ def array_rows_to_dict(self, path: List[str]) -> OrderedDict[str, Dict[str, ConfigItemWrapper]]:
175
312
  """Given a path to the root of a config array, return each element as a named dict."""
176
- # TODO BNCH-52772 Improve docstring if we keep this method
177
313
  array_keys = self.config_keys_by_path(path)
178
314
  # Although we don't have a way of preserving order when pulling array elements from the API right now
179
315
  # we should intentionally order these to accommodate a potential ordered future
180
- array_elements_map = collections.OrderedDict()
316
+ array_elements_map = _DefaultOrderedDict(root_path=path)
181
317
  for key in array_keys:
318
+ array_elements_map[key] = _DefaultOrderedDict(root_path=[*path, key])
182
319
  # Don't care about order for the keys within a row, only the order of the rows themselves
183
- array_elements_map[key] = {
184
- array_element_key: self.config_by_path([*path, key, array_element_key])
185
- for array_element_key in self.config_keys_by_path([*path, key])
186
- if self.config_by_path([*path, key, array_element_key]) is not None
187
- }
188
- # TODO BNCH-52772 MyPy thinks the inner dict values can be None
189
- return array_elements_map # type: ignore
320
+ for array_element_key in self.config_keys_by_path([*path, key]):
321
+ if self.config_by_path([*path, key, array_element_key]) is not None:
322
+ array_elements_map[key][array_element_key] = self.config_by_path(
323
+ [*path, key, array_element_key]
324
+ )
325
+ return array_elements_map
190
326
 
191
327
  def invalidate_cache(self) -> None:
192
328
  """
@@ -195,7 +331,7 @@ class ConfigItemStore:
195
331
  Will force retrieval of configuration from the ConfigProvider the next time the link store is accessed.
196
332
  """
197
333
  self._configuration = None
198
- self._configuration_map = None
334
+ self._configuration_dict = None
199
335
  self._array_path_row_names = dict()
200
336
 
201
337
 
@@ -1,7 +1,6 @@
1
- from datetime import date, datetime
1
+ from datetime import datetime
2
2
  from typing import List, Optional, Union
3
3
 
4
- from benchling_api_client.v2.beta.models.app_config_field_type import AppConfigFieldType
5
4
  from benchling_api_client.v2.beta.models.base_manifest_config import BaseManifestConfig
6
5
  from benchling_api_client.v2.beta.models.dropdown_dependency import DropdownDependency
7
6
  from benchling_api_client.v2.beta.models.dropdown_dependency_types import DropdownDependencyTypes
@@ -29,92 +28,7 @@ from benchling_api_client.v2.beta.models.workflow_task_schema_dependency_type im
29
28
  from benchling_api_client.v2.extensions import UnknownType
30
29
  from benchling_api_client.v2.stable.extensions import NotPresentError
31
30
 
32
- from benchling_sdk.apps.config.scalars import JsonType
33
- from benchling_sdk.models import (
34
- AaSequence,
35
- AssayResult,
36
- AssayRun,
37
- Box,
38
- Container,
39
- CustomEntity,
40
- DnaOligo,
41
- DnaSequence,
42
- Entry,
43
- Location,
44
- Mixture,
45
- Molecule,
46
- Plate,
47
- Request,
48
- RnaOligo,
49
- RnaSequence,
50
- WorkflowTask,
51
- )
52
-
53
- _MODEL_TYPES_FROM_SCHEMA_TYPE = {
54
- SchemaDependencyTypes.CONTAINER_SCHEMA: Container,
55
- SchemaDependencyTypes.PLATE_SCHEMA: Plate,
56
- SchemaDependencyTypes.BOX_SCHEMA: Box,
57
- SchemaDependencyTypes.LOCATION_SCHEMA: Location,
58
- SchemaDependencyTypes.ENTRY_SCHEMA: Entry,
59
- SchemaDependencyTypes.REQUEST_SCHEMA: Request,
60
- SchemaDependencyTypes.RESULT_SCHEMA: AssayResult,
61
- SchemaDependencyTypes.RUN_SCHEMA: AssayRun,
62
- SchemaDependencyTypes.WORKFLOW_TASK_SCHEMA: WorkflowTask,
63
- }
64
-
65
-
66
- _SCALAR_TYPES_FROM_CONFIG = {
67
- ScalarConfigTypes.BOOLEAN: bool,
68
- ScalarConfigTypes.DATE: date,
69
- ScalarConfigTypes.DATETIME: datetime,
70
- ScalarConfigTypes.FLOAT: float,
71
- ScalarConfigTypes.INTEGER: int,
72
- ScalarConfigTypes.JSON: JsonType,
73
- ScalarConfigTypes.TEXT: str,
74
- }
75
-
76
-
77
- _FIELD_SCALAR_TYPES_FROM_CONFIG = {
78
- AppConfigFieldType.BOOLEAN: bool,
79
- AppConfigFieldType.DATE: date,
80
- AppConfigFieldType.DATETIME: datetime,
81
- AppConfigFieldType.FLOAT: float,
82
- AppConfigFieldType.INTEGER: int,
83
- AppConfigFieldType.JSON: JsonType,
84
- AppConfigFieldType.TEXT: str,
85
- }
86
-
87
-
88
- ModelType = Union[AssayResult, AssayRun, Box, Container, Entry, Location, Plate, Request]
89
-
90
- _INSTANCE_FROM_SCHEMA_SUBTYPE = {
91
- SchemaDependencySubtypes.AA_SEQUENCE: AaSequence,
92
- SchemaDependencySubtypes.CUSTOM_ENTITY: CustomEntity,
93
- SchemaDependencySubtypes.DNA_SEQUENCE: DnaSequence,
94
- SchemaDependencySubtypes.DNA_OLIGO: DnaOligo,
95
- SchemaDependencySubtypes.MIXTURE: Mixture,
96
- SchemaDependencySubtypes.MOLECULE: Molecule,
97
- SchemaDependencySubtypes.RNA_OLIGO: RnaOligo,
98
- SchemaDependencySubtypes.RNA_SEQUENCE: RnaSequence,
99
- }
100
-
101
- EntitySubtype = Union[
102
- AaSequence, CustomEntity, DnaOligo, DnaSequence, Mixture, Molecule, RnaOligo, RnaSequence
103
- ]
104
-
105
- AnyDependency = Union[
106
- BaseManifestConfig,
107
- DropdownDependency,
108
- EntitySchemaDependency,
109
- FieldDefinitionsManifest,
110
- ManifestArrayConfig,
111
- ManifestScalarConfig,
112
- ResourceDependency,
113
- SchemaDependency,
114
- WorkflowTaskSchemaDependency,
115
- ]
116
-
117
- ArrayElementDependency = Union[
31
+ _ArrayElementDependency = Union[
118
32
  SchemaDependency,
119
33
  EntitySchemaDependency,
120
34
  WorkflowTaskSchemaDependency,
@@ -124,13 +38,13 @@ ArrayElementDependency = Union[
124
38
  ]
125
39
 
126
40
 
127
- class UnsupportedSubTypeError(Exception):
41
+ class _UnsupportedSubTypeError(Exception):
128
42
  """Error when an unsupported subtype is encountered."""
129
43
 
130
44
  pass
131
45
 
132
46
 
133
- def field_definitions_from_dependency(
47
+ def _field_definitions_from_dependency(
134
48
  dependency: Union[
135
49
  EntitySchemaDependency,
136
50
  SchemaDependency,
@@ -148,7 +62,7 @@ def field_definitions_from_dependency(
148
62
  return []
149
63
 
150
64
 
151
- def element_definition_from_dependency(dependency: ManifestArrayConfig) -> List[ArrayElementDependency]:
65
+ def _element_definition_from_dependency(dependency: ManifestArrayConfig) -> List[_ArrayElementDependency]:
152
66
  """Safely return an element definition as a list of dependencies from an array dependency or empty list."""
153
67
  try:
154
68
  if hasattr(dependency, "element_definition"):
@@ -161,7 +75,7 @@ def element_definition_from_dependency(dependency: ManifestArrayConfig) -> List[
161
75
  return []
162
76
 
163
77
 
164
- def enum_from_dependency(
78
+ def _enum_from_dependency(
165
79
  dependency: Union[
166
80
  ManifestFloatScalarConfig,
167
81
  ManifestIntegerScalarConfig,
@@ -180,8 +94,8 @@ def enum_from_dependency(
180
94
 
181
95
  # TODO BNCH-57036 All element definitions currently deserialize to UnknownType. Hack around this temporarily
182
96
  def _fix_element_definition_deserialization(
183
- element: Union[UnknownType, ArrayElementDependency]
184
- ) -> ArrayElementDependency:
97
+ element: Union[UnknownType, _ArrayElementDependency]
98
+ ) -> _ArrayElementDependency:
185
99
  if isinstance(element, UnknownType):
186
100
  if "type" in element.value:
187
101
  element_type = element.value["type"]
@@ -201,7 +115,7 @@ def _fix_element_definition_deserialization(
201
115
  return element
202
116
 
203
117
 
204
- def workflow_task_schema_output_from_dependency(
118
+ def _workflow_task_schema_output_from_dependency(
205
119
  dependency: WorkflowTaskSchemaDependency,
206
120
  ) -> Optional[WorkflowTaskSchemaDependencyOutput]:
207
121
  """Safely return a workflow task schema output from a workflow task schema or None."""
@@ -214,7 +128,7 @@ def workflow_task_schema_output_from_dependency(
214
128
  return None
215
129
 
216
130
 
217
- def options_from_dependency(dependency: DropdownDependency) -> List[BaseManifestConfig]:
131
+ def _options_from_dependency(dependency: DropdownDependency) -> List[BaseManifestConfig]:
218
132
  """Safely return a list of options from a dropdown dependency or empty list."""
219
133
  try:
220
134
  if hasattr(dependency, "options"):
@@ -225,7 +139,7 @@ def options_from_dependency(dependency: DropdownDependency) -> List[BaseManifest
225
139
  return []
226
140
 
227
141
 
228
- def subtype_from_entity_schema_dependency(
142
+ def _subtype_from_entity_schema_dependency(
229
143
  dependency: EntitySchemaDependency,
230
144
  ) -> Optional[SchemaDependencySubtypes]:
231
145
  """Safely return an entity schema dependency's subtype, if present."""
@@ -236,3 +150,8 @@ def subtype_from_entity_schema_dependency(
236
150
  except NotPresentError:
237
151
  pass
238
152
  return None
153
+
154
+
155
+ def datetime_config_value_to_str(value: datetime) -> str:
156
+ """Convert a datetime value to a valid string accepted by a datetime app config item."""
157
+ return value.strftime("%Y-%m-%d %I:%M:%S %p")