tol-sdk 1.6.37__py3-none-any.whl → 1.7.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 (44) hide show
  1. tol/api_base/blueprint.py +29 -6
  2. tol/api_base/controller.py +14 -5
  3. tol/api_client/api_datasource.py +15 -7
  4. tol/api_client/client.py +12 -6
  5. tol/api_client/converter.py +22 -8
  6. tol/api_client/factory.py +5 -3
  7. tol/api_client/view.py +75 -205
  8. tol/cli/cli.py +1 -1
  9. tol/core/__init__.py +1 -0
  10. tol/core/http_client.py +4 -2
  11. tol/core/operator/cursor.py +5 -3
  12. tol/core/operator/detail_getter.py +7 -15
  13. tol/core/operator/list_getter.py +3 -1
  14. tol/core/operator/page_getter.py +3 -1
  15. tol/core/operator/relational.py +9 -4
  16. tol/core/requested_fields.py +189 -0
  17. tol/elastic/elastic_datasource.py +2 -1
  18. tol/flows/converters/benchling_extraction_to_elastic_extraction_converter.py +25 -6
  19. tol/flows/converters/benchling_extraction_to_elastic_sequencing_request_converter.py +28 -7
  20. tol/flows/converters/benchling_sequencing_request_to_elastic_sequencing_request_converter.py +30 -9
  21. tol/flows/converters/benchling_tissue_prep_to_elastic_tissue_prep_converter.py +14 -3
  22. tol/flows/converters/elastic_sample_to_benchling_tissue_update_converter.py +1 -1
  23. tol/flows/converters/elastic_sample_to_elastic_sequencing_request_update_converter.py +4 -1
  24. tol/flows/converters/elastic_tolid_to_elastic_genome_note_update_converter.py +4 -1
  25. tol/flows/converters/elastic_tolid_to_elastic_sample_update_converter.py +4 -1
  26. tol/sources/sts.py +6 -2
  27. tol/sql/database.py +80 -44
  28. tol/sql/factory.py +2 -2
  29. tol/sql/filter.py +22 -20
  30. tol/sql/model.py +43 -38
  31. tol/sql/relationship.py +1 -1
  32. tol/sql/sql_converter.py +49 -142
  33. tol/sql/sql_datasource.py +85 -180
  34. tol/sql/{board → standard}/__init__.py +1 -1
  35. tol/sql/standard/factory.py +549 -0
  36. {tol_sdk-1.6.37.dist-info → tol_sdk-1.7.0.dist-info}/METADATA +1 -1
  37. {tol_sdk-1.6.37.dist-info → tol_sdk-1.7.0.dist-info}/RECORD +41 -42
  38. tol/sql/board/factory.py +0 -341
  39. tol/sql/loader/__init__.py +0 -6
  40. tol/sql/loader/factory.py +0 -246
  41. {tol_sdk-1.6.37.dist-info → tol_sdk-1.7.0.dist-info}/WHEEL +0 -0
  42. {tol_sdk-1.6.37.dist-info → tol_sdk-1.7.0.dist-info}/entry_points.txt +0 -0
  43. {tol_sdk-1.6.37.dist-info → tol_sdk-1.7.0.dist-info}/licenses/LICENSE +0 -0
  44. {tol_sdk-1.6.37.dist-info → tol_sdk-1.7.0.dist-info}/top_level.txt +0 -0
tol/sql/filter.py CHANGED
@@ -6,10 +6,10 @@ from __future__ import annotations
6
6
 
7
7
  from abc import ABC, abstractmethod
8
8
  from collections import defaultdict
9
- from collections.abc import MutableMapping
9
+ from collections.abc import Iterable, Iterator, MutableMapping
10
10
  from functools import reduce
11
11
  from itertools import chain
12
- from typing import Any, Dict, Iterable, Iterator, Optional, Tuple, Type
12
+ from typing import Any, Dict, Optional, Tuple, Type
13
13
 
14
14
  from sqlalchemy import BinaryExpression, cast, inspect, not_
15
15
  from sqlalchemy.dialects.postgresql import JSONB
@@ -30,8 +30,8 @@ class AliasTrie(MutableMapping[str, 'AliasTrie']):
30
30
  def alias(self) -> AliasedClass[Model]:
31
31
  return self.__alias
32
32
 
33
- def __getitem__(self, k: str) -> AliasTrie:
34
- return self.__dict[k]
33
+ def __getitem__(self, key: str) -> AliasTrie:
34
+ return self.__dict[key]
35
35
 
36
36
  def __setitem__(self, key: str, value: AliasTrie) -> None:
37
37
  self.__dict[key] = value
@@ -67,7 +67,9 @@ class DatabaseFilter(ABC):
67
67
  """Adds a relation field to the filter, for joining later"""
68
68
 
69
69
  @abstractmethod
70
- def get_query(self, session: Session, base_model: type[Model]) -> Query[Model]:
70
+ def get_query(
71
+ self, session: Session, base_model: type[Model]
72
+ ) -> tuple[Query[Model], AliasedClass[Model]]:
71
73
  """Gets an aliased query"""
72
74
 
73
75
 
@@ -103,7 +105,7 @@ class DefaultDatabaseFilter(DatabaseFilter):
103
105
  query = self.__apply_joins(
104
106
  query,
105
107
  self.__alias_trie,
106
- self.__base_alias,
108
+ self.__base_model,
107
109
  )
108
110
 
109
111
  if self.__filter is None:
@@ -117,13 +119,10 @@ class DefaultDatabaseFilter(DatabaseFilter):
117
119
 
118
120
  return query
119
121
 
120
- def get_query(self, session: Session, base_model: Model) -> Query[Model]:
121
- # TODO this is not thread safe
122
- self.__base_alias: AliasedClass[Model] = aliased(
123
- base_model,
124
- )
122
+ def get_query(self, session: Session, base_model: Model) -> [Query[Model]]:
123
+ self.__base_model = base_model
125
124
 
126
- return session.query(self.__base_alias)
125
+ return session.query(base_model)
127
126
 
128
127
  def __apply_joins(
129
128
  self,
@@ -135,6 +134,7 @@ class DefaultDatabaseFilter(DatabaseFilter):
135
134
  for part, trie in parent_trie.items():
136
135
  alias = trie.alias
137
136
 
137
+ # Probably want an outerjoin() here
138
138
  query = query.join(alias, getattr(parent_alias, part))
139
139
 
140
140
  query = self.__apply_joins(
@@ -160,11 +160,11 @@ class DefaultDatabaseFilter(DatabaseFilter):
160
160
  self.__rel_keys.add(field)
161
161
 
162
162
  def __build_alias_trie(self, paths: Iterable[str]) -> AliasTrie:
163
- trie = AliasTrie(self.__base_alias)
163
+ trie = AliasTrie(self.__base_model)
164
164
 
165
165
  for path in paths:
166
166
  parts = path.split('.')
167
- current_alias = self.__base_alias
167
+ current_alias = self.__base_model
168
168
  current = trie
169
169
  for part in parts[:-1]:
170
170
  if part not in current:
@@ -176,6 +176,8 @@ class DefaultDatabaseFilter(DatabaseFilter):
176
176
  )
177
177
  current[part] = step
178
178
  current = step
179
+ else:
180
+ current = current[part]
179
181
  current_alias = current.alias
180
182
 
181
183
  return trie
@@ -520,16 +522,16 @@ class DefaultDatabaseFilter(DatabaseFilter):
520
522
  return self.__get_column_attr(model, key)
521
523
 
522
524
  def __get_id_column(self, model: type[Model]) -> MappedColumn:
523
- og_model: type[Model] = model._aliased_insp.mapper.class_
525
+ og_model: type[Model] = inspect(model).mapper.class_
524
526
  id_key = og_model.get_id_column_name()
525
527
  return self.__get_column_attr(model, id_key)
526
528
 
527
529
  def __get_column_attr(self, model: AliasedClass[Model], key: str) -> MappedColumn:
528
- columns = {
529
- col.key: col
530
- for col in model._aliased_insp.selectable.c
531
- }
532
- return columns[key]
530
+ for col in inspect(model).selectable.c:
531
+ if col.key == key:
532
+ return col
533
+ msg = f"Failed to find column '{key}' in '{model}'"
534
+ raise ValueError(msg)
533
535
 
534
536
  def __get_relation_column(
535
537
  self,
tol/sql/model.py CHANGED
@@ -6,6 +6,8 @@ from __future__ import annotations
6
6
 
7
7
  from abc import ABCMeta
8
8
  from datetime import datetime
9
+ from functools import cache
10
+ from types import MappingProxyType # A read-only dict
9
11
  from typing import Any, Iterable, Optional, Type
10
12
 
11
13
  from sqlalchemy import JSON, inspect
@@ -198,30 +200,32 @@ def model_base() -> Type[DefaultModel]:
198
200
  return getattr(cls, name)
199
201
 
200
202
  @classmethod
201
- def get_to_many_relationship_config(cls) -> dict[str, str]:
203
+ @cache
204
+ def get_to_many_relationship_config(cls) -> MappingProxyType[str, str]:
202
205
  relationships = inspect(cls).relationships
203
206
  all_ = {
204
- cls.__get_relationshship_name(r): cls.__get_relationship_target(r)
207
+ r.key: cls.__get_relationship_target(r)
205
208
  for r in relationships
206
209
  if cls.__is_to_many_relationship(r)
207
210
  }
208
- return {
211
+ return MappingProxyType({
209
212
  k: v for k, v in all_.items()
210
213
  if not k.startswith('_')
211
- }
214
+ })
212
215
 
213
216
  @classmethod
214
- def get_to_one_relationship_config(cls) -> dict[str, str]:
217
+ @cache
218
+ def get_to_one_relationship_config(cls) -> MappingProxyType[str, str]:
215
219
  relationships = inspect(cls).relationships
216
220
  all_ = {
217
- cls.__get_relationshship_name(r): cls.__get_relationship_target(r)
221
+ r.key: cls.__get_relationship_target(r)
218
222
  for r in relationships
219
223
  if cls.__is_to_one_relationship(r)
220
224
  }
221
- return {
225
+ return MappingProxyType({
222
226
  k: v for k, v in all_.items()
223
227
  if not k.startswith('_')
224
- }
228
+ })
225
229
 
226
230
  @classmethod
227
231
  def get_foreign_key_name(cls, relationship_name: str) -> str:
@@ -230,12 +234,13 @@ def model_base() -> Type[DefaultModel]:
230
234
  return foreign_key.name
231
235
 
232
236
  @classmethod
233
- def get_attribute_types(cls) -> dict[str, type]:
237
+ @cache
238
+ def get_attribute_types(cls) -> MappingProxyType[str, type]:
234
239
  names = cls.__get_attribute_names()
235
240
  columns = inspect(cls).columns
236
- return {
241
+ return MappingProxyType({
237
242
  k: columns[k].type.python_type for k in names
238
- }
243
+ })
239
244
 
240
245
  @classmethod
241
246
  def get_id_attribute_type(cls) -> dict[str, type]:
@@ -268,13 +273,15 @@ def model_base() -> Type[DefaultModel]:
268
273
  cls,
269
274
  relationship_name: str
270
275
  ) -> MappedColumn:
276
+ """
277
+ Returns the local foreign key column given a relationship name
278
+ """
271
279
 
272
- relationships = inspect(cls).relationships
273
- all_keys = relationships[relationship_name]._calculated_foreign_keys
274
- if len(all_keys) != 1:
280
+ rel = inspect(cls).relationships[relationship_name]
281
+ all_cols = tuple(rel.local_columns)
282
+ if len(all_cols) != 1:
275
283
  raise NotImplementedError('Composite keys are not supported.')
276
- (foreign_key, ) = all_keys
277
- return foreign_key
284
+ return all_cols[0]
278
285
 
279
286
  def __get_attributes_map(
280
287
  self,
@@ -285,13 +292,9 @@ def model_base() -> Type[DefaultModel]:
285
292
  name: getattr(self, name) for name in names
286
293
  }
287
294
 
288
- @classmethod
289
- def __get_relationshship_name(cls, relationship) -> str:
290
- return str(relationship).split('.')[-1]
291
-
292
295
  @classmethod
293
296
  def __get_relationship_target(cls, relationship) -> str:
294
- return list(relationship.remote_side)[0].table.name
297
+ return relationship.entity.tables[0].name
295
298
 
296
299
  @classmethod
297
300
  def __get_all_relationship_names(cls) -> list[str]:
@@ -310,47 +313,49 @@ def model_base() -> Type[DefaultModel]:
310
313
  )
311
314
 
312
315
  @classmethod
313
- def get_all_foreign_key_names(cls) -> set[str]:
316
+ @cache
317
+ def get_all_foreign_key_names(cls) -> frozenset[str]:
314
318
  """
315
319
  Returns only the names of columns which are foreign keys used in
316
- relationsips.
320
+ relationsips. i.e. To-one relationships.
317
321
  """
318
322
 
323
+ col_to_attr = cls.__get_column_name_to_attribute_map()
324
+
319
325
  foreign_keys = set()
320
326
  for rel in inspect(cls).relationships:
321
327
  for col in rel.local_columns:
322
- # Test if it really is a foreign key
323
- if len(col.foreign_keys) > 0:
324
- if attr_name := cls.__get_foreign_key_attribute_name(col.name):
325
- foreign_keys.add(attr_name)
326
- return foreign_keys
328
+ # If it is really a foreign key, get the name of the
329
+ # attribute in the model
330
+ if len(col.foreign_keys) > 0 and (attr_name := col_to_attr.get(col.name)):
331
+ foreign_keys.add(attr_name)
332
+ return frozenset(foreign_keys)
327
333
 
328
334
  @classmethod
329
- def __get_foreign_key_attribute_name(cls, foreign_key_name: str) -> str | None:
335
+ def __get_column_name_to_attribute_map(cls) -> dict[str, str]:
330
336
  """
331
- Gets the model's attribute name in the class definition from the schema's
332
- `foreign_key_name`.
337
+ Returns a dict mapping database column names to this model's
338
+ attribute names.
333
339
  """
334
340
 
335
- col_to_attr = {
341
+ return {
336
342
  col_prop.columns[0].name: col_prop.key
337
343
  for col_prop in inspect(cls).column_attrs
338
344
  }
339
345
 
340
- return col_to_attr.get(foreign_key_name)
341
-
342
346
  @classmethod
343
- def __get_attribute_names(cls) -> list[str]:
347
+ @cache
348
+ def __get_attribute_names(cls) -> tuple[str]:
344
349
  excluded = cls.get_excluded_column_names()
345
350
  mapper = inspect(cls)
346
351
  relationships = cls.__get_all_relationship_names()
347
352
  foreign_keys = cls.get_all_foreign_key_names()
348
- return [
349
- k for k in mapper.attrs.keys()
353
+ return tuple(
354
+ k for k in mapper.attrs.keys() # noqa: SIM118
350
355
  if k not in excluded
351
356
  and k not in relationships
352
357
  and k not in foreign_keys
353
- ]
358
+ )
354
359
 
355
360
  class LogBase(ModelBase):
356
361
  """
tol/sql/relationship.py CHANGED
@@ -69,7 +69,7 @@ class DefaultSqlRelationshipConfig(ABC):
69
69
  else:
70
70
  return object_type, RelationshipConfig(
71
71
  to_one=self.__map_config(to_one),
72
- to_many=self.__map_config(to_many)
72
+ to_many=self.__map_config(to_many),
73
73
  )
74
74
 
75
75
  def __map_config(self, config: Dict[str, str]) -> Dict[str, str]:
tol/sql/sql_converter.py CHANGED
@@ -3,10 +3,10 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
5
  from abc import ABC
6
- from typing import Any, Callable, Optional, TypeVar
6
+ from typing import Any, Callable
7
7
 
8
8
  from .model import Model
9
- from ..core import DataObject
9
+ from ..core import DataObject, ReqFieldsTree
10
10
  from ..core.core_converter import Converter
11
11
  from ..core.factory import DataObjectFactory
12
12
 
@@ -15,14 +15,6 @@ TypeFunction = Callable[[Model], str]
15
15
  """Takes a Model instance, and returns the corresponding DataObject type."""
16
16
 
17
17
 
18
- In = TypeVar('In')
19
- """The input representation type"""
20
-
21
-
22
- Out = TypeVar('Out')
23
- """The output representation type"""
24
-
25
-
26
18
  class ModelConverter(Converter[Model, DataObject], ABC):
27
19
  """
28
20
  Converts Sqlalchemy model instances to DataObject instances.
@@ -30,13 +22,11 @@ class ModelConverter(Converter[Model, DataObject], ABC):
30
22
 
31
23
 
32
24
  class DefaultModelConverter(ModelConverter):
33
-
34
25
  def __init__(
35
26
  self,
36
27
  type_function: TypeFunction,
37
28
  data_object_factory: DataObjectFactory,
38
- max_depth: int = 1,
39
- requested_fields: list[str] | None = None,
29
+ requested_tree: ReqFieldsTree,
40
30
  ) -> None:
41
31
  """
42
32
  Takes a type_function Callable, which determines the type of the
@@ -45,116 +35,55 @@ class DefaultModelConverter(ModelConverter):
45
35
 
46
36
  self.__type_function = type_function
47
37
  self.__data_object_factory = data_object_factory
48
-
49
- self.__requested_fields = requested_fields
50
- self.__max_depth = (
51
- None
52
- if self.__requested_fields
53
- else max_depth
54
- )
38
+ self.__requested_tree = requested_tree
55
39
 
56
40
  def convert(self, model: Model) -> DataObject:
57
- return self.__convert_to_max_depth(
58
- model,
59
- self.__initial_marker
60
- )
61
-
62
- def __convert_to_max_depth(
63
- self,
64
- model: Model | None,
65
- marker: int
66
- ) -> DataObject:
67
-
68
41
  if model is None:
69
42
  return None
70
-
71
- type_ = self.__type_function(model)
72
-
73
- return self.__data_object_factory(
74
- type_,
75
- id_=model.instance_id,
76
- attributes=model.instance_attributes,
77
- to_one=self.__convert_to_ones(
78
- model,
79
- marker
80
- )
81
- )
82
-
83
- @property
84
- def __initial_marker(self) -> int | str:
85
- return (
86
- ''
87
- if self.__requested_fields
88
- else 0
89
- )
90
-
91
- def __convert_to_ones(
92
- self,
93
- model: Model,
94
- marker: int | str
95
- ) -> dict[str, Optional[DataObject]]:
96
-
97
- if self.__max_depth and marker >= self.__max_depth:
98
- return {}
99
-
100
- return {
101
- k: self.__convert_to_max_depth(
102
- model.instance_to_one_relations[k],
103
- self.__get_next_marker(
104
- k,
105
- marker,
106
- )
107
- )
108
- for k in self.__get_requested_to_ones(
109
- model,
110
- marker,
43
+ if tree := self.__requested_tree:
44
+ return self.__convert_requested(model, tree)
45
+ else:
46
+ return self.__data_object_factory(
47
+ self.__type_function(model),
48
+ id_=model.instance_id,
49
+ attributes=model.instance_attributes,
111
50
  )
112
- }
113
-
114
- def __get_next_marker(
115
- self,
116
- k: str,
117
- marker: int | str
118
- ) -> int | str:
119
51
 
120
- if self.__requested_fields:
121
- return f'{marker}.{k}' if marker else k
52
+ def __convert_requested(self, model, tree):
53
+ if attr_names := tree.attribute_names:
54
+ attributes = {x: getattr(model, x) for x in attr_names if x != 'id'}
122
55
  else:
123
- return marker + 1
124
-
125
- def __get_requested_to_ones(
126
- self,
127
- model_instance: Model,
128
- marker: int | str
129
- ) -> list[str]:
130
-
131
- all_keys = list(
132
- model_instance.get_to_one_relationship_config().keys()
133
- )
56
+ attributes = model.instance_attributes
134
57
 
135
- if not self.__requested_fields:
136
- return all_keys
58
+ req_to_ones = self.__convert_to_ones_requested(model, tree)
59
+ req_to_many = self.__convert_to_many_requested(model, tree)
137
60
 
138
- return [
139
- k for k in all_keys
140
- if self.__requested_to_one(k, marker)
141
- ]
142
-
143
- def __requested_to_one(
144
- self,
145
- k: str,
146
- marker: int | str
147
- ) -> bool:
148
-
149
- next_marker = self.__get_next_marker(
150
- k,
151
- marker,
152
- )
153
-
154
- return any(
155
- r.startswith(next_marker)
156
- for r in self.__requested_fields
61
+ obj = self.__data_object_factory(
62
+ self.__type_function(model),
63
+ id_=model.instance_id,
64
+ attributes=attributes,
65
+ to_one=req_to_ones,
66
+ to_many=req_to_many,
157
67
  )
68
+ return obj
69
+
70
+ def __convert_to_ones_requested(self, model, tree):
71
+ to_ones = {}
72
+ for rel_name, remote in model.get_to_one_relationship_config().items():
73
+ one = None
74
+ if sub_tree := tree.get_sub_tree(rel_name):
75
+ if sub_model := getattr(model, rel_name):
76
+ one = self.__convert_requested(sub_model, sub_tree)
77
+ else:
78
+ # Create a stub DataObject
79
+ rel_col = model.get_foreign_key_name(rel_name)
80
+ if rel_id := getattr(model, rel_col):
81
+ one = self.__data_object_factory(remote, id_=rel_id)
82
+ to_ones[rel_name] = one
83
+ return to_ones if to_ones else None
84
+
85
+ def __convert_to_many_requested(self, model, tree):
86
+ return None
158
87
 
159
88
 
160
89
  class DataObjectConverter(Converter[DataObject, Model], ABC):
@@ -164,11 +93,7 @@ class DataObjectConverter(Converter[DataObject, Model], ABC):
164
93
 
165
94
 
166
95
  class DefaultDataObjectConverter(DataObjectConverter):
167
-
168
- def __init__(
169
- self,
170
- type_models_dict: dict[str, type[Model]]
171
- ) -> None:
96
+ def __init__(self, type_models_dict: dict[str, type[Model]]) -> None:
172
97
  """
173
98
  `type_models_dict` maps object type to the
174
99
  corresponding `type[Model]` class.
@@ -182,40 +107,22 @@ class DefaultDataObjectConverter(DataObjectConverter):
182
107
  return model_class(
183
108
  **self.__get_id_dict(input_.id, model_class),
184
109
  **input_.attributes,
185
- **self.__get_relation_dict(
186
- model_class,
187
- input_._to_one_objects
188
- )
110
+ **self.__get_relation_dict(model_class, input_._to_one_objects),
189
111
  )
190
112
 
191
- def __get_id_dict(
192
- self,
193
- id_: str,
194
- model_class: type[Model]
195
- ) -> dict[str, str]:
196
-
113
+ def __get_id_dict(self, id_: str, model_class: type[Model]) -> dict[str, str]:
197
114
  id_column_name = model_class.get_id_column_name()
198
115
  return {id_column_name: id_}
199
116
 
200
117
  def __get_relation_dict(
201
- self,
202
- model_class: type[Model],
203
- ones: dict[str, DataObject]
118
+ self, model_class: type[Model], ones: dict[str, DataObject]
204
119
  ) -> dict[str, str]:
205
120
  # TODO validation - relationship names and their types
206
121
 
207
122
  return {
208
- model_class.get_foreign_key_name(
209
- rel_name
210
- ): self.__map_to_foreign_key(rel_obj)
123
+ model_class.get_foreign_key_name(rel_name): self.__map_to_foreign_key(rel_obj)
211
124
  for rel_name, rel_obj in ones.items()
212
125
  }
213
126
 
214
- def __map_to_foreign_key(
215
- self,
216
- rel_obj: DataObject | None
217
- ) -> Any | None:
218
-
219
- return (
220
- None if rel_obj is None else rel_obj.id
221
- )
127
+ def __map_to_foreign_key(self, rel_obj: DataObject | None) -> Any | None:
128
+ return None if rel_obj is None else rel_obj.id