labfreed 0.0.8__py2.py3-none-any.whl → 0.0.9__py2.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.
labfreed/TREX/parse.py ADDED
@@ -0,0 +1,128 @@
1
+ import logging
2
+ import re
3
+
4
+ from .data_model import *
5
+ from labfreed.validation import LabFREEDValidationError
6
+
7
+ class TREX_Parser():
8
+ def __init__(self, suppress_errors=False):
9
+ self._suppress_errors = suppress_errors
10
+
11
+ def parse_trex_str(self, trex_str, name=None) -> TREX:
12
+ trex = _from_trex_string(trex_str, name=name)
13
+
14
+ trex.print_validation_messages(trex_str)
15
+ if not trex.is_valid() and not self._suppress_errors:
16
+ raise LabFREEDValidationError(validation_msgs = trex.get_nested_validation_messages())
17
+
18
+ return trex
19
+
20
+
21
+ def _from_trex_string(trex_str, name=None, enforce_type=True) -> TREX:
22
+ if not trex_str:
23
+ raise ValueError(f'T-REX must be a string of non zero length')
24
+
25
+ # remove extension indicator. Precaution in case it is not done yet
26
+ if trex_str[0]=="*":
27
+ trex_str=trex_str[1:-1]
28
+ # remove line breaks. for editing T-REXes it's more convenient to have them in, so one never knows
29
+ trex_str = trex_str.replace('\n','')
30
+
31
+ d = re.match('((?P<name>.+)\$(?P<type>.+)/)?(?P<data>.+)', trex_str).groupdict()
32
+ if not d:
33
+ raise ValueError('TREX is invalid.')
34
+
35
+ type = d.get('type')
36
+ if not type:
37
+ logging.warning('No type given. Assume its trex')
38
+ elif type != 'TREX' and enforce_type:
39
+ logging.error(f'Extension type {type} is not TREX. Aborting')
40
+ raise ValueError(f'Extension type {type} is not TREX.')
41
+ else:
42
+ logging.warning('Extension type {type} is not TREX. Try anyways')
43
+
44
+ s_name = d.get('name')
45
+ if name and s_name:
46
+ logging.warning(f'conflicting names given. The string contained {s_name}, method parameter was {name}. Method parameter wins.')
47
+ elif not name and not s_name:
48
+ raise ValueError('No extension name was given')
49
+ elif s_name:
50
+ name = s_name
51
+
52
+ data = d.get('data')
53
+
54
+ segment_strings = data.split('+')
55
+ out_segments = list()
56
+ for s in segment_strings:
57
+ # there are only two valid options. The segment is a scalar or a table.
58
+ # Constructors do the parsing anyways and raise exceptions if invalid data
59
+ # try both options and then let it fail
60
+ segment = _deserialize_table_segment_from_trex_segment_str(s)
61
+ if not segment:
62
+ segment = _deserialize_value_segment_from_trex_segment_str(s)
63
+ if not segment:
64
+ raise ValueError('TREX contains neither valid value segment nor table')
65
+
66
+ out_segments.append(segment)
67
+ trex = TREX(name_= name, segments=out_segments)
68
+ trex._trex_str = trex_str
69
+
70
+ return trex
71
+
72
+
73
+
74
+ def _deserialize_value_segment_from_trex_segment_str(trex_segment_str) -> ValueSegment:
75
+ #re_scalar_pattern = re.compile(f"(?P<name>[\w\.-]*?)\$(?P<unit>[\w\.]*?):(?P<value>.*)")
76
+ re_scalar_pattern = re.compile(f"(?P<name>.+?)\$(?P<unit>.+?):(?P<value>.+)")
77
+ matches = re_scalar_pattern.match(trex_segment_str)
78
+ if not matches:
79
+ return None
80
+
81
+ name, type_, value = matches.groups()
82
+ out = ValueSegment.get_subclass(type=type_, value=value, key=name)
83
+ return out
84
+
85
+
86
+ def _deserialize_table_segment_from_trex_segment_str(trex_segment_str) -> TREX_Table:
87
+ # re_table_pattern = re.compile(f"(?P<tablename>[\w\.-]*?)\$\$(?P<header>[\w\.,\$:]*?)::(?P<body>.*)")
88
+ # re_col_head_pattern = re.compile(f"(?P<name>[\w\.-]*?)\$(?P<unit>[\w\.]*)")
89
+ re_table_pattern = re.compile(r"(?P<tablename>.+?)\$\$(?P<header>.+?)::(?P<body>.+)")
90
+
91
+ matches = re_table_pattern.match(trex_segment_str)
92
+ if not matches:
93
+ return None
94
+ name, header, body = matches.groups()
95
+
96
+ column_headers_str = header.split(':')
97
+
98
+ headers = []
99
+ for colum_header in column_headers_str:
100
+ ch = colum_header.split('$')
101
+ col_key = ch[0]
102
+ col_type = ch[1] if len(ch) > 1 else ''
103
+ headers.append(ColumnHeader(key=col_key, type=col_type))
104
+
105
+ data = [row.split(':') for row in body.split('::') ]
106
+ col_types = [h.type for h in headers]
107
+ # convert to correct value types
108
+ data_with_types = [[str_to_value_type(c,t) for c, t in zip(r, col_types)] for r in data]
109
+ data = [ TableRow(r) for r in data_with_types]
110
+
111
+ out = TREX_Table(column_headers=headers, data=data_with_types, key=name)
112
+ return out
113
+
114
+
115
+ def str_to_value_type(s:str, t:str):
116
+ match t:
117
+ case 'T.D': v = DateValue(value=s)
118
+ case 'T.B': v = BoolValue(value=s)
119
+ case 'T.A': v = AlphanumericValue(value=s)
120
+ case 'T.T': v = TextValue(value=s)
121
+ case 'T.X': v = BinaryValue(value=s)
122
+ case 'E' : v = ErrorValue(value=s)
123
+ case _ : v = NumericValue(value=s)
124
+ return v
125
+
126
+
127
+
128
+
@@ -0,0 +1,3 @@
1
+ from .data_model import TREX
2
+ def serialize_as_trex_str(trex:TREX):
3
+ return trex.data
@@ -0,0 +1,90 @@
1
+ from functools import cache
2
+ import json
3
+ from pathlib import Path
4
+
5
+
6
+
7
+ @cache
8
+ def unece_units() -> list[dict]:
9
+ p = Path(__file__).parent / 'UneceUnits.json'
10
+ with open(p) as f:
11
+ l = json.load(f)
12
+ return l
13
+
14
+ @cache
15
+ def unece_unit_codes():
16
+ codes= [u.get('commonCode') for u in unece_units() if u.get('state') == 'ACTIVE']
17
+ return codes
18
+
19
+
20
+ # def quantity_from_UN_CEFACT(value:str, unit_UN_CEFACT) -> UnitQuantity:
21
+ # """
22
+ # Maps units from https://unece.org/trade/documents/revision-17-annexes-i-iii
23
+ # to an object of the quantities library https://python-quantities.readthedocs.io/en/latest/index.html
24
+ # """
25
+ # # cast to numeric type. try int first, which will fail if string has no decimals.
26
+ # # nothing to worry yet: try floast next. if that fails the input was not a str representation of a number
27
+ # try:
28
+ # value_out = int(value)
29
+ # except ValueError:
30
+ # try:
31
+ # value_out = float(value)
32
+ # except ValueError as e:
33
+ # raise Exception(f'Input {value} is not a str representation of a number') from e
34
+
35
+ # d = {um[0]: um[1] for um in unit_map}
36
+
37
+ # unit = d.get(unit_UN_CEFACT)
38
+ # if not unit:
39
+ # raise NotImplementedError(f"lookup for unit {unit} not implemented")
40
+ # out = UnitQuantity(data=value_out, unit_name=unit.name, unit_symbol=unit.symbol)
41
+
42
+ # return out
43
+
44
+
45
+
46
+ # def quantity_to_UN_CEFACT(value:UnitQuantity ) -> Tuple[int|float, str]:
47
+ # d = {um[1].symbol: um[0] for um in unit_map}
48
+
49
+ # unit_un_cefact = d.get(value.unit_symbol)
50
+ # if not unit_un_cefact:
51
+ # raise NotImplementedError(f"lookup for unit {value.unit_symbol} not implemented")
52
+ # return value.data, unit_un_cefact
53
+
54
+
55
+
56
+
57
+
58
+ def check_compatibility_unece_quantities():
59
+ unece = get_unece_units()
60
+ print(f'Number of units in file: {len(unece)}')
61
+
62
+ failed = list()
63
+ sucess = list()
64
+ for u in unece:
65
+ if u.get('state') == 'ACTIVE':
66
+ try:
67
+ if not u.get('symbol'):
68
+ assert False
69
+ u.get('name')
70
+ validate_unit(u.get('symbol'))
71
+ sucess.append(u)
72
+ except AssertionError as e:
73
+ failed.append(u)
74
+ else:
75
+ pass
76
+
77
+
78
+
79
+ print('[blue] FAILED [/blue]')
80
+ for u in failed:
81
+ print(f'{u.get('commonCode')}: {u.get('name')}')
82
+
83
+ print('[yellow] SUCCESSFUL [/yellow]')
84
+ for u in sucess:
85
+ print(u)
86
+
87
+ print(f'{len(failed)} / {len(unece)} failed to convert')
88
+
89
+
90
+
labfreed/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
  Python implementation of LabFREED building blocks
3
3
  '''
4
4
 
5
- __version__ = "0.0.8"
5
+ __version__ = "0.0.9"
@@ -1,12 +1,17 @@
1
+ from functools import cache
2
+ import json
3
+ from pathlib import Path
4
+
5
+ from rich import print
6
+
1
7
  from typing import Tuple
2
8
  from typing_extensions import Annotated
3
9
  from pydantic import BaseModel, AfterValidator
4
10
  import quantities as pq
5
- from quantities import Quantity, UnitQuantity, units, dimensionless
11
+ from quantities import units
6
12
  from .uncertainty import to_significant_digits_str
7
13
 
8
14
 
9
-
10
15
  def validate_unit(unit_name:str) -> str :
11
16
  """
12
17
  Pydantic validator function for the unit.
@@ -92,51 +97,12 @@ unit_map = [
92
97
 
93
98
  ]
94
99
 
95
-
96
- def quantity_from_UN_CEFACT(value:str, unit_UN_CEFACT) -> PydanticUncertainQuantity:
97
- """
98
- Maps units from https://unece.org/trade/documents/revision-17-annexes-i-iii
99
- to an object of the quantities library https://python-quantities.readthedocs.io/en/latest/index.html
100
- """
101
- # cast to numeric type. try int first, which will fail if string has no decimals.
102
- # nothing to worry yet: try floast next. if that fails the input was not a str representation of a number
103
- try:
104
- value_out = int(value)
105
- except ValueError:
106
- try:
107
- value_out = float(value)
108
- except ValueError as e:
109
- raise Exception(f'Input {value} is not a str representation of a number') from e
110
-
111
- d = {um[0]: um[1] for um in unit_map}
112
-
113
- unit = d.get(unit_UN_CEFACT)
114
- if not unit:
115
- raise NotImplementedError(f"lookup for unit {unit} not implemented")
116
- out = PydanticUncertainQuantity(data=value_out, unit_name=unit.name, unit_symbol=unit.symbol)
117
-
118
- return out
119
-
120
-
121
- def quantity_to_UN_CEFACT(value:PydanticUncertainQuantity ) -> Tuple[int|float, str]:
122
- d = {um[1].symbol: um[0] for um in unit_map}
123
-
124
- unit_un_cefact = d.get(value.unit_symbol)
125
- if not unit_un_cefact:
126
- raise NotImplementedError(f"lookup for unit {value.unit_symbol} not implemented")
127
- return value.data, unit_un_cefact
128
-
129
-
130
-
131
-
132
100
 
133
101
 
134
102
 
135
103
 
136
-
137
104
  if __name__ == "__main__":
138
- pass
139
-
105
+ pass
140
106
 
141
107
 
142
108
 
labfreed/validation.py CHANGED
@@ -1,71 +1,147 @@
1
1
  from pydantic import BaseModel, Field, PrivateAttr
2
2
  from typing import List, Set, Tuple
3
3
 
4
+ from rich import print
5
+ from rich.text import Text
6
+
4
7
 
5
8
  domain_name_pattern = r"(?!-)([A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,63}"
6
9
  hsegment_pattern = r"[A-Za-z0-9_\-\.~!$&'()+,:;=@]|%[0-9A-Fa-f]{2}"
7
10
 
8
11
 
9
- class ValidationWarning(BaseModel):
12
+ class ValidationMessage(BaseModel):
10
13
  source:str
11
14
  type: str
12
15
  problem_msg:str
13
16
  recommendation_msg: str = ""
14
17
  highlight:str = "" #this can be used to highlight problematic parts
15
18
  highlight_sub:list[str] = Field(default_factory=list())
19
+
20
+ @property
21
+ def emphazised_highlight(self):
22
+ fmt = lambda s: f'[emph]{s}[/emph]'
23
+
24
+ if not self.highlight_sub:
25
+ return fmt(self.highlight)
26
+
27
+ result = []
28
+ for c in self.highlight:
29
+ if c in self.highlight_sub:
30
+ result.append(fmt(c))
31
+ else:
32
+ result.append(c)
33
+
34
+ return ''.join(result)
35
+
36
+
37
+ class LabFREEDValidationError(ValueError):
38
+ def __init__(self, message=None, validation_msgs=None):
39
+ super().__init__(message)
40
+ self._validation_msgs = validation_msgs
41
+
42
+ @property
43
+ def validation_msgs(self):
44
+ return self._validation_msgs
16
45
 
17
46
 
18
47
 
19
48
 
20
- class BaseModelWithWarnings(BaseModel):
49
+ class BaseModelWithValidationMessages(BaseModel):
21
50
  """ Extension of Pydantic BaseModel, so that validator can issue warnings.
22
51
  The purpose of that is to allow only minimal validation but on top check for stricter recommendations"""
23
- _warnings: list[ValidationWarning] = PrivateAttr(default_factory=list)
52
+ _validation_messages: list[ValidationMessage] = PrivateAttr(default_factory=list)
24
53
 
25
- def add_warning(self, *, msg: str, type:str, recommendation:str="", source:str="", highlight_pattern="", highlight_sub=None):
54
+ def add_validation_message(self, *, msg: str, type:str, recommendation:str="", source:str="", highlight_pattern="", highlight_sub=None):
26
55
  if not highlight_sub:
27
56
  highlight_sub = []
28
- w = ValidationWarning(problem_msg=msg, recommendation_msg=recommendation, source=source, type=type, highlight=highlight_pattern, highlight_sub=highlight_sub)
29
- if not w in self._warnings:
30
- self._warnings.append(w)
57
+ w = ValidationMessage(problem_msg=msg, recommendation_msg=recommendation, source=source, type=type, highlight=highlight_pattern, highlight_sub=highlight_sub)
31
58
 
32
- def get_warnings(self) -> list[ValidationWarning]:
33
- return self._warnings
59
+ if not w in self._validation_messages:
60
+ self._validation_messages.append(w)
34
61
 
35
- def clear_warnings(self):
36
- self._warnings.clear()
62
+ def get_validation_messages(self) -> list[ValidationMessage]:
63
+ return self._validation_messages
37
64
 
65
+ def get_errors(self) -> list[ValidationMessage]:
66
+ return filter_errors(self._validation_messages)
38
67
 
39
- # Function to extract warnings from a model and its nested models
40
- def extract_warnings(model: BaseModelWithWarnings, parent_name: str = "", visited: Set[int] = None) -> List[ValidationWarning]:
41
- """
42
- Recursively extract warnings from a Pydantic model and its nested fields.
68
+ def get_warnings(self) -> list[ValidationMessage]:
69
+ return filter_warnings(self._validation_messages)
43
70
 
44
- :param model: The Pydantic model instance to inspect.
45
- :param parent_name: The name of the parent model to track the path.
46
- :return: List of tuples containing (model name, warning message).
47
- """
48
- if visited is None:
49
- visited = set()
50
-
51
- model_id = id(model)
52
- if model_id in visited:
53
- return []
54
- visited.add(model_id)
55
-
56
- warnings_list = [(parent_name or model.__class__.__name__, model_id, warning) for warning in model.get_warnings()]
71
+ def is_valid(self) -> bool:
72
+ return len(filter_errors(self.get_nested_validation_messages())) == 0
73
+
74
+ # Function to extract warnings from a model and its nested models
75
+ def get_nested_validation_messages(self, parent_name: str = "", visited: Set[int] = None) -> List[ValidationMessage]:
76
+ """
77
+ Recursively extract warnings from a Pydantic model and its nested fields.
78
+
79
+ :param model: The Pydantic model instance to inspect.
80
+ :param parent_name: The name of the parent model to track the path.
81
+ :return: List of tuples containing (model name, warning message).
82
+ """
83
+ if visited is None:
84
+ visited = set()
85
+
86
+ model_id = id(self)
87
+ if model_id in visited:
88
+ return []
89
+ visited.add(model_id)
90
+
91
+ warnings_list = [warning for warning in self.get_validation_messages()]
92
+ # warnings_list = [(parent_name or self.__class__.__name__, model_id, warning) for warning in self.get_validation_messages()]
93
+
57
94
 
58
- for field_name, field in model.__fields__.items():
59
- full_path = f"{parent_name}.{field_name}" if parent_name else field_name
60
- value = getattr(model, field_name)
95
+ for field_name, field in self.__fields__.items():
96
+ full_path = f"{parent_name}.{field_name}" if parent_name else field_name
97
+ value = getattr(self, field_name)
61
98
 
62
- if isinstance(value, BaseModelWithWarnings):
63
- warnings_list.extend(extract_warnings(value, full_path, visited))
64
- elif isinstance(value, list):
65
- for index, item in enumerate(value):
66
- if isinstance(item, BaseModelWithWarnings):
67
- list_path = f"{full_path}[{index}]"
68
- warnings_list.extend(extract_warnings(item, list_path, visited))
99
+ if isinstance(value, BaseModelWithValidationMessages):
100
+ warnings_list.extend(value.get_nested_validation_messages(full_path, visited))
101
+ elif isinstance(value, list):
102
+ for index, item in enumerate(value):
103
+ if isinstance(item, BaseModelWithValidationMessages):
104
+ list_path = f"{full_path}[{index}]"
105
+ warnings_list.extend(item.get_nested_validation_messages(list_path, visited))
106
+ return warnings_list
107
+
108
+
109
+ def get_nested_errors(self) -> list[ValidationMessage]:
110
+ return filter_errors(self.get_nested_validation_messages())
111
+
112
+ def get_nested_warnings(self) -> list[ValidationMessage]:
113
+ return filter_warnings(self.get_nested_validation_messages())
114
+
115
+
116
+ def print_validation_messages(self, str_to_highlight_in):
117
+ msgs = self.get_nested_validation_messages()
118
+ print('\n'.join(['\n',
119
+ '=======================================',
120
+ 'Validation Results',
121
+ '---------------------------------------'
122
+ ]
123
+ )
124
+ )
125
+
126
+ for m in msgs:
127
+ if m.type.casefold() == "error":
128
+ color = 'red'
129
+ else:
130
+ color = 'yellow'
131
+
132
+ text = Text.from_markup(f'\n [bold {color}]{m.type} [/bold {color}] in \t {m.source}' )
133
+ print(text)
134
+ formatted_highlight = m.emphazised_highlight.replace('emph', f'bold {color}')
135
+ fmtd = str_to_highlight_in.replace(m.highlight, formatted_highlight)
136
+ fmtd = Text.from_markup(fmtd)
137
+ print(fmtd)
138
+ print(Text.from_markup(f'{m.problem_msg}'))
139
+
140
+
141
+
142
+ def filter_errors(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
143
+ return [ m for m in val_msg if m.type.casefold() == "error" ]
69
144
 
70
- return warnings_list
145
+ def filter_warnings(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
146
+ return [ m for m in val_msg if m.type.casefold() != "error" ]
71
147
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labfreed
3
- Version: 0.0.8
3
+ Version: 0.0.9
4
4
  Summary: Python implementation of LabFREED building blocks
5
5
  Author-email: Reto Thürer <thuerer.r@buchi.com>
6
6
  License-Expression: MIT
@@ -0,0 +1,22 @@
1
+ labfreed/__init__.py,sha256=NqKE8cu-i6wAIxbyZat0L_dbFSjpJgHYzJYqGEGXBBk,87
2
+ labfreed/validation.py,sha256=3w69iAX_fH6QdceMj_sgR9pQhJsSy5mXhxLxz_YNHfU,5850
3
+ labfreed/DisplayNameExtension/DisplayNameExtension.py,sha256=MKc9YzI5KKEfnH8glXEteB29ZMfZxbmvFzJTLKbOX_g,1051
4
+ labfreed/PAC_CAT/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
5
+ labfreed/PAC_CAT/data_model.py,sha256=hob-WNs2-633LmxQ7Ot3RBpcvStYFzdj20QDQZOQyqY,4306
6
+ labfreed/PAC_ID/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ labfreed/PAC_ID/data_model.py,sha256=c2exBF2AXwXxudPS9Cd74xex0VB5Q-EeJHQi43LRmwQ,9300
8
+ labfreed/PAC_ID/parse.py,sha256=g0AXzJ9El8TZ2c05GOMN6yfa3FUAFDFVndx5mefe_ZM,5175
9
+ labfreed/PAC_ID/serialize.py,sha256=0BhF7aXGlLpr312lkBvl1O5fXDFZeLLPgSBddO9Y86Q,1963
10
+ labfreed/PAC_ID/well_known_segment_keys.py,sha256=zrzMvvS42urPpiwinI-IhHPgT3r86zEBl4TlEMOfzbU,338
11
+ labfreed/TREX/UneceUnits.json,sha256=kwfQSp_nTuWbADfBBgqTWrvPl6XtM5SedEVLbMJrM7M,898953
12
+ labfreed/TREX/data_model.py,sha256=727da6PPvl-5gwPbljTDNprU38de80Cs1Q1bxKJ6DWI,29804
13
+ labfreed/TREX/parse.py,sha256=rV7EDCaY9cmBJNqsrSZQLNzcivyOCzLsKXW2sAOitaA,4867
14
+ labfreed/TREX/serialize.py,sha256=5M0c8l4xTtiX4PIKVRI3Gt-jNFYNcKOeuO3C-m1HE5g,89
15
+ labfreed/TREX/unece_units.py,sha256=7PL4eR8SGklnuR5gC4ooAvgFFYg9dCF9HmwIU25OZYw,2682
16
+ labfreed/conversion_tools/uncertainty.py,sha256=l3WxrLnWTQYfX28gFisXwDcVPvT8bCAd4q6Xl02dRdE,1117
17
+ labfreed/conversion_tools/unit_utilities.py,sha256=5NXDt-XRkajcg2lLdg0vDBWbmfhUCqeY4hu_k6PkbCY,2445
18
+ labfreed/utilities/base36.py,sha256=2lwmEMWm8qrFJkcrP-nMPwS0eCm2THhCJ3Vk-TdGQg0,2455
19
+ labfreed-0.0.9.dist-info/licenses/LICENSE,sha256=gHFOv9FRKHxO8cInP3YXyPoJnuNeqrvcHjaE_wPSsQ8,1100
20
+ labfreed-0.0.9.dist-info/WHEEL,sha256=BXjIu84EnBiZ4HkNUBN93Hamt5EPQMQ6VkF7-VZ_Pu0,100
21
+ labfreed-0.0.9.dist-info/METADATA,sha256=whgM7VD1R3S7JlP96c9mVBYWoDGwJzjdxdmE2nZRMFU,206
22
+ labfreed-0.0.9.dist-info/RECORD,,