tol-sdk 1.7.5b0__py3-none-any.whl → 1.7.5b3__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.
- tol/core/operator/detail_getter.py +1 -1
- tol/ena/client.py +3 -2
- tol/flows/converters/__init__.py +2 -0
- tol/flows/converters/incoming_sample_to_ena_sample_converter.py +130 -0
- tol/flows/converters/incoming_sample_to_incoming_sample_with_lists_converter.py +46 -0
- tol/sql/auth/models.py +21 -12
- tol/validators/__init__.py +2 -1
- tol/validators/allowed_keys.py +2 -1
- tol/validators/allowed_values.py +2 -1
- tol/validators/allowed_values_from_datasource.py +9 -10
- tol/validators/assert_on_condition.py +6 -6
- tol/validators/converter_and_validate.py +93 -0
- tol/validators/ena_checklist.py +73 -0
- tol/validators/ena_submittable.py +1 -0
- tol/validators/interfaces/__init__.py +1 -1
- tol/validators/interfaces/condition_evaluator.py +42 -1
- tol/validators/min_one_valid_value.py +1 -0
- tol/validators/mutually_exclusive.py +12 -8
- tol/validators/regex.py +29 -4
- tol/validators/regex_by_value.py +24 -4
- tol/validators/specimens_have_same_taxon.py +1 -1
- tol/validators/sts_fields.py +1 -0
- tol/validators/tolid.py +2 -1
- tol/validators/unique_values.py +2 -1
- tol/validators/unique_whole_organisms.py +1 -1
- {tol_sdk-1.7.5b0.dist-info → tol_sdk-1.7.5b3.dist-info}/METADATA +1 -1
- {tol_sdk-1.7.5b0.dist-info → tol_sdk-1.7.5b3.dist-info}/RECORD +31 -27
- {tol_sdk-1.7.5b0.dist-info → tol_sdk-1.7.5b3.dist-info}/WHEEL +0 -0
- {tol_sdk-1.7.5b0.dist-info → tol_sdk-1.7.5b3.dist-info}/entry_points.txt +0 -0
- {tol_sdk-1.7.5b0.dist-info → tol_sdk-1.7.5b3.dist-info}/licenses/LICENSE +0 -0
- {tol_sdk-1.7.5b0.dist-info → tol_sdk-1.7.5b3.dist-info}/top_level.txt +0 -0
tol/ena/client.py
CHANGED
|
@@ -116,12 +116,13 @@ class EnaApiClient(HttpClient):
|
|
|
116
116
|
Returns the URL and parameters for a detail query.
|
|
117
117
|
"""
|
|
118
118
|
if object_type == 'checklist':
|
|
119
|
-
ids = ','.join(object_ids)
|
|
119
|
+
ids = ','.join(str(id_) for id_ in object_ids)
|
|
120
120
|
url = f'{self.__ena_url}/ena/browser/api/xml/{ids}'
|
|
121
121
|
params = {}
|
|
122
122
|
return url, params
|
|
123
123
|
if object_type == 'submittable_taxon':
|
|
124
|
-
|
|
124
|
+
# This is actually called separately for each taxon id
|
|
125
|
+
ids = ','.join(str(id_) for id_ in object_ids)
|
|
125
126
|
url = f'{self.__ena_url}/ena/taxonomy/rest/tax-id/{ids}'
|
|
126
127
|
params = {}
|
|
127
128
|
return url, params
|
tol/flows/converters/__init__.py
CHANGED
|
@@ -34,6 +34,8 @@ from .gap_assembly_to_elastic_assembly_analysis_converter import GapAssemblyToEl
|
|
|
34
34
|
from .genome_notes_genome_note_to_elastic_genome_note_converter import GenomeNotesGenomeNoteToElasticGenomeNoteConverter # noqa F401
|
|
35
35
|
from .goat_taxon_to_elastic_species_converter import GoatTaxonToElasticSpeciesConverter # noqa F401
|
|
36
36
|
from .grit_issue_to_elastic_curation_converter import GritIssueToElasticCurationConverter # noqa F401
|
|
37
|
+
from .incoming_sample_to_ena_sample_converter import IncomingSampleToEnaSampleConverter # noqa
|
|
38
|
+
from .incoming_sample_to_incoming_sample_with_lists_converter import IncomingSampleToIncomingSampleWithListsConverter # noqa F401
|
|
37
39
|
from .informatics_tolid_to_elastic_tolid_converter import InformaticsTolidToElasticTolidConverter # noqa F401
|
|
38
40
|
from .labwhere_location_to_elastic_sample_update_converter import LabwhereLocationToElasticSampleUpdateConverter # noqa F401
|
|
39
41
|
from .labwhere_location_to_sts_tray_converter import LabwhereLocationToStsTrayConverter # noqa F401
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Genome Research Ltd.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
from tol.core import DataObject, DataObjectToDataObjectOrUpdateConverter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IncomingSampleToEnaSampleConverter(DataObjectToDataObjectOrUpdateConverter):
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
14
|
+
class Config:
|
|
15
|
+
ena_checklist_id: str
|
|
16
|
+
project_name: str
|
|
17
|
+
|
|
18
|
+
__slots__ = ['__config']
|
|
19
|
+
__config: Config
|
|
20
|
+
|
|
21
|
+
def __init__(self, data_object_factory, config: Config) -> None:
|
|
22
|
+
super().__init__(data_object_factory)
|
|
23
|
+
self.__config = config
|
|
24
|
+
self._data_object_factory = data_object_factory
|
|
25
|
+
|
|
26
|
+
def convert(self, data_object: DataObject) -> Iterable[DataObject]:
|
|
27
|
+
"""
|
|
28
|
+
converting the samples DataObject into ENA format
|
|
29
|
+
"""
|
|
30
|
+
s = data_object
|
|
31
|
+
attributes = {
|
|
32
|
+
'ENA-CHECKLIST': self.__config.ena_checklist_id,
|
|
33
|
+
'organism part': self.__replace_underscores(
|
|
34
|
+
s.attributes.get('ORGANISM_PART')),
|
|
35
|
+
'lifestage': (
|
|
36
|
+
'spore-bearing structure'
|
|
37
|
+
if s.attributes.get('LIFESTAGE') == 'SPORE_BEARING_STRUCTURE'
|
|
38
|
+
else self.__replace_underscores(
|
|
39
|
+
s.attributes.get('LIFESTAGE'))
|
|
40
|
+
),
|
|
41
|
+
'project name':
|
|
42
|
+
self.__config.project_name,
|
|
43
|
+
'collected by':
|
|
44
|
+
self.__replace_underscores(
|
|
45
|
+
s.attributes.get('COLLECTED_BY')),
|
|
46
|
+
'collection date':
|
|
47
|
+
self.__replace_underscores(
|
|
48
|
+
s.attributes.get('DATE_OF_COLLECTION')).lower(),
|
|
49
|
+
'geographic location (country and/or sea)':
|
|
50
|
+
self.__collection_country(s).replace('_', ' '),
|
|
51
|
+
'geographic location (latitude)':
|
|
52
|
+
self.__replace_underscores(
|
|
53
|
+
s.attributes.get('DECIMAL_LATITUDE')).lower(),
|
|
54
|
+
'geographic location (latitude) units':
|
|
55
|
+
'DD',
|
|
56
|
+
'geographic location (longitude)':
|
|
57
|
+
self.__replace_underscores(
|
|
58
|
+
s.attributes.get('DECIMAL_LONGITUDE')).lower(),
|
|
59
|
+
'geographic location (longitude) units':
|
|
60
|
+
'DD',
|
|
61
|
+
'geographic location (region and locality)':
|
|
62
|
+
self.__collection_region(s).replace('_', ' '),
|
|
63
|
+
'identified_by':
|
|
64
|
+
self.__replace_underscores(
|
|
65
|
+
s.attributes.get('IDENTIFIED_BY')),
|
|
66
|
+
'habitat':
|
|
67
|
+
self.__replace_underscores(
|
|
68
|
+
s.attributes.get('HABITAT')),
|
|
69
|
+
'identifier_affiliation':
|
|
70
|
+
self.__replace_underscores(
|
|
71
|
+
s.attributes.get('IDENTIFIER_AFFILIATION')),
|
|
72
|
+
'sex':
|
|
73
|
+
self.__replace_underscores(
|
|
74
|
+
s.attributes.get('SEX')),
|
|
75
|
+
'relationship':
|
|
76
|
+
self.__replace_underscores(
|
|
77
|
+
s.attributes.get('RELATIONSHIP')),
|
|
78
|
+
'SYMBIONT':
|
|
79
|
+
'Y' if s.attributes.get('SYMBIONT') == 'SYMBIONT' else 'N',
|
|
80
|
+
'collecting institution':
|
|
81
|
+
self.__replace_underscores(
|
|
82
|
+
s.attributes.get('COLLECTOR_AFFILIATION'))
|
|
83
|
+
}
|
|
84
|
+
if self.__sanitise(s.attributes.get('DEPTH')) != '':
|
|
85
|
+
attributes['geographic location (depth)'] = s.attributes.get('DEPTH')
|
|
86
|
+
attributes['geographic location (depth) units'] = 'm'
|
|
87
|
+
if self.__sanitise(s.attributes.get('ELEVATION')) != '':
|
|
88
|
+
attributes['geographic location (elevation)'] = s.attributes.get('ELEVATION')
|
|
89
|
+
attributes['geographic location (elevation) units'] = 'm'
|
|
90
|
+
if self.__sanitise(s.attributes.get('ORIGINAL_COLLECTION_DATE')) != '':
|
|
91
|
+
attributes['original collection date'] = s.attributes.get('ORIGINAL_COLLECTION_DATE')
|
|
92
|
+
if self.__sanitise(s.attributes.get('ORIGINAL_GEOGRAPHIC_LOCATION')) != '':
|
|
93
|
+
attributes['original geographic location'] = self.__replace_underscores(s.attributes.get('ORIGINAL_GEOGRAPHIC_LOCATION')) # noqa
|
|
94
|
+
if s.attributes.get('GAL') is not None:
|
|
95
|
+
attributes['GAL'] = s.attributes.get('GAL')
|
|
96
|
+
if s.attributes.get('VOUCHER_ID') is not None:
|
|
97
|
+
attributes['specimen_voucher'] = s.attributes.get('VOUCHER_ID')
|
|
98
|
+
if s.attributes.get('SPECIMEN_ID') is not None:
|
|
99
|
+
attributes['specimen_id'] = s.attributes.get('SPECIMEN_ID')
|
|
100
|
+
if s.attributes.get('GAL_SAMPLE_ID') is not None:
|
|
101
|
+
attributes['GAL_sample_id'] = s.attributes.get('GAL_SAMPLE_ID')
|
|
102
|
+
if s.attributes.get('CULTURE_OR_STRAIN_ID') is not None:
|
|
103
|
+
attributes['culture_or_strain_id'] = s.attributes.get('CULTURE_OR_STRAIN_ID')
|
|
104
|
+
|
|
105
|
+
ret = self._data_object_factory(
|
|
106
|
+
'sample',
|
|
107
|
+
s.id,
|
|
108
|
+
attributes=attributes,
|
|
109
|
+
)
|
|
110
|
+
yield ret
|
|
111
|
+
|
|
112
|
+
def __collection_country(self, data_object: DataObject):
|
|
113
|
+
return re.split(
|
|
114
|
+
r'\s*\|\s*',
|
|
115
|
+
data_object.attributes.get('COLLECTION_LOCATION'))[0]
|
|
116
|
+
|
|
117
|
+
def __collection_region(self, data_object: DataObject):
|
|
118
|
+
return ' | '.join(re.split(
|
|
119
|
+
r'\s*\|\s*',
|
|
120
|
+
data_object.attributes.get('COLLECTION_LOCATION'))[1:])
|
|
121
|
+
|
|
122
|
+
def __replace_underscores(self, value):
|
|
123
|
+
if type(value) != str:
|
|
124
|
+
return value
|
|
125
|
+
return self.__sanitise(value, '').replace('_', ' ')
|
|
126
|
+
|
|
127
|
+
def __sanitise(self, value, default_value=''):
|
|
128
|
+
if value is None:
|
|
129
|
+
return default_value
|
|
130
|
+
return value
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Genome Research Ltd.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from tol.core import DataObject, DataObjectToDataObjectOrUpdateConverter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IncomingSampleToIncomingSampleWithListsConverter(DataObjectToDataObjectOrUpdateConverter):
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
13
|
+
class Config:
|
|
14
|
+
fields_to_convert: str
|
|
15
|
+
separator: str = '|'
|
|
16
|
+
|
|
17
|
+
__slots__ = ['__config']
|
|
18
|
+
__config: Config
|
|
19
|
+
|
|
20
|
+
def __init__(self, data_object_factory, config: Config) -> None:
|
|
21
|
+
super().__init__(data_object_factory)
|
|
22
|
+
self.__config = config
|
|
23
|
+
self._data_object_factory = data_object_factory
|
|
24
|
+
|
|
25
|
+
def convert(self, data_object: DataObject) -> Iterable[DataObject]:
|
|
26
|
+
"""
|
|
27
|
+
converting the samples DataObject into ENA format
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
ret = self._data_object_factory(
|
|
31
|
+
data_object.type,
|
|
32
|
+
data_object.id,
|
|
33
|
+
attributes={
|
|
34
|
+
k: v for k, v in data_object.attributes.items()
|
|
35
|
+
if k not in self.__config.fields_to_convert
|
|
36
|
+
} | {
|
|
37
|
+
field: self.__convert_to_list(data_object.get_field_by_name(field))
|
|
38
|
+
for field in self.__config.fields_to_convert
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
yield ret
|
|
42
|
+
|
|
43
|
+
def __convert_to_list(self, value: str | None) -> list[str]:
|
|
44
|
+
if not value:
|
|
45
|
+
return []
|
|
46
|
+
return [item.strip() for item in value.split(self.__config.separator)]
|
tol/sql/auth/models.py
CHANGED
|
@@ -19,6 +19,7 @@ from sqlalchemy.ext.declarative import declared_attr
|
|
|
19
19
|
from sqlalchemy.orm import (
|
|
20
20
|
Mapped,
|
|
21
21
|
Session,
|
|
22
|
+
joinedload,
|
|
22
23
|
mapped_column,
|
|
23
24
|
relationship
|
|
24
25
|
)
|
|
@@ -184,7 +185,7 @@ def create_models(
|
|
|
184
185
|
|
|
185
186
|
id: Mapped[str] = mapped_column( # noqa A003
|
|
186
187
|
primary_key=True,
|
|
187
|
-
name=
|
|
188
|
+
name='state_id' if prefix_with_name else None
|
|
188
189
|
)
|
|
189
190
|
|
|
190
191
|
created_at: Mapped[datetime] = mapped_column(
|
|
@@ -257,13 +258,13 @@ def create_models(
|
|
|
257
258
|
id: Mapped[int] = mapped_column( # noqa A003
|
|
258
259
|
primary_key=True,
|
|
259
260
|
autoincrement=True,
|
|
260
|
-
name=
|
|
261
|
+
name='user_id' if prefix_with_name else None
|
|
261
262
|
)
|
|
262
263
|
|
|
263
|
-
_tokens: Mapped[list[
|
|
264
|
+
_tokens: Mapped[list[Token]] = relationship(
|
|
264
265
|
back_populates='user'
|
|
265
266
|
)
|
|
266
|
-
_role_bindings: Mapped[list[
|
|
267
|
+
_role_bindings: Mapped[list[RoleBinding]] = relationship(
|
|
267
268
|
back_populates='user'
|
|
268
269
|
)
|
|
269
270
|
|
|
@@ -340,7 +341,7 @@ def create_models(
|
|
|
340
341
|
|
|
341
342
|
class _TokenPKMixin:
|
|
342
343
|
"""
|
|
343
|
-
Has an integer
|
|
344
|
+
Has an integer as a primary key.
|
|
344
345
|
|
|
345
346
|
This is all that is needed in most apps,
|
|
346
347
|
and is the default.
|
|
@@ -403,11 +404,19 @@ def create_models(
|
|
|
403
404
|
token: str
|
|
404
405
|
) -> Optional[Token]:
|
|
405
406
|
|
|
406
|
-
return
|
|
407
|
-
cls
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
407
|
+
return (
|
|
408
|
+
sess.query(cls)
|
|
409
|
+
.filter_by(token=token)
|
|
410
|
+
.options(
|
|
411
|
+
# Join through to the Role table when fetching a Token so
|
|
412
|
+
# that SQLAlchemy only fires one SELECT query instead of
|
|
413
|
+
# four to get `token.user._role_bindings.role.name`
|
|
414
|
+
joinedload(cls.user)
|
|
415
|
+
.joinedload(User._role_bindings)
|
|
416
|
+
.joinedload(RoleBinding.role)
|
|
417
|
+
)
|
|
418
|
+
.one_or_none()
|
|
419
|
+
)
|
|
411
420
|
|
|
412
421
|
@classmethod
|
|
413
422
|
def get_or_create(
|
|
@@ -475,14 +484,14 @@ def create_models(
|
|
|
475
484
|
id: Mapped[int] = mapped_column( # noqa A003
|
|
476
485
|
primary_key=True,
|
|
477
486
|
autoincrement=True,
|
|
478
|
-
name=
|
|
487
|
+
name='role_id' if prefix_with_name else None
|
|
479
488
|
)
|
|
480
489
|
|
|
481
490
|
name: Mapped[str] = mapped_column(
|
|
482
491
|
unique=True
|
|
483
492
|
)
|
|
484
493
|
|
|
485
|
-
_role_bindings: Mapped[list[
|
|
494
|
+
_role_bindings: Mapped[list[RoleBinding]] = relationship(
|
|
486
495
|
back_populates='role'
|
|
487
496
|
)
|
|
488
497
|
|
tol/validators/__init__.py
CHANGED
|
@@ -6,6 +6,8 @@ from .allowed_keys import AllowedKeysValidator # noqa
|
|
|
6
6
|
from .allowed_values import AllowedValuesValidator # noqa
|
|
7
7
|
from .allowed_values_from_datasource import AllowedValuesFromDataSourceValidator # noqa
|
|
8
8
|
from .assert_on_condition import AssertOnConditionValidator # noqa
|
|
9
|
+
from .converter_and_validate import ConverterAndValidateValidator # noqa
|
|
10
|
+
from .ena_checklist import EnaChecklistValidator # noqa
|
|
9
11
|
from .mutually_exclusive import MutuallyExclusiveValidator # noqa
|
|
10
12
|
from .ena_submittable import EnaSubmittableValidator # noqa
|
|
11
13
|
from .regex import Regex, RegexValidator # noqa
|
|
@@ -15,6 +17,5 @@ from .sts_fields import StsFieldsValidator # noqa
|
|
|
15
17
|
from .tolid import TolidValidator # noqa
|
|
16
18
|
from .unique_values import UniqueValuesValidator # noqa
|
|
17
19
|
from .unique_whole_organisms import UniqueWholeOrganismsValidator # noqa
|
|
18
|
-
# Conditions are used where validators are defined, not just within validators
|
|
19
20
|
from .interfaces import Condition # noqa
|
|
20
21
|
from .min_one_valid_value import MinOneValidValueValidator # noqa
|
tol/validators/allowed_keys.py
CHANGED
tol/validators/allowed_values.py
CHANGED
|
@@ -17,7 +17,6 @@ class AllowedValuesFromDataSourceValidator(Validator):
|
|
|
17
17
|
"""
|
|
18
18
|
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
19
19
|
class Config:
|
|
20
|
-
allowed_values: List[str | int | float] | None = None
|
|
21
20
|
datasource_instance_id: int
|
|
22
21
|
datasource_object_type: str
|
|
23
22
|
datasource_field_name: str
|
|
@@ -30,22 +29,19 @@ class AllowedValuesFromDataSourceValidator(Validator):
|
|
|
30
29
|
def __init__(
|
|
31
30
|
self,
|
|
32
31
|
config: Config,
|
|
32
|
+
allowed_values: List[str | int | float] | None = None, # For testing
|
|
33
|
+
**kwargs
|
|
33
34
|
) -> None:
|
|
34
35
|
|
|
35
36
|
super().__init__()
|
|
36
37
|
|
|
37
38
|
self.__config = config
|
|
38
|
-
self.__cached_list =
|
|
39
|
+
self.__cached_list = allowed_values \
|
|
39
40
|
or self.__initialize_list_from_datasource()
|
|
40
41
|
|
|
41
|
-
if self.__config.allowed_values is None:
|
|
42
|
-
self.__cached_list = self.__initialize_list_from_datasource()
|
|
43
|
-
else:
|
|
44
|
-
self.__cached_list = self.__config.allowed_values
|
|
45
|
-
|
|
46
42
|
def __initialize_list_from_datasource(self) -> List[str | int | float]:
|
|
47
43
|
dsi = portaldb().get_one('data_source_instance', self.__config.datasource_instance_id)
|
|
48
|
-
ds = DataSourceUtils.
|
|
44
|
+
ds = DataSourceUtils.get_datasource_by_datasource_instance(dsi)
|
|
49
45
|
return [
|
|
50
46
|
obj.get_field_by_name(
|
|
51
47
|
self.__config.datasource_field_name
|
|
@@ -59,8 +55,11 @@ class AllowedValuesFromDataSourceValidator(Validator):
|
|
|
59
55
|
obj: DataObject
|
|
60
56
|
) -> None:
|
|
61
57
|
field_value = obj.get_field_by_name(self.__config.field_name)
|
|
62
|
-
if
|
|
63
|
-
|
|
58
|
+
if not field_value:
|
|
59
|
+
return
|
|
60
|
+
if not isinstance(field_value, list):
|
|
61
|
+
field_value = [field_value]
|
|
62
|
+
if any(value not in self.__cached_list for value in field_value):
|
|
64
63
|
multiple_cached_values = len(self.__cached_list) > 1
|
|
65
64
|
|
|
66
65
|
cached_list_str = ''
|
|
@@ -7,7 +7,7 @@ from typing import List
|
|
|
7
7
|
|
|
8
8
|
from tol.core import DataObject, Validator
|
|
9
9
|
|
|
10
|
-
from .interfaces import Condition, ConditionEvaluator
|
|
10
|
+
from .interfaces import Condition, ConditionDict, ConditionEvaluator
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class AssertOnConditionValidator(Validator, ConditionEvaluator):
|
|
@@ -19,13 +19,13 @@ class AssertOnConditionValidator(Validator, ConditionEvaluator):
|
|
|
19
19
|
"""
|
|
20
20
|
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
21
21
|
class Config:
|
|
22
|
-
condition:
|
|
23
|
-
assertions: List[
|
|
22
|
+
condition: ConditionDict
|
|
23
|
+
assertions: List[ConditionDict]
|
|
24
24
|
|
|
25
25
|
__slots__ = ['__config']
|
|
26
26
|
__config: Config
|
|
27
27
|
|
|
28
|
-
def __init__(self, config: Config) -> None:
|
|
28
|
+
def __init__(self, config: Config, **kwargs) -> None:
|
|
29
29
|
super().__init__()
|
|
30
30
|
|
|
31
31
|
self.__config = config
|
|
@@ -33,10 +33,10 @@ class AssertOnConditionValidator(Validator, ConditionEvaluator):
|
|
|
33
33
|
def _validate_data_object(self, obj: DataObject) -> None:
|
|
34
34
|
# Check condition atribute
|
|
35
35
|
# (only perform the assertions if the condition passes)
|
|
36
|
-
if self._does_condition_pass(self.__config.condition, obj):
|
|
36
|
+
if self._does_condition_pass(Condition.from_dict(self.__config.condition), obj):
|
|
37
37
|
# Perform each assertion
|
|
38
38
|
for assertion in self.__config.assertions:
|
|
39
|
-
self.__perform_assertion(obj, assertion)
|
|
39
|
+
self.__perform_assertion(obj, Condition.from_dict(assertion))
|
|
40
40
|
|
|
41
41
|
def __perform_assertion(self, obj: DataObject, assertion: Condition) -> None:
|
|
42
42
|
# There's only an error or warning if the assertion condition fails
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Genome Research Ltd.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from tol.core import DataObject
|
|
9
|
+
from tol.core.factory import DataObjectFactory
|
|
10
|
+
from tol.core.validate import ValidationResult, Validator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConverterAndValidateValidator(Validator):
|
|
14
|
+
"""
|
|
15
|
+
Convert DataObjects, validate the converted ones, and return the original
|
|
16
|
+
input unchanged. Inner validator results are not merged here.
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
"converters": [{
|
|
20
|
+
"module": "<path.to.module>",
|
|
21
|
+
"class_name": "<path.to.ConverterClass>",
|
|
22
|
+
"config": { ... }
|
|
23
|
+
}],
|
|
24
|
+
"validators": [{
|
|
25
|
+
"module": "<path.to.module>",
|
|
26
|
+
"class_name": "<path.to.ValidatorClass>",
|
|
27
|
+
"config": { ... }
|
|
28
|
+
}]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
33
|
+
class Config:
|
|
34
|
+
converters: list[dict]
|
|
35
|
+
validators: list[dict]
|
|
36
|
+
|
|
37
|
+
__slots__ = [
|
|
38
|
+
'__converters',
|
|
39
|
+
'__validators'
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
config: Config,
|
|
45
|
+
data_object_factory: DataObjectFactory,
|
|
46
|
+
**kwargs
|
|
47
|
+
) -> None:
|
|
48
|
+
super().__init__()
|
|
49
|
+
self.__converters = []
|
|
50
|
+
self.__validators = []
|
|
51
|
+
|
|
52
|
+
for conv in config.converters:
|
|
53
|
+
__module = importlib.import_module(conv.get('module'))
|
|
54
|
+
converter_class = getattr(__module, conv.get('class_name'))
|
|
55
|
+
|
|
56
|
+
converter_conf = converter_class.Config(
|
|
57
|
+
**conv.get('config')
|
|
58
|
+
)
|
|
59
|
+
self.__converters.append(converter_class(
|
|
60
|
+
data_object_factory=data_object_factory,
|
|
61
|
+
config=converter_conf,
|
|
62
|
+
))
|
|
63
|
+
for val in config.validators:
|
|
64
|
+
__module = importlib.import_module(val.get('module'))
|
|
65
|
+
validator_class = getattr(__module, val.get('class_name'))
|
|
66
|
+
|
|
67
|
+
validator_conf = validator_class.Config(
|
|
68
|
+
**val.get('config')
|
|
69
|
+
)
|
|
70
|
+
self.__validators.append(validator_class(
|
|
71
|
+
data_object_factory=data_object_factory,
|
|
72
|
+
config=validator_conf,
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
def _validate_data_object(self, obj: DataObject) -> None:
|
|
76
|
+
converted_objs = [obj]
|
|
77
|
+
for converter in self.__converters:
|
|
78
|
+
converted_objs = converter.convert_iterable(converted_objs)
|
|
79
|
+
for obj in converted_objs:
|
|
80
|
+
for validator in self.__validators:
|
|
81
|
+
validator._validate_data_object(obj)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def results(self) -> list[ValidationResult]:
|
|
85
|
+
return [result for validator in self.__validators for result in validator.results]
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def warnings(self) -> list[ValidationResult]:
|
|
89
|
+
return [warning for validator in self.__validators for warning in validator.warnings]
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def errors(self) -> list[ValidationResult]:
|
|
93
|
+
return [error for validator in self.__validators for error in validator.errors]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Genome Research Ltd.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from tol.core import DataSource
|
|
9
|
+
from tol.core.data_object import DataObject
|
|
10
|
+
from tol.core.validate import Validator
|
|
11
|
+
from tol.sources.ena import ena
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EnaChecklistValidator(Validator):
|
|
15
|
+
"""
|
|
16
|
+
validates the ENA_CHECKLIST for each samples
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
20
|
+
class Config:
|
|
21
|
+
ena_checklist_id: str
|
|
22
|
+
|
|
23
|
+
__slots__ = ['__config']
|
|
24
|
+
__config: Config
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: Config, datasource: DataSource = ena(), **kwargs) -> None:
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.__config = config
|
|
29
|
+
self._datasource = datasource
|
|
30
|
+
|
|
31
|
+
def _validate_data_object(self, obj: DataObject) -> None:
|
|
32
|
+
ena_datasource = self._datasource
|
|
33
|
+
ena_checklist = ena_datasource.get_one('checklist', self.__config.ena_checklist_id)
|
|
34
|
+
|
|
35
|
+
validations = ena_checklist.attributes['checklist']
|
|
36
|
+
for key in validations:
|
|
37
|
+
field_name = key
|
|
38
|
+
if 'field' in validations[key]:
|
|
39
|
+
field_name = validations[key]['field']
|
|
40
|
+
if 'mandatory' in validations[key] and key not in obj.attributes:
|
|
41
|
+
self.add_error(object_id=obj.id, detail='Must be given', field=[field_name])
|
|
42
|
+
continue
|
|
43
|
+
if 'mandatory' in validations[key] and obj.attributes[key] is None:
|
|
44
|
+
self.add_error(object_id=obj.id, detail='Must be given', field=[field_name])
|
|
45
|
+
continue
|
|
46
|
+
if 'mandatory' in validations[key] and obj.attributes.get(key) == '':
|
|
47
|
+
self.add_error(
|
|
48
|
+
object_id=obj.id,
|
|
49
|
+
detail='Must not be empty', field=[field_name]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if 'restricted text' in validations[key] and key in obj.attributes:
|
|
53
|
+
for condition in validations[key]:
|
|
54
|
+
if type(condition) == str and '(' in condition:
|
|
55
|
+
regex = condition
|
|
56
|
+
compiled_re = re.compile(regex)
|
|
57
|
+
if not compiled_re.search(obj.attributes.get(key)):
|
|
58
|
+
self.add_error(
|
|
59
|
+
object_id=obj.id,
|
|
60
|
+
detail='Must match specific pattern', field=[field_name]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Check against allowed values
|
|
64
|
+
if 'text choice' in validations[key] and key in obj.attributes:
|
|
65
|
+
for condition in validations[key]:
|
|
66
|
+
if type(condition) == list:
|
|
67
|
+
allowed_values = condition
|
|
68
|
+
if obj.attributes.get(key).lower() not in \
|
|
69
|
+
[x.lower() for x in allowed_values]:
|
|
70
|
+
self.add_error(
|
|
71
|
+
object_id=obj.id,
|
|
72
|
+
detail='Must be in allowed values', field=[field_name]
|
|
73
|
+
)
|
|
@@ -4,11 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
from abc import ABC
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from typing import Any, Tuple
|
|
7
|
+
from typing import Any, Dict, Tuple, cast
|
|
8
8
|
|
|
9
9
|
from tol.core import DataObject
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
ConditionDict = Dict[str, str | Any | bool]
|
|
13
|
+
"""
|
|
14
|
+
The dict representation of a Condition. Conditions can be constructed
|
|
15
|
+
from such a dict through Condition.from_dict(condition_dict)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
12
19
|
@dataclass(slots=True)
|
|
13
20
|
class Condition:
|
|
14
21
|
field: str
|
|
@@ -21,6 +28,40 @@ class Condition:
|
|
|
21
28
|
def __repr__(self) -> str:
|
|
22
29
|
return f'{self.field} {self.operator} {self.value}'
|
|
23
30
|
|
|
31
|
+
@staticmethod
|
|
32
|
+
def from_dict(condition_dict: ConditionDict) -> 'Condition':
|
|
33
|
+
"""
|
|
34
|
+
A means of instantiating a Condition from a dictionary.
|
|
35
|
+
This is a separate method (rather than constructing with kwargs
|
|
36
|
+
like `Condition(**condition_dict))`) to allow for both precense
|
|
37
|
+
and type checking for each field.
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
# Extract fields
|
|
41
|
+
field = condition_dict['field']
|
|
42
|
+
operator = condition_dict['operator']
|
|
43
|
+
value = condition_dict['value']
|
|
44
|
+
is_error = condition_dict.get('is_error', True)
|
|
45
|
+
|
|
46
|
+
# Ensure fields are the correct type
|
|
47
|
+
if not isinstance(field, str) and not isinstance(operator, str):
|
|
48
|
+
raise Exception(
|
|
49
|
+
f'Dictionary {condition_dict} not in valid format '
|
|
50
|
+
f'to convert to Condition (type of condition dict incorrect)'
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return Condition(
|
|
54
|
+
cast(str, field),
|
|
55
|
+
cast(str, operator),
|
|
56
|
+
value,
|
|
57
|
+
cast(bool, is_error),
|
|
58
|
+
)
|
|
59
|
+
except IndexError as e:
|
|
60
|
+
raise Exception(
|
|
61
|
+
f'Dictionary {condition_dict} not in valid format '
|
|
62
|
+
f'to convert to Condition (key "{e.args[0]}" not found)'
|
|
63
|
+
)
|
|
64
|
+
|
|
24
65
|
|
|
25
66
|
class ConditionEvaluator(ABC):
|
|
26
67
|
"""
|
|
@@ -7,7 +7,7 @@ from typing import Any, List
|
|
|
7
7
|
|
|
8
8
|
from tol.core import DataObject, Validator
|
|
9
9
|
|
|
10
|
-
from .interfaces import Condition, ConditionEvaluator
|
|
10
|
+
from .interfaces import Condition, ConditionDict, ConditionEvaluator
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class MutuallyExclusiveValidator(Validator, ConditionEvaluator):
|
|
@@ -19,12 +19,16 @@ class MutuallyExclusiveValidator(Validator, ConditionEvaluator):
|
|
|
19
19
|
"""
|
|
20
20
|
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
21
21
|
class Config:
|
|
22
|
-
first_field_where:
|
|
23
|
-
second_field_where:
|
|
22
|
+
first_field_where: ConditionDict
|
|
23
|
+
second_field_where: ConditionDict
|
|
24
24
|
target_fields: List[str]
|
|
25
25
|
detail: str | None = None
|
|
26
26
|
|
|
27
27
|
def _get_error_message(self) -> str:
|
|
28
|
+
# Extract conditions
|
|
29
|
+
first_condition = Condition.from_dict(self.first_field_where)
|
|
30
|
+
second_condition = Condition.from_dict(self.second_field_where)
|
|
31
|
+
|
|
28
32
|
# Use a pre-defined, hard-coded detail message if one was not provided
|
|
29
33
|
if self.detail is None:
|
|
30
34
|
multiple_target_fields = len(self.target_fields) > 1
|
|
@@ -47,8 +51,8 @@ class MutuallyExclusiveValidator(Validator, ConditionEvaluator):
|
|
|
47
51
|
|
|
48
52
|
return (
|
|
49
53
|
f'The field{possible_plural} {target_fields_str} cannot have the same '
|
|
50
|
-
f'value{possible_plural} both when {
|
|
51
|
-
f'{
|
|
54
|
+
f'value{possible_plural} both when {first_condition} and when '
|
|
55
|
+
f'{second_condition}'
|
|
52
56
|
)
|
|
53
57
|
else:
|
|
54
58
|
return self.detail
|
|
@@ -58,7 +62,7 @@ class MutuallyExclusiveValidator(Validator, ConditionEvaluator):
|
|
|
58
62
|
__first_list: List[Any]
|
|
59
63
|
__second_list: List[Any]
|
|
60
64
|
|
|
61
|
-
def __init__(self, config: Config) -> None:
|
|
65
|
+
def __init__(self, config: Config, **kwargs) -> None:
|
|
62
66
|
super().__init__()
|
|
63
67
|
|
|
64
68
|
self.__config = config
|
|
@@ -67,7 +71,7 @@ class MutuallyExclusiveValidator(Validator, ConditionEvaluator):
|
|
|
67
71
|
|
|
68
72
|
def _validate_data_object(self, obj: DataObject) -> None:
|
|
69
73
|
# Check first field
|
|
70
|
-
if self._does_condition_pass(self.__config.first_field_where, obj):
|
|
74
|
+
if self._does_condition_pass(Condition.from_dict(self.__config.first_field_where), obj):
|
|
71
75
|
# Check whether the values of the target fields were found in the second list
|
|
72
76
|
if [
|
|
73
77
|
obj.get_field_by_name(target_field)
|
|
@@ -86,7 +90,7 @@ class MutuallyExclusiveValidator(Validator, ConditionEvaluator):
|
|
|
86
90
|
]
|
|
87
91
|
)
|
|
88
92
|
# Check second field (same as the first condition, but for the second!)
|
|
89
|
-
elif self._does_condition_pass(self.__config.second_field_where, obj):
|
|
93
|
+
elif self._does_condition_pass(Condition.from_dict(self.__config.second_field_where), obj):
|
|
90
94
|
# Check whether the values of the target fields were found in the first list
|
|
91
95
|
if [
|
|
92
96
|
obj.get_field_by_name(target_field)
|
tol/validators/regex.py
CHANGED
|
@@ -10,6 +10,12 @@ from tol.core import DataObject
|
|
|
10
10
|
from tol.core.validate import Validator
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
RegexDict = dict[
|
|
14
|
+
str,
|
|
15
|
+
str | bool | list[Any],
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
13
19
|
@dataclass(frozen=True, kw_only=True)
|
|
14
20
|
class Regex:
|
|
15
21
|
key: str
|
|
@@ -20,7 +26,10 @@ class Regex:
|
|
|
20
26
|
|
|
21
27
|
def is_allowed(self, __v: Any) -> bool:
|
|
22
28
|
# Check regex
|
|
23
|
-
return re.search(
|
|
29
|
+
return bool(re.search(
|
|
30
|
+
self.regex,
|
|
31
|
+
str(__v) if __v is not None else ''
|
|
32
|
+
))
|
|
24
33
|
|
|
25
34
|
|
|
26
35
|
class RegexValidator(Validator):
|
|
@@ -31,19 +40,20 @@ class RegexValidator(Validator):
|
|
|
31
40
|
"""
|
|
32
41
|
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
33
42
|
class Config:
|
|
34
|
-
regexes: List[Regex]
|
|
43
|
+
regexes: List[Regex | RegexDict]
|
|
35
44
|
|
|
36
45
|
__slots__ = ['__config']
|
|
37
46
|
__config: Config
|
|
38
47
|
|
|
39
48
|
def __init__(
|
|
40
49
|
self,
|
|
41
|
-
config: Config
|
|
50
|
+
config: Config,
|
|
51
|
+
**kwargs
|
|
42
52
|
) -> None:
|
|
43
53
|
|
|
44
54
|
super().__init__()
|
|
45
55
|
|
|
46
|
-
self.__config = config
|
|
56
|
+
self.__config = self.__get_config(config)
|
|
47
57
|
|
|
48
58
|
def _validate_data_object(
|
|
49
59
|
self,
|
|
@@ -93,3 +103,18 @@ class RegexValidator(Validator):
|
|
|
93
103
|
detail=c.detail,
|
|
94
104
|
field=c.key,
|
|
95
105
|
)
|
|
106
|
+
|
|
107
|
+
def __get_config(
|
|
108
|
+
self,
|
|
109
|
+
config: Config,
|
|
110
|
+
) -> Config:
|
|
111
|
+
|
|
112
|
+
# Ensure config is in Regex format
|
|
113
|
+
# (as you can either pass in a list of Regex or a RegexDict,
|
|
114
|
+
# which can be used to initialize a Regex)
|
|
115
|
+
return self.Config(
|
|
116
|
+
regexes=[
|
|
117
|
+
c if isinstance(c, Regex) else Regex(**c)
|
|
118
|
+
for c in config.regexes
|
|
119
|
+
]
|
|
120
|
+
)
|
tol/validators/regex_by_value.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import Dict, List
|
|
|
8
8
|
from tol.core import DataObject
|
|
9
9
|
from tol.core.validate import Validator
|
|
10
10
|
|
|
11
|
-
from .regex import Regex
|
|
11
|
+
from .regex import Regex, RegexDict
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class RegexByValueValidator(Validator):
|
|
@@ -20,19 +20,20 @@ class RegexByValueValidator(Validator):
|
|
|
20
20
|
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
21
21
|
class Config:
|
|
22
22
|
key_column: str
|
|
23
|
-
regexes: Dict[str, List[Regex]]
|
|
23
|
+
regexes: Dict[str, List[Regex | RegexDict]]
|
|
24
24
|
|
|
25
25
|
__slots__ = ['__config']
|
|
26
26
|
config: Config
|
|
27
27
|
|
|
28
28
|
def __init__(
|
|
29
29
|
self,
|
|
30
|
-
config: Config
|
|
30
|
+
config: Config,
|
|
31
|
+
**kwargs
|
|
31
32
|
) -> None:
|
|
32
33
|
|
|
33
34
|
super().__init__()
|
|
34
35
|
|
|
35
|
-
self.__config = config
|
|
36
|
+
self.__config = self.__get_config(config)
|
|
36
37
|
|
|
37
38
|
def _validate_data_object(
|
|
38
39
|
self,
|
|
@@ -78,3 +79,22 @@ class RegexByValueValidator(Validator):
|
|
|
78
79
|
detail=c.detail,
|
|
79
80
|
field=c.key,
|
|
80
81
|
)
|
|
82
|
+
|
|
83
|
+
def __get_config(
|
|
84
|
+
self,
|
|
85
|
+
config: Config,
|
|
86
|
+
) -> Config:
|
|
87
|
+
|
|
88
|
+
# Ensure config is in Regex format
|
|
89
|
+
# (as you can either pass in a list of Regex or a RegexDict,
|
|
90
|
+
# which can be used to initialize a Regex)
|
|
91
|
+
return self.Config(
|
|
92
|
+
key_column=config.key_column,
|
|
93
|
+
regexes={
|
|
94
|
+
k: [
|
|
95
|
+
c if isinstance(c, Regex) else Regex(**c)
|
|
96
|
+
for c in v
|
|
97
|
+
]
|
|
98
|
+
for k, v in config.regexes.items()
|
|
99
|
+
}
|
|
100
|
+
)
|
|
@@ -25,7 +25,7 @@ class SpecimensHaveSameTaxonValidator(Validator):
|
|
|
25
25
|
__config: Config
|
|
26
26
|
__seen: Dict[str, str]
|
|
27
27
|
|
|
28
|
-
def __init__(self, config: Config) -> None:
|
|
28
|
+
def __init__(self, config: Config, **kwargs) -> None:
|
|
29
29
|
super().__init__()
|
|
30
30
|
self.__seen = {}
|
|
31
31
|
self.__config = config
|
tol/validators/sts_fields.py
CHANGED
tol/validators/tolid.py
CHANGED
|
@@ -34,6 +34,7 @@ class TolidValidator(Validator):
|
|
|
34
34
|
self,
|
|
35
35
|
config: Config,
|
|
36
36
|
datasource=tolid(),
|
|
37
|
+
**kwargs
|
|
37
38
|
) -> None:
|
|
38
39
|
|
|
39
40
|
super().__init__()
|
|
@@ -90,7 +91,7 @@ class TolidValidator(Validator):
|
|
|
90
91
|
f.and_ = {'specimen_id': {'eq': {'value': specimen_id}}}
|
|
91
92
|
self.__cached_tolids[specimen_id] = list(self.__datasource.get_list(
|
|
92
93
|
object_type='specimen',
|
|
93
|
-
|
|
94
|
+
object_filters=f
|
|
94
95
|
))
|
|
95
96
|
|
|
96
97
|
if (len(self.__cached_tolids[specimen_id]) == 0):
|
tol/validators/unique_values.py
CHANGED
|
@@ -28,7 +28,7 @@ class UniqueWholeOrganismsValidator(Validator):
|
|
|
28
28
|
__whole_organisms: List[str]
|
|
29
29
|
__part_organisms: List[str]
|
|
30
30
|
|
|
31
|
-
def __init__(self, config: Config) -> None:
|
|
31
|
+
def __init__(self, config: Config, **kwargs) -> None:
|
|
32
32
|
super().__init__()
|
|
33
33
|
self.__whole_organisms = []
|
|
34
34
|
self.__part_organisms = []
|
|
@@ -110,7 +110,7 @@ tol/core/operator/counter.py,sha256=aD7CRcSfHUfH-ASivJhUSncbLc8gvwdbQUDONjHp4oo,
|
|
|
110
110
|
tol/core/operator/cursor.py,sha256=G8RkuLpQS6vDAXhE2D1WXLSEuGDkqv3YwSrW_QhGTlc,3960
|
|
111
111
|
tol/core/operator/declare.py,sha256=W1jngGKNy-5D8T8nt_-6lpfOFTa-ilVr9YalRdJNG3w,333
|
|
112
112
|
tol/core/operator/deleter.py,sha256=v8C6aBSQGwVCYmZ_PIeGVQExSyilQi5C-6G-wuACCBg,755
|
|
113
|
-
tol/core/operator/detail_getter.py,sha256=
|
|
113
|
+
tol/core/operator/detail_getter.py,sha256=EG7EuVh73ZzHjXh4fmIy0WtTJii5bHgqzoqbT8OuHqk,2795
|
|
114
114
|
tol/core/operator/enricher.py,sha256=ijJvE8aYCIVsOpmU2EHiTB8NPGwnGucebcDccm5o7qg,4079
|
|
115
115
|
tol/core/operator/enum.py,sha256=JsJkj7LIMLoCnx2n1rdSX9LxGK9bbfoloXVie4rR_3M,1651
|
|
116
116
|
tol/core/operator/group_statter.py,sha256=jZ45J3OGtypOfosWChWAwe05POpBejaoDNAO2qJP1GM,897
|
|
@@ -133,7 +133,7 @@ tol/eln/entities.py,sha256=3-Z4ibZR11zepz5Nw74PnkpxA_HA31FsRUx61FSPYUo,1209
|
|
|
133
133
|
tol/eln/generators.py,sha256=ghRUiohLakiAcMzGVPJrPxYmdWijs63YxfRTqmKu3Zc,1227
|
|
134
134
|
tol/eln/sanitise.py,sha256=fMj-VrQTnw4zn2X0wnjWQAI8gWAa8RYqNuv23LXQssI,406
|
|
135
135
|
tol/ena/__init__.py,sha256=T3TCqaHpgi2Uk2PjPGu60GaG2V8cTrHJlVLtZfLFhTQ,174
|
|
136
|
-
tol/ena/client.py,sha256=
|
|
136
|
+
tol/ena/client.py,sha256=ldmm7Z9_auQf1zVWjsFLXYgbKvGtSHTsr88YO3zfv2Y,6731
|
|
137
137
|
tol/ena/converter.py,sha256=nxbo4IFzzOvKNMq3Aeiw5iDqVWvY33nTngLppjHAoGY,1144
|
|
138
138
|
tol/ena/ena_datasource.py,sha256=jEvyUaH4pfFxmdtn6O_PwOdjPz6u80uAT3SLlR2f5nM,8968
|
|
139
139
|
tol/ena/ena_methods.py,sha256=jgpLssZq-F-vgkO-fYu4jrXenmNkdFpFKAY3VKp5HHE,9209
|
|
@@ -148,7 +148,7 @@ tol/flows/__init__.py,sha256=M7iSvnBJs6fJ8M38cW0bYQa9WW0TN8FHAMjIHPDNAJ4,166
|
|
|
148
148
|
tol/flows/logger.py,sha256=rWXbaknGcPEZRFvC1CiB1qkhFRZsQk435w7VyJ3cpyw,170
|
|
149
149
|
tol/flows/secrets.py,sha256=1mlbsxaahzYRfVAx3XdztHOmUCtDMSJDzHysdbaCtj0,352
|
|
150
150
|
tol/flows/sequencing_submissions.py,sha256=ukz_y5be-BCBN2y3JPQ2EK6b3jwOCh-187j-jnw3EUY,11027
|
|
151
|
-
tol/flows/converters/__init__.py,sha256=
|
|
151
|
+
tol/flows/converters/__init__.py,sha256=Ee1yMFWwsRoG71ZFCsqtNwF45AR8farmV9h3ERrK994,6154
|
|
152
152
|
tol/flows/converters/benchling_entity_to_benchling_worklist_item_converter_factory.py,sha256=PN27fcvN4JLBnLrtPPAot1cWjAwPQHVcIDoMfPDeKzU,1210
|
|
153
153
|
tol/flows/converters/benchling_extraction_to_elastic_extraction_converter.py,sha256=S8pbmIeKlcXrLPRJHYBUGP0-Q7jTOV2QQk2TeA2naWo,1966
|
|
154
154
|
tol/flows/converters/benchling_extraction_to_elastic_sequencing_request_converter.py,sha256=2RiyRvGRSWzpUwEI4p-s0afshJpFUUxPqv2z-nyDSVg,1992
|
|
@@ -180,6 +180,8 @@ tol/flows/converters/gap_assembly_to_elastic_assembly_converter.py,sha256=XK-es-
|
|
|
180
180
|
tol/flows/converters/genome_notes_genome_note_to_elastic_genome_note_converter.py,sha256=AaUWbVTaWU-NXnUQPaPwI41TE7a-nC4zlg-jrWpPT2s,1166
|
|
181
181
|
tol/flows/converters/goat_taxon_to_elastic_species_converter.py,sha256=1NGs9427OdXGsBaMB467nOF7aTlJsUKYCuoSoABw9L4,1074
|
|
182
182
|
tol/flows/converters/grit_issue_to_elastic_curation_converter.py,sha256=XpRpoRn589MxTqEk6zPWGn6tamJiqY9Ctxk8v0q-dvA,3953
|
|
183
|
+
tol/flows/converters/incoming_sample_to_ena_sample_converter.py,sha256=HmGsg-VCE4W9Dl3lAlcNhWfkVYp1d22DZlFoTaFzeqA,5560
|
|
184
|
+
tol/flows/converters/incoming_sample_to_incoming_sample_with_lists_converter.py,sha256=5Fp1_ojsYqvRcKTgXJbyWqetPisi_vtWFcWr6RtGZoA,1504
|
|
183
185
|
tol/flows/converters/informatics_tolid_to_elastic_tolid_converter.py,sha256=VrvtsDTPlc5Xa3K4rcAMHwV4n71zOH7q5EfALLLQ1tI,587
|
|
184
186
|
tol/flows/converters/labwhere_location_to_elastic_sample_update_converter.py,sha256=NJNmG9sCc2WXc-2J5XfCKXhb2sDH82nZUBekd16PHcw,656
|
|
185
187
|
tol/flows/converters/labwhere_location_to_sts_tray_converter.py,sha256=dSBP5HfdvyIGvNQD6bhi0RKvkwhN8jq69g-0hOXUqEg,672
|
|
@@ -302,7 +304,7 @@ tol/sql/action/__init__.py,sha256=T1zAsCza_lvsNtXF1ecSLt9OFGup8tGnIs68YylBmXI,14
|
|
|
302
304
|
tol/sql/action/factory.py,sha256=HkareJp_57ud0_Bdd9Kwz3_Rnq2l211sGJgftohFAHg,3589
|
|
303
305
|
tol/sql/auth/__init__.py,sha256=e3JuwugXmXobklqZ1Mt1w03qPgb1WdUaJVM7oblzHyk,202
|
|
304
306
|
tol/sql/auth/blueprint.py,sha256=u0vT_TC9IMKrg08QFa9W29_83mT0y0jzLj3DvXy1BBw,25906
|
|
305
|
-
tol/sql/auth/models.py,sha256=
|
|
307
|
+
tol/sql/auth/models.py,sha256=U4CsKMMyzGMg6hj4tp_iRenr3_Q--64WJmHWvxQ2--Q,12297
|
|
306
308
|
tol/sql/pipeline_step/__init__.py,sha256=O7u4RrLfuoB0mwLcPxFoUrdTBZGB_4bE1vWCn5ho-qw,147
|
|
307
309
|
tol/sql/pipeline_step/factory.py,sha256=E5_Bs1379UGlRajKnb77iPmqhXBMemQR1C53SSPjXB4,5134
|
|
308
310
|
tol/sql/standard/__init__.py,sha256=2NbLXFk0rneGZosZ2ESIRcT0WMK0KncmPWaLPqvX-i4,142
|
|
@@ -318,26 +320,28 @@ tol/treeval/treeval_datasource.py,sha256=GzY6JwH67b5QdV-UVdCFJfgGAIuZ96J2nl53YxZ
|
|
|
318
320
|
tol/utils/__init__.py,sha256=764-Na1OaNGUDWpMIu51ZtXG7n_nB5MccUFK6LmkWRI,138
|
|
319
321
|
tol/utils/csv.py,sha256=mihww25fSn72c4h-RFeqD_pFIG6KHZP4v1_C0rx81ws,421
|
|
320
322
|
tol/utils/s3.py,sha256=aoYCwJ-qcMqFrpxmViFqPa0O1jgp0phtztO3-0CSNjw,491
|
|
321
|
-
tol/validators/__init__.py,sha256=
|
|
322
|
-
tol/validators/allowed_keys.py,sha256=
|
|
323
|
-
tol/validators/allowed_values.py,sha256
|
|
324
|
-
tol/validators/allowed_values_from_datasource.py,sha256=
|
|
325
|
-
tol/validators/assert_on_condition.py,sha256=
|
|
326
|
-
tol/validators/
|
|
327
|
-
tol/validators/
|
|
328
|
-
tol/validators/
|
|
329
|
-
tol/validators/
|
|
330
|
-
tol/validators/
|
|
331
|
-
tol/validators/
|
|
332
|
-
tol/validators/
|
|
333
|
-
tol/validators/
|
|
334
|
-
tol/validators/
|
|
335
|
-
tol/validators/
|
|
336
|
-
tol/validators/
|
|
337
|
-
tol/validators/
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
tol_sdk-1.7.
|
|
341
|
-
tol_sdk-1.7.
|
|
342
|
-
tol_sdk-1.7.
|
|
343
|
-
tol_sdk-1.7.
|
|
323
|
+
tol/validators/__init__.py,sha256=mJDlsI_W2y5jxazwOlyf-COl_Vlj1Xk1yC5xASouGH8,1134
|
|
324
|
+
tol/validators/allowed_keys.py,sha256=RJcHBiguL84B8hjSRaXLNES21yZqaKFwJNp2Tz9zvh0,1506
|
|
325
|
+
tol/validators/allowed_values.py,sha256=-Yy3Sqo1WYacGKlot_dn3M2o7Oj5MXOioJrJmrWCCxs,1536
|
|
326
|
+
tol/validators/allowed_values_from_datasource.py,sha256=ICFO6FcYXDN7M2Cv1OwpyN38CdhmY7oU-njzIatA3-w,3185
|
|
327
|
+
tol/validators/assert_on_condition.py,sha256=eBGgSVfIQ6e45SheM-ZDg7daXJjyZxRVS5L8AWvbXag,2027
|
|
328
|
+
tol/validators/converter_and_validate.py,sha256=YjhLsh0qMcyZEnHK2GJFotJfZssOtr8qU4uszcPQmrg,2960
|
|
329
|
+
tol/validators/ena_checklist.py,sha256=VGJeDrHH-XzueforuyyCEgEi6y9NurhvuOSL-gSDoOE,2885
|
|
330
|
+
tol/validators/ena_submittable.py,sha256=CujF9t4mA4N3Wm_5rA5MRp401aW19kbioOZpfWVXg6I,1965
|
|
331
|
+
tol/validators/min_one_valid_value.py,sha256=gZUHtfRA-Lvpw0d1FJoAA31cRJpLbbxAJCC9DCt5lCY,1442
|
|
332
|
+
tol/validators/mutually_exclusive.py,sha256=6blZK-2IY4Eq79fHKKrm-pxsQ6B5DNH5ldtxOFVCPhU,4492
|
|
333
|
+
tol/validators/regex.py,sha256=YdFHPcvEo6jNbXxDPTnpAQeOv3kSX4OUZUKfWmFFWl0,2602
|
|
334
|
+
tol/validators/regex_by_value.py,sha256=XM5EnT4vgD17rfpR3bUE9I56IemSw26BI9MZtMakd4E,2582
|
|
335
|
+
tol/validators/specimens_have_same_taxon.py,sha256=m2LLRIZMdhPj1fzyioDJOraI6UHXgy1l963xhezgk7E,2177
|
|
336
|
+
tol/validators/sts_fields.py,sha256=A_NkQFn2TMNFv2yU_ercs7CXlh-oib33ZmDZtc6SuKQ,3459
|
|
337
|
+
tol/validators/tolid.py,sha256=kgo-OWW3at6jK4DQtdgVFjO06sDvqG4QulGkI-jjVRU,3893
|
|
338
|
+
tol/validators/unique_values.py,sha256=o5IrfUNLEmlEp8kpInTtFnTq-FqiHSC9TItKdf-LI1o,3114
|
|
339
|
+
tol/validators/unique_whole_organisms.py,sha256=RdqA1GzIf3LTdrmNGGdxv0aW2udDY2P9EaqZb40hhik,5735
|
|
340
|
+
tol/validators/interfaces/__init__.py,sha256=jtOxnwnwqV_29xjmmMcS_kvlt-pQiWwQYJn2YRP07_w,172
|
|
341
|
+
tol/validators/interfaces/condition_evaluator.py,sha256=nj8Cb8hi47OBy6OVNfeLhF-Pjwtr8MiOSymYL6hfVes,3766
|
|
342
|
+
tol_sdk-1.7.5b3.dist-info/licenses/LICENSE,sha256=RF9Jacy-9BpUAQQ20INhTgtaNBkmdTolYCHtrrkM2-8,1077
|
|
343
|
+
tol_sdk-1.7.5b3.dist-info/METADATA,sha256=EJao_K6Vaa6nFKvIM8iw1vx73RocSQXiK4KkuFfQRVM,3081
|
|
344
|
+
tol_sdk-1.7.5b3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
345
|
+
tol_sdk-1.7.5b3.dist-info/entry_points.txt,sha256=jH3HfTwxjzog7E3lq8CKpUWGIRY9FSXbyL6CpUmv6D0,36
|
|
346
|
+
tol_sdk-1.7.5b3.dist-info/top_level.txt,sha256=PwKMQLphyZNvagBoriVbl8uwHXQl8IC1niawVG0iXMM,10
|
|
347
|
+
tol_sdk-1.7.5b3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|