labfreed 0.2.8__py3-none-any.whl → 0.2.9__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.

Potentially problematic release.


This version of labfreed might be problematic. Click here for more details.

Files changed (44) hide show
  1. labfreed/__init__.py +11 -11
  2. labfreed/labfreed_infrastructure.py +258 -258
  3. labfreed/pac_cat/__init__.py +19 -19
  4. labfreed/pac_cat/category_base.py +51 -51
  5. labfreed/pac_cat/pac_cat.py +150 -150
  6. labfreed/pac_cat/predefined_categories.py +200 -200
  7. labfreed/pac_id/__init__.py +19 -19
  8. labfreed/pac_id/extension.py +48 -48
  9. labfreed/pac_id/id_segment.py +89 -89
  10. labfreed/pac_id/pac_id.py +140 -140
  11. labfreed/pac_id/url_parser.py +155 -155
  12. labfreed/pac_id/url_serializer.py +85 -84
  13. labfreed/pac_id_resolver/__init__.py +2 -2
  14. labfreed/pac_id_resolver/cit_common.py +81 -81
  15. labfreed/pac_id_resolver/cit_v1.py +244 -244
  16. labfreed/pac_id_resolver/cit_v2.py +313 -313
  17. labfreed/pac_id_resolver/resolver.py +97 -97
  18. labfreed/pac_id_resolver/services.py +82 -82
  19. labfreed/qr/__init__.py +1 -1
  20. labfreed/qr/generate_qr.py +422 -422
  21. labfreed/trex/__init__.py +16 -16
  22. labfreed/trex/python_convenience/__init__.py +3 -3
  23. labfreed/trex/python_convenience/data_table.py +87 -87
  24. labfreed/trex/python_convenience/pyTREX.py +248 -248
  25. labfreed/trex/python_convenience/quantity.py +66 -66
  26. labfreed/trex/table_segment.py +245 -245
  27. labfreed/trex/trex.py +69 -69
  28. labfreed/trex/trex_base_models.py +209 -209
  29. labfreed/trex/value_segments.py +99 -99
  30. labfreed/utilities/base36.py +82 -82
  31. labfreed/well_known_extensions/__init__.py +4 -4
  32. labfreed/well_known_extensions/default_extension_interpreters.py +6 -6
  33. labfreed/well_known_extensions/display_name_extension.py +40 -40
  34. labfreed/well_known_extensions/trex_extension.py +30 -30
  35. labfreed/well_known_keys/gs1/__init__.py +5 -5
  36. labfreed/well_known_keys/gs1/gs1.py +3 -3
  37. labfreed/well_known_keys/labfreed/well_known_keys.py +15 -15
  38. labfreed/well_known_keys/unece/__init__.py +3 -3
  39. labfreed/well_known_keys/unece/unece_units.py +67 -67
  40. {labfreed-0.2.8.dist-info → labfreed-0.2.9.dist-info}/METADATA +11 -8
  41. labfreed-0.2.9.dist-info/RECORD +45 -0
  42. {labfreed-0.2.8.dist-info → labfreed-0.2.9.dist-info}/licenses/LICENSE +21 -21
  43. labfreed-0.2.8.dist-info/RECORD +0 -45
  44. {labfreed-0.2.8.dist-info → labfreed-0.2.9.dist-info}/WHEEL +0 -0
@@ -1,249 +1,249 @@
1
-
2
- from datetime import date, datetime, time
3
- import logging
4
- import re
5
- from typing import Self
6
-
7
- from pydantic import RootModel
8
- from labfreed.well_known_keys.unece.unece_units import unece_unit
9
- from labfreed.trex.python_convenience.data_table import DataTable
10
- from labfreed.utilities.base36 import from_base36, base36, to_base36
11
-
12
- from labfreed.trex.python_convenience.quantity import Quantity, unece_unit_code_from_quantity
13
- from labfreed.trex.table_segment import ColumnHeader, TableSegment
14
- from labfreed.trex.trex import TREX
15
- from labfreed.trex.trex_base_models import AlphanumericValue, BinaryValue, BoolValue, DateValue, ErrorValue, NumericValue, TextValue
16
- from labfreed.trex.value_segments import BoolSegment, ErrorSegment, TextSegment, NumericSegment, AlphanumericSegment, DateSegment, ValueSegment
17
-
18
-
19
- class pyTREX(RootModel[dict[str, Quantity | datetime | time | date | bool | str | base36 | DataTable]]):
20
- ''' A wrapper around dict, which knows how to convert to and from TREX.
21
- It restricts the types allowed as values. Keys must be str.
22
- '''
23
- model_config = {'arbitrary_types_allowed':True} # needed to allow Quantity and DataTable w/o implementing the pydantic schema
24
- '''@private'''
25
-
26
-
27
- @classmethod
28
- def from_trex(cls, trex:TREX) -> Self:
29
- '''Creates a pyTREX from a TREX'''
30
- return {seg.key: _trex_segment_to_python_type(seg) for seg in trex.segments}
31
-
32
-
33
- def to_trex(self) -> TREX:
34
- '''Creates a TREX'''
35
- segments = list()
36
- for k, v in self.root.items():
37
- if v is None:
38
- value = _error_value_from_python_type(v)
39
- segments.append(ErrorSegment(key=k, value=value.value))
40
- elif isinstance(v, bool):
41
- value = _bool_value_from_python_type(v)
42
- segments.append(BoolSegment(key=k, value=value.value))
43
- elif isinstance(v, Quantity):
44
- unece_code = unece_unit_code_from_quantity(v)
45
- value = _numeric_value_from_python_type(v.value)
46
- segments.append(NumericSegment(key=k, value=value.value, type=unece_code))
47
- elif isinstance(v, (int, float)):
48
- value = _numeric_value_from_python_type(v)
49
- segments.append(NumericSegment(key=k, value=value.value, type='C63')) # unitless
50
- elif isinstance(v, (datetime, time, date)):
51
- value = _date_value_from_python_type(v)
52
- segments.append(DateSegment(key=k, value=value.value))
53
- elif isinstance(v, str):
54
- if re.fullmatch(r'[A-Z0-9\-\.]*', v):
55
- value = _alphanumeric_value_from_python_type(v)
56
- segments.append(AlphanumericSegment(key=k, value=value.value))
57
- else:
58
- v = to_base36(v)
59
- value = _text_value_from_python_type(v)
60
- segments.append(TextSegment(key=k, value=value.value))
61
- elif isinstance(v, base36):
62
- value = _text_value_from_python_type(v)
63
- segments.append(TextSegment(key=k, value=value.value))
64
-
65
- elif isinstance(v, DataTable):
66
- v:DataTable = v
67
- headers = list()
68
- for nm, rt in zip(v.col_names, v.row_template):
69
- if isinstance(rt, bool): # must come first otherwise int matches the bool
70
- t = 'T.B'
71
- elif isinstance(rt, Quantity):
72
- unece_code = unece_unit_code_from_quantity(rt)
73
- t = unece_code
74
- elif isinstance(rt, (datetime, time, date)):
75
- t = 'T.D'
76
- elif isinstance(rt, str):
77
- if re.fullmatch(r'[A-Z0-9\-\.]*', rt):
78
- t = 'T.A'
79
- else:
80
- t = 'T.T'
81
- elif isinstance(rt, base36):
82
- t = 'T.T'
83
-
84
- headers.append(ColumnHeader(key=nm, type=t))
85
- data = []
86
- for row in v.data:
87
- r = []
88
- for e in row:
89
- if e is None:
90
- r.append(_error_value_from_python_type(e))
91
- elif isinstance(e, bool): # must come first otherwise int matches the bool
92
- r.append(_bool_value_from_python_type(e))
93
- elif isinstance(e, Quantity):
94
- r.append(_numeric_value_from_python_type(e.value))
95
- elif isinstance(e, (int, float)):
96
- r.append(_numeric_value_from_python_type(e))
97
- elif isinstance(e, (datetime, time, date)):
98
- r.append(_date_value_from_python_type(e))
99
- elif isinstance(e, str):
100
- if re.fullmatch(r'[A-Z0-9\-\.]*', e):
101
- r.append(_alphanumeric_value_from_python_type(e))
102
- else:
103
- e = to_base36(e)
104
- r.append(_text_value_from_python_type(e))
105
- elif isinstance(e, base36):
106
- r.append(_text_value_from_python_type(e))
107
- data.append(r)
108
- segments.append(TableSegment(key=k, column_headers=headers, data=data))
109
- return TREX(segments=segments)
110
-
111
- # make the usual dict methods available, for convenience
112
- def __getitem__(self, key): return self.root[key]
113
- def __setitem__(self, key, value): self.root[key] = value
114
- def update(self, *args, **kwargs):
115
- return self.root.update(*args, **kwargs)
116
- def keys(self): return self.root.keys()
117
- def values(self): return self.root.values()
118
- def items(self): return self.root.items()
119
- def __contains__(self, key): return key in self.root
120
- def __iter__(self): return iter(self.root)
121
- def __len__(self): return len(self.root)
122
-
123
-
124
-
125
- # Helper functions to convert python types to TREX types
126
-
127
- def _numeric_value_from_python_type(v:int|float):
128
- return NumericValue(value = str(v))
129
-
130
-
131
- def _date_value_from_python_type(v:date|time|datetime):
132
- sd = ""
133
- st = ""
134
- if isinstance(v, date) or isinstance(v, datetime):
135
- sd = v.strftime('%Y%m%d')
136
- if isinstance(v, time) or isinstance(v, datetime):
137
- if v.microsecond:
138
- st = v.strftime("T%H%M%S.") + f"{v.microsecond // 1000:03d}"
139
- elif v.second:
140
- st = v.strftime("T%H%M%S")
141
- else:
142
- st = v.strftime("T%H%M")
143
-
144
- return DateValue(value = sd + st)
145
-
146
-
147
- def _bool_value_from_python_type(v:bool):
148
- return BoolValue(value = 'T' if v else 'F')
149
-
150
-
151
- def _alphanumeric_value_from_python_type(v:str):
152
- return AlphanumericValue(value = v)
153
-
154
-
155
- def _text_value_from_python_type(v:base36|str):
156
- if isinstance(v, str):
157
- logging.info('Got str for text value > converting to base36')
158
- out = to_base36(v).root
159
- else:
160
- out = v.root
161
- return TextValue(value = out)
162
-
163
-
164
- def _binary_value_from_python_type(v:base36|str):
165
- if isinstance(v, str):
166
- out = v
167
- else:
168
- out = v.root
169
- return BinaryValue(value = out)
170
-
171
-
172
- def _error_value_from_python_type(v:str):
173
- if v is None:
174
- v = '-'
175
- return ErrorValue(value = v)
176
-
177
-
178
-
179
- # Helper functions to convert from TREX types to python types
180
- def _trex_segment_to_python_type(v):
181
- '''Converts a TREX segment to a python value. Note the segment key must be handles outside.'''
182
- if isinstance(v, NumericSegment):
183
- num_val = _trex_value_to_python_type(v)
184
- u = unece_unit(v.type)
185
- unit = u.get('symbol')
186
- return Quantity(value=num_val, unit=unit)
187
-
188
- # value segments are derived from their respective value type
189
- elif isinstance(v, ValueSegment):
190
- return _trex_value_to_python_type(v)
191
-
192
- elif isinstance(v, TableSegment):
193
- table = DataTable(col_names=[ch.key for ch in v.column_headers])
194
- for row in v.data:
195
- r = []
196
- for e, h in zip(row, v.column_headers):
197
- if isinstance(e, NumericValue):
198
- u = unece_unit(h.type)
199
- unit = u.get('symbol')
200
- r.append(Quantity(value=e.value, unit=unit))
201
- else:
202
- r.append(_trex_value_to_python_type(e))
203
- table.append(r)
204
- return table
205
-
206
-
207
-
208
- def _trex_value_to_python_type(v):
209
- '''Converts a TREX value to the corresponding python type'''
210
- if isinstance(v, NumericValue):
211
- if '.' not in v.value and 'E' not in v.value:
212
- return int(v.value)
213
- else:
214
- return float(v.value)
215
-
216
- elif isinstance(v,DateValue):
217
- d = v._date_time_dict
218
- if d.get('year') and d.get('hour'): # input is only a time
219
- return datetime(**d)
220
- elif d.get('year'):
221
- return date(**d)
222
- else:
223
- return time(**d)
224
-
225
- elif isinstance(v, BoolValue):
226
- if v.value == 'T':
227
- return True
228
- elif v.value == 'F':
229
- return False
230
- else:
231
- Exception(f'{v} is not valid boolean. That really should not have been possible -- Contact the maintainers of the library')
232
-
233
- elif isinstance(v, AlphanumericValue):
234
- return v.value
235
-
236
- elif isinstance(v, TextValue):
237
- decoded = from_base36(v.value)
238
- return decoded
239
-
240
- elif isinstance(v, BinaryValue):
241
- decoded = bytes(from_base36(v.value), encoding='utf-8')
242
- return decoded
243
-
244
- elif isinstance(v, ErrorValue):
245
- return v.value
246
-
247
- else:
248
- raise (TypeError(f'Invalid type {type(v)} of segment'))
1
+
2
+ from datetime import date, datetime, time
3
+ import logging
4
+ import re
5
+ from typing import Self
6
+
7
+ from pydantic import RootModel
8
+ from labfreed.well_known_keys.unece.unece_units import unece_unit
9
+ from labfreed.trex.python_convenience.data_table import DataTable
10
+ from labfreed.utilities.base36 import from_base36, base36, to_base36
11
+
12
+ from labfreed.trex.python_convenience.quantity import Quantity, unece_unit_code_from_quantity
13
+ from labfreed.trex.table_segment import ColumnHeader, TableSegment
14
+ from labfreed.trex.trex import TREX
15
+ from labfreed.trex.trex_base_models import AlphanumericValue, BinaryValue, BoolValue, DateValue, ErrorValue, NumericValue, TextValue
16
+ from labfreed.trex.value_segments import BoolSegment, ErrorSegment, TextSegment, NumericSegment, AlphanumericSegment, DateSegment, ValueSegment
17
+
18
+
19
+ class pyTREX(RootModel[dict[str, Quantity | datetime | time | date | bool | str | base36 | DataTable]]):
20
+ ''' A wrapper around dict, which knows how to convert to and from TREX.
21
+ It restricts the types allowed as values. Keys must be str.
22
+ '''
23
+ model_config = {'arbitrary_types_allowed':True} # needed to allow Quantity and DataTable w/o implementing the pydantic schema
24
+ '''@private'''
25
+
26
+
27
+ @classmethod
28
+ def from_trex(cls, trex:TREX) -> Self:
29
+ '''Creates a pyTREX from a TREX'''
30
+ return {seg.key: _trex_segment_to_python_type(seg) for seg in trex.segments}
31
+
32
+
33
+ def to_trex(self) -> TREX:
34
+ '''Creates a TREX'''
35
+ segments = list()
36
+ for k, v in self.root.items():
37
+ if v is None:
38
+ value = _error_value_from_python_type(v)
39
+ segments.append(ErrorSegment(key=k, value=value.value))
40
+ elif isinstance(v, bool):
41
+ value = _bool_value_from_python_type(v)
42
+ segments.append(BoolSegment(key=k, value=value.value))
43
+ elif isinstance(v, Quantity):
44
+ unece_code = unece_unit_code_from_quantity(v)
45
+ value = _numeric_value_from_python_type(v.value)
46
+ segments.append(NumericSegment(key=k, value=value.value, type=unece_code))
47
+ elif isinstance(v, (int, float)):
48
+ value = _numeric_value_from_python_type(v)
49
+ segments.append(NumericSegment(key=k, value=value.value, type='C63')) # unitless
50
+ elif isinstance(v, (datetime, time, date)):
51
+ value = _date_value_from_python_type(v)
52
+ segments.append(DateSegment(key=k, value=value.value))
53
+ elif isinstance(v, str):
54
+ if re.fullmatch(r'[A-Z0-9\-\.]*', v):
55
+ value = _alphanumeric_value_from_python_type(v)
56
+ segments.append(AlphanumericSegment(key=k, value=value.value))
57
+ else:
58
+ v = to_base36(v)
59
+ value = _text_value_from_python_type(v)
60
+ segments.append(TextSegment(key=k, value=value.value))
61
+ elif isinstance(v, base36):
62
+ value = _text_value_from_python_type(v)
63
+ segments.append(TextSegment(key=k, value=value.value))
64
+
65
+ elif isinstance(v, DataTable):
66
+ v:DataTable = v
67
+ headers = list()
68
+ for nm, rt in zip(v.col_names, v.row_template):
69
+ if isinstance(rt, bool): # must come first otherwise int matches the bool
70
+ t = 'T.B'
71
+ elif isinstance(rt, Quantity):
72
+ unece_code = unece_unit_code_from_quantity(rt)
73
+ t = unece_code
74
+ elif isinstance(rt, (datetime, time, date)):
75
+ t = 'T.D'
76
+ elif isinstance(rt, str):
77
+ if re.fullmatch(r'[A-Z0-9\-\.]*', rt):
78
+ t = 'T.A'
79
+ else:
80
+ t = 'T.T'
81
+ elif isinstance(rt, base36):
82
+ t = 'T.T'
83
+
84
+ headers.append(ColumnHeader(key=nm, type=t))
85
+ data = []
86
+ for row in v.data:
87
+ r = []
88
+ for e in row:
89
+ if e is None:
90
+ r.append(_error_value_from_python_type(e))
91
+ elif isinstance(e, bool): # must come first otherwise int matches the bool
92
+ r.append(_bool_value_from_python_type(e))
93
+ elif isinstance(e, Quantity):
94
+ r.append(_numeric_value_from_python_type(e.value))
95
+ elif isinstance(e, (int, float)):
96
+ r.append(_numeric_value_from_python_type(e))
97
+ elif isinstance(e, (datetime, time, date)):
98
+ r.append(_date_value_from_python_type(e))
99
+ elif isinstance(e, str):
100
+ if re.fullmatch(r'[A-Z0-9\-\.]*', e):
101
+ r.append(_alphanumeric_value_from_python_type(e))
102
+ else:
103
+ e = to_base36(e)
104
+ r.append(_text_value_from_python_type(e))
105
+ elif isinstance(e, base36):
106
+ r.append(_text_value_from_python_type(e))
107
+ data.append(r)
108
+ segments.append(TableSegment(key=k, column_headers=headers, data=data))
109
+ return TREX(segments=segments)
110
+
111
+ # make the usual dict methods available, for convenience
112
+ def __getitem__(self, key): return self.root[key]
113
+ def __setitem__(self, key, value): self.root[key] = value
114
+ def update(self, *args, **kwargs):
115
+ return self.root.update(*args, **kwargs)
116
+ def keys(self): return self.root.keys()
117
+ def values(self): return self.root.values()
118
+ def items(self): return self.root.items()
119
+ def __contains__(self, key): return key in self.root
120
+ def __iter__(self): return iter(self.root)
121
+ def __len__(self): return len(self.root)
122
+
123
+
124
+
125
+ # Helper functions to convert python types to TREX types
126
+
127
+ def _numeric_value_from_python_type(v:int|float):
128
+ return NumericValue(value = str(v))
129
+
130
+
131
+ def _date_value_from_python_type(v:date|time|datetime):
132
+ sd = ""
133
+ st = ""
134
+ if isinstance(v, date) or isinstance(v, datetime):
135
+ sd = v.strftime('%Y%m%d')
136
+ if isinstance(v, time) or isinstance(v, datetime):
137
+ if v.microsecond:
138
+ st = v.strftime("T%H%M%S.") + f"{v.microsecond // 1000:03d}"
139
+ elif v.second:
140
+ st = v.strftime("T%H%M%S")
141
+ else:
142
+ st = v.strftime("T%H%M")
143
+
144
+ return DateValue(value = sd + st)
145
+
146
+
147
+ def _bool_value_from_python_type(v:bool):
148
+ return BoolValue(value = 'T' if v else 'F')
149
+
150
+
151
+ def _alphanumeric_value_from_python_type(v:str):
152
+ return AlphanumericValue(value = v)
153
+
154
+
155
+ def _text_value_from_python_type(v:base36|str):
156
+ if isinstance(v, str):
157
+ logging.info('Got str for text value > converting to base36')
158
+ out = to_base36(v).root
159
+ else:
160
+ out = v.root
161
+ return TextValue(value = out)
162
+
163
+
164
+ def _binary_value_from_python_type(v:base36|str):
165
+ if isinstance(v, str):
166
+ out = v
167
+ else:
168
+ out = v.root
169
+ return BinaryValue(value = out)
170
+
171
+
172
+ def _error_value_from_python_type(v:str):
173
+ if v is None:
174
+ v = '-'
175
+ return ErrorValue(value = v)
176
+
177
+
178
+
179
+ # Helper functions to convert from TREX types to python types
180
+ def _trex_segment_to_python_type(v):
181
+ '''Converts a TREX segment to a python value. Note the segment key must be handles outside.'''
182
+ if isinstance(v, NumericSegment):
183
+ num_val = _trex_value_to_python_type(v)
184
+ u = unece_unit(v.type)
185
+ unit = u.get('symbol')
186
+ return Quantity(value=num_val, unit=unit)
187
+
188
+ # value segments are derived from their respective value type
189
+ elif isinstance(v, ValueSegment):
190
+ return _trex_value_to_python_type(v)
191
+
192
+ elif isinstance(v, TableSegment):
193
+ table = DataTable(col_names=[ch.key for ch in v.column_headers])
194
+ for row in v.data:
195
+ r = []
196
+ for e, h in zip(row, v.column_headers):
197
+ if isinstance(e, NumericValue):
198
+ u = unece_unit(h.type)
199
+ unit = u.get('symbol')
200
+ r.append(Quantity(value=e.value, unit=unit))
201
+ else:
202
+ r.append(_trex_value_to_python_type(e))
203
+ table.append(r)
204
+ return table
205
+
206
+
207
+
208
+ def _trex_value_to_python_type(v):
209
+ '''Converts a TREX value to the corresponding python type'''
210
+ if isinstance(v, NumericValue):
211
+ if '.' not in v.value and 'E' not in v.value:
212
+ return int(v.value)
213
+ else:
214
+ return float(v.value)
215
+
216
+ elif isinstance(v,DateValue):
217
+ d = v._date_time_dict
218
+ if d.get('year') and d.get('hour'): # input is only a time
219
+ return datetime(**d)
220
+ elif d.get('year'):
221
+ return date(**d)
222
+ else:
223
+ return time(**d)
224
+
225
+ elif isinstance(v, BoolValue):
226
+ if v.value == 'T':
227
+ return True
228
+ elif v.value == 'F':
229
+ return False
230
+ else:
231
+ Exception(f'{v} is not valid boolean. That really should not have been possible -- Contact the maintainers of the library')
232
+
233
+ elif isinstance(v, AlphanumericValue):
234
+ return v.value
235
+
236
+ elif isinstance(v, TextValue):
237
+ decoded = from_base36(v.value)
238
+ return decoded
239
+
240
+ elif isinstance(v, BinaryValue):
241
+ decoded = bytes(from_base36(v.value), encoding='utf-8')
242
+ return decoded
243
+
244
+ elif isinstance(v, ErrorValue):
245
+ return v.value
246
+
247
+ else:
248
+ raise (TypeError(f'Invalid type {type(v)} of segment'))
249
249
 
@@ -1,66 +1,66 @@
1
- from pydantic import BaseModel, model_validator
2
- from labfreed.well_known_keys.unece.unece_units import unece_units
3
-
4
-
5
- class Quantity(BaseModel):
6
- ''' Represents a quantity'''
7
- value: float|int
8
- unit: str | None
9
- '''unit. Use SI symbols. Set to None of the Quantity is dimensionless'''
10
- log_least_significant_digit: int|None = None
11
-
12
- @model_validator(mode='before')
13
- @classmethod
14
- def transform_inputs(cls, d:dict):
15
- if not isinstance(d, dict):
16
- return d
17
- # decimals_to_log_significant_digits
18
- if decimals:= d.pop('decimals', None):
19
- d['log_least_significant_digit'] = - decimals
20
-
21
- #dimensionless_unit
22
- unit= d.get('unit')
23
- if unit and unit in ['1', '', 'dimensionless']:
24
- d['unit'] = None
25
- return d
26
-
27
-
28
- @model_validator(mode='after')
29
- def significat_digits_for_int(self):
30
- if isinstance(self.value, int):
31
- self.log_least_significant_digit = 0
32
- return self
33
-
34
- @property
35
- def float(self) -> float:
36
- ''' for clarity returns the value'''
37
- return self.value
38
-
39
- def __str__(self):
40
- unit_symbol = self.unit
41
- if self.unit == "dimensionless" or not self.unit:
42
- unit_symbol = ""
43
- if self.log_least_significant_digit is not None:
44
- val = f"{self.value:.{self.log_least_significant_digit}f}"
45
- else:
46
- val = str(self.value)
47
- return f"{val} {unit_symbol}"
48
-
49
- def __repr__(self):
50
- return f'Quantity: {self.__repr__()}'
51
-
52
-
53
-
54
-
55
- def unece_unit_code_from_quantity(q:Quantity):
56
- if not q.unit:
57
- return 'C62' # dimensionless
58
- by_name = [ u['commonCode'] for u in unece_units() if u.get('name','') == q.unit]
59
- by_symbol = [ u['commonCode'] for u in unece_units() if u.get('symbol','') == q.unit]
60
- by_code = [ u['commonCode'] for u in unece_units() if u.get('commonCode','') == q.unit]
61
- code = list(set(by_name) | set(by_symbol) | set(by_code))
62
- if len(code) != 1:
63
- raise ValueError(f'No UNECE unit code found for Quantity {q}' )
64
- return code[0]
65
-
66
-
1
+ from pydantic import BaseModel, model_validator
2
+ from labfreed.well_known_keys.unece.unece_units import unece_units
3
+
4
+
5
+ class Quantity(BaseModel):
6
+ ''' Represents a quantity'''
7
+ value: float|int
8
+ unit: str | None
9
+ '''unit. Use SI symbols. Set to None of the Quantity is dimensionless'''
10
+ log_least_significant_digit: int|None = None
11
+
12
+ @model_validator(mode='before')
13
+ @classmethod
14
+ def transform_inputs(cls, d:dict):
15
+ if not isinstance(d, dict):
16
+ return d
17
+ # decimals_to_log_significant_digits
18
+ if decimals:= d.pop('decimals', None):
19
+ d['log_least_significant_digit'] = - decimals
20
+
21
+ #dimensionless_unit
22
+ unit= d.get('unit')
23
+ if unit and unit in ['1', '', 'dimensionless']:
24
+ d['unit'] = None
25
+ return d
26
+
27
+
28
+ @model_validator(mode='after')
29
+ def significat_digits_for_int(self):
30
+ if isinstance(self.value, int):
31
+ self.log_least_significant_digit = 0
32
+ return self
33
+
34
+ @property
35
+ def float(self) -> float:
36
+ ''' for clarity returns the value'''
37
+ return self.value
38
+
39
+ def __str__(self):
40
+ unit_symbol = self.unit
41
+ if self.unit == "dimensionless" or not self.unit:
42
+ unit_symbol = ""
43
+ if self.log_least_significant_digit is not None:
44
+ val = f"{self.value:.{self.log_least_significant_digit}f}"
45
+ else:
46
+ val = str(self.value)
47
+ return f"{val} {unit_symbol}"
48
+
49
+ def __repr__(self):
50
+ return f'Quantity: {self.__repr__()}'
51
+
52
+
53
+
54
+
55
+ def unece_unit_code_from_quantity(q:Quantity):
56
+ if not q.unit:
57
+ return 'C62' # dimensionless
58
+ by_name = [ u['commonCode'] for u in unece_units() if u.get('name','') == q.unit]
59
+ by_symbol = [ u['commonCode'] for u in unece_units() if u.get('symbol','') == q.unit]
60
+ by_code = [ u['commonCode'] for u in unece_units() if u.get('commonCode','') == q.unit]
61
+ code = list(set(by_name) | set(by_symbol) | set(by_code))
62
+ if len(code) != 1:
63
+ raise ValueError(f'No UNECE unit code found for Quantity {q}' )
64
+ return code[0]
65
+
66
+