labfreed 0.1.0__py2.py3-none-any.whl → 0.1.1__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.

Potentially problematic release.


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

labfreed/IO/parse_pac.py CHANGED
@@ -25,6 +25,7 @@ class PACID_With_Extensions(BaseModelWithValidationMessages):
25
25
  def __str__(self):
26
26
  out = str(self.pac_id)
27
27
  out += '*'.join(str(e) for e in self.extensions)
28
+ return out
28
29
 
29
30
  def get_extension_of_type(self, type:str) -> list[Extension]:
30
31
  return [e for e in self.extensions if e.type == type]
@@ -87,7 +88,7 @@ class PAC_Parser():
87
88
  def __init__(self, extension_interpreters:dict[str, Extension]=None):
88
89
  self.extension_interpreters = extension_interpreters or {'TREX': TREX, 'N': DisplayNames}
89
90
 
90
- def parse(self, pac_url:str) -> PACID_With_Extensions:
91
+ def parse(self, pac_url:str, suppress_errors=False) -> PACID_With_Extensions:
91
92
  if '*' in pac_url:
92
93
  id_str, ext_str = pac_url.split('*', 1)
93
94
  else:
@@ -98,8 +99,8 @@ class PAC_Parser():
98
99
  extensions = self._parse_extensions(ext_str)
99
100
 
100
101
  pac_with_extension = PACID_With_Extensions(pac_id=pac_id, extensions=extensions)
101
- if not pac_with_extension.is_valid():
102
- raise LabFREEDValidationError(validation_msgs = pac_with_extension.get_nested_validation_messages())
102
+ if not pac_with_extension.is_valid() and not suppress_errors:
103
+ raise LabFREEDValidationError(validation_msgs = pac_with_extension.get_nested_validation_messages(), model=pac_with_extension)
103
104
 
104
105
  return pac_with_extension
105
106
 
@@ -4,7 +4,11 @@ from abc import ABC
4
4
  from typing import Self
5
5
  from pydantic import Field, computed_field, model_validator
6
6
 
7
- from labfreed.validation import BaseModelWithValidationMessages
7
+ from rich import print
8
+ from rich.text import Text
9
+ from rich.table import Table
10
+
11
+ from labfreed.validation import BaseModelWithValidationMessages, ValidationMsgLevel
8
12
 
9
13
  from ..PAC_ID.data_model import PACID, IDSegment
10
14
 
@@ -132,24 +136,37 @@ class PAC_CAT(PACID):
132
136
  for k in duplicate_keys:
133
137
  self.add_validation_message(
134
138
  source=f"identifier {k}",
135
- type="Error",
139
+ level = ValidationMsgLevel.ERROR,
136
140
  msg=f"Duplicate key {k} in category {c.key}",
137
141
  highlight_pattern = k
138
142
  )
139
143
  return self
140
144
 
141
145
  def print_categories(self):
146
+
147
+ table = Table(title=f'Categories in {str(self)}', show_header=False)
148
+ table.add_column('0')
149
+ table.add_column('1')
142
150
  s = ''
143
151
  for i, c in enumerate(self.categories):
144
152
  if i == 0:
145
- title = 'Main Category\n----------'
153
+ title = Text('Main Category', style='bold')
146
154
  else:
147
- title = 'Category\n------ '
155
+ title = Text('Category', style='bold')
156
+
157
+ table.add_row(title)
148
158
 
149
- s += f'{title}\n'
150
- s += str(c)
151
- s += '\n'
152
- print(s)
159
+ for field_name, field_info in c.model_fields.items():
160
+ if not getattr(c, field_name):
161
+ continue
162
+ table.add_row(f"{field_name} ({field_info.alias or ''})",
163
+ f" {getattr(c, field_name)}"
164
+ )
165
+ table.add_section()
166
+ print(table)
167
+
168
+
169
+
153
170
 
154
171
 
155
172
  # @classmethod
@@ -202,18 +219,21 @@ class Category(BaseModelWithValidationMessages):
202
219
  @model_validator(mode='after')
203
220
  def warn_unusual_category_key(self):
204
221
  ''' this base class is instantiated only if the key is not a known category key'''
205
- self.add_validation_message(
206
- source=f"Category {self.key}",
207
- type="Warning",
208
- msg=f'Category key {self.key} is not a well known key. It is recommended to use well known keys only',
209
- highlight_pattern = f"{self.key}"
210
- )
222
+ if type(self) is Category:
223
+ self.add_validation_message(
224
+ source=f"Category {self.key}",
225
+ level = ValidationMsgLevel.RECOMMENDATION,
226
+ msg=f'Category key {self.key} is not a well known key. It is recommended to use well known keys only',
227
+ highlight_pattern = f"{self.key}"
228
+ )
211
229
  return self
212
230
 
213
231
 
214
232
  def __str__(self):
215
233
  s = '\n'.join( [f'{field_name} \t ({field_info.alias or ''}): \t {getattr(self, field_name)}' for field_name, field_info in self.model_fields.items() if getattr(self, field_name)])
216
234
  return s
235
+
236
+
217
237
 
218
238
 
219
239
  # def to_identifier_category(self, use_short_notation=False):
@@ -278,14 +298,14 @@ class Material_Device(Category):
278
298
  if not self.model_number:
279
299
  self.add_validation_message(
280
300
  source=f"Category {self.key}",
281
- type="Error",
301
+ level = ValidationMsgLevel.ERROR,
282
302
  msg=f'Category key {self.key} is missing mandatory field Model Number',
283
303
  highlight_pattern = f"{self.key}"
284
304
  )
285
305
  if not self.serial_number:
286
306
  self.add_validation_message(
287
307
  source=f"Category {self.key}",
288
- type="Error",
308
+ level = ValidationMsgLevel.ERROR,
289
309
  msg=f'Category key {self.key} is missing mandatory field Serial Number',
290
310
  highlight_pattern = f"{self.key}"
291
311
  )
@@ -303,7 +323,7 @@ class Material_Substance(Category):
303
323
  if not self.product_number:
304
324
  self.add_validation_message(
305
325
  source=f"Category {self.key}",
306
- type="Error",
326
+ level = ValidationMsgLevel.ERROR,
307
327
  msg=f'Category key {self.key} is missing mandatory field Product Number',
308
328
  highlight_pattern = f"{self.key}"
309
329
  )
@@ -321,7 +341,7 @@ class Material_Consumable(Category):
321
341
  if not self.product_number:
322
342
  self.add_validation_message(
323
343
  source=f"Category {self.key}",
324
- type="Error",
344
+ level = ValidationMsgLevel.ERROR,
325
345
  msg=f"Category key {self.key} is missing mandatory field 'Product Number'",
326
346
  highlight_pattern = f"{self.key}"
327
347
  )
@@ -341,7 +361,7 @@ class Data_Abstract(Category, ABC):
341
361
  if not self.id:
342
362
  self.add_validation_message(
343
363
  source=f"Category {self.key}",
344
- type="Error",
364
+ level = ValidationMsgLevel.ERROR,
345
365
  msg=f"Category key {self.key} is missing mandatory field 'ID'",
346
366
  highlight_pattern = f"{self.key}"
347
367
  )
@@ -6,7 +6,7 @@ from pydantic import Field, ValidationInfo, computed_field, conlist, model_valid
6
6
  from abc import ABC, abstractproperty, abstractstaticmethod
7
7
 
8
8
  from ..utilities.well_known_keys import WellKnownKeys
9
- from labfreed.validation import BaseModelWithValidationMessages, ValidationMessage, hsegment_pattern, domain_name_pattern
9
+ from labfreed.validation import BaseModelWithValidationMessages, ValidationMessage, ValidationMsgLevel, hsegment_pattern, domain_name_pattern
10
10
 
11
11
 
12
12
  class IDSegment(BaseModelWithValidationMessages):
@@ -23,9 +23,8 @@ class IDSegment(BaseModelWithValidationMessages):
23
23
  if not_allowed_chars := set(re.sub(hsegment_pattern, '', key)):
24
24
  self.add_validation_message(
25
25
  source=f"id segment key {key}",
26
- type="Error",
27
- msg=f"{' '.join(not_allowed_chars)} must not be used.",
28
- recommendation = "The segment key must be a valid hsegment",
26
+ level = ValidationMsgLevel.ERROR,
27
+ msg=f"{' '.join(not_allowed_chars)} must not be used. The segment key must be a valid hsegment",
29
28
  highlight_pattern = key,
30
29
  highlight_sub = not_allowed_chars
31
30
  )
@@ -33,9 +32,8 @@ class IDSegment(BaseModelWithValidationMessages):
33
32
  if not_allowed_chars := set(re.sub(hsegment_pattern, '', value)):
34
33
  self.add_validation_message(
35
34
  source=f"id segment key {value}",
36
- type="Error",
37
- msg=f"{' '.join(not_allowed_chars)} must not be used.",
38
- recommendation = "The segment key must be a valid hsegment",
35
+ level = ValidationMsgLevel.ERROR,
36
+ msg=f"{' '.join(not_allowed_chars)} must not be used. The segment key must be a valid hsegment",
39
37
  highlight_pattern = value,
40
38
  highlight_sub = not_allowed_chars
41
39
  )
@@ -44,9 +42,8 @@ class IDSegment(BaseModelWithValidationMessages):
44
42
  if not_recommended_chars := set(re.sub(r'[A-Z0-9-:+]', '', key)):
45
43
  self.add_validation_message(
46
44
  source=f"id segment key {key}",
47
- type="Recommendation",
48
- msg=f"{' '.join(not_recommended_chars)} should not be used.",
49
- recommendation = "SHOULD be limited to A-Z, 0-9, and -+",
45
+ level = ValidationMsgLevel.RECOMMENDATION,
46
+ msg=f"{' '.join(not_recommended_chars)} should not be used. Characters SHOULD be limited to upper case letters (A-Z), numbers (0-9), '-' and '+' ",
50
47
  highlight_pattern = key,
51
48
  highlight_sub = not_recommended_chars
52
49
  )
@@ -55,10 +52,10 @@ class IDSegment(BaseModelWithValidationMessages):
55
52
  if key and key not in [k.value for k in WellKnownKeys]:
56
53
  self.add_validation_message(
57
54
  source=f"id segment key {key}",
58
- type="Recommendation",
59
- msg=f"{key} is not a well known segment key.",
60
- recommendation = "RECOMMENDED to be a well-known id segment key.",
61
- highlight_pattern = key
55
+ level = ValidationMsgLevel.RECOMMENDATION,
56
+ msg=f"{key} is not a well known segment key. It is RECOMMENDED to use well-known keys.",
57
+ highlight_pattern = key,
58
+ highlight_sub=[key]
62
59
  )
63
60
 
64
61
 
@@ -66,9 +63,8 @@ class IDSegment(BaseModelWithValidationMessages):
66
63
  if not_recommended_chars := set(re.sub(r'[A-Z0-9-:+]', '', value)):
67
64
  self.add_validation_message(
68
65
  source=f"id segment value {value}",
69
- type="Recommendation",
70
- msg=f"Characters {' '.join(not_recommended_chars)} should not be used.",
71
- recommendation = "SHOULD be limited to A-Z, 0-9, and -+",
66
+ level = ValidationMsgLevel.RECOMMENDATION,
67
+ msg=f"Characters {' '.join(not_recommended_chars)} should not be used., Characters SHOULD be limited to upper case letters (A-Z), numbers (0-9), '-' and '+' ",
72
68
  highlight_pattern = value,
73
69
  highlight_sub = not_recommended_chars
74
70
  )
@@ -78,14 +74,14 @@ class IDSegment(BaseModelWithValidationMessages):
78
74
  if ':' in key:
79
75
  self.add_validation_message(
80
76
  source=f"id segment key {key}",
81
- type="Recommendation",
77
+ level = ValidationMsgLevel.RECOMMENDATION,
82
78
  msg=f"Character ':' should not be used in segment key, since this character is used to separate key and value this can lead to undefined behaviour.",
83
79
  highlight_pattern = key
84
80
  )
85
81
  if ':' in value:
86
82
  self.add_validation_message(
87
83
  source=f"id segment value {value}",
88
- type="Recommendation",
84
+ level = ValidationMsgLevel.RECOMMENDATION,
89
85
  msg=f"Character ':' should not be used in segment value, since this character is used to separate key and value this can lead to undefined behaviour.",
90
86
  highlight_pattern = value
91
87
  )
@@ -113,9 +109,8 @@ class PACID(BaseModelWithValidationMessages):
113
109
  if l > 256:
114
110
  self.add_validation_message(
115
111
  source=f"identifier",
116
- type="Error",
117
- msg=f'Identifier is {l} characters long, Identifier must not exceed 256 characters.',
118
- highlight_pattern = ""
112
+ level = ValidationMsgLevel.ERROR,
113
+ msg=f'Identifier is {l} characters long, Identifier must not exceed 256 characters.'
119
114
  )
120
115
  return self
121
116
 
@@ -125,7 +120,7 @@ class PACID(BaseModelWithValidationMessages):
125
120
  if not re.fullmatch(domain_name_pattern, self.issuer):
126
121
  self.add_validation_message(
127
122
  source="PAC-ID",
128
- type="Error",
123
+ level = ValidationMsgLevel.ERROR,
129
124
  highlight_pattern=self.issuer,
130
125
  msg=f"Issuer must be a valid domain name. "
131
126
  )
@@ -134,7 +129,7 @@ class PACID(BaseModelWithValidationMessages):
134
129
  if not_recommended_chars := set(re.sub(r'[A-Z0-9\.-]', '', self.issuer)):
135
130
  self.add_validation_message(
136
131
  source="PAC-ID",
137
- type="Recommendation",
132
+ level = ValidationMsgLevel.RECOMMENDATION,
138
133
  highlight_pattern=self.issuer,
139
134
  highlight_sub=not_recommended_chars,
140
135
  msg=f"Characters {' '.join(not_recommended_chars)} should not be used. Issuer SHOULD contain only the characters A-Z, 0-9, -, and . "
@@ -150,7 +145,7 @@ class PACID(BaseModelWithValidationMessages):
150
145
  for k in duplicate_keys:
151
146
  self.add_validation_message(
152
147
  source=f"identifier {k}",
153
- type="Recommendation",
148
+ level = ValidationMsgLevel.RECOMMENDATION,
154
149
  msg=f"Duplicate segment key {k}. This will probably lead to undefined behaviour",
155
150
  highlight_pattern = k
156
151
  )
@@ -26,7 +26,7 @@ macros:
26
26
  service_type: userhandover-generic
27
27
  application_intents:
28
28
  - dummy
29
- template_url: https://mettorius.com/om/an={$..*['240'].value}
29
+ template_url: https://mettorius.com/
30
30
 
31
31
 
32
32
  cit:
@@ -1,4 +1,7 @@
1
+ from concurrent.futures import ThreadPoolExecutor, as_completed
2
+ from enum import Enum, auto
1
3
  from pydantic import BaseModel, Field, field_validator
4
+ from requests import RequestException, request, ConnectionError, ReadTimeout
2
5
  from rich import print
3
6
  from rich.table import Table
4
7
 
@@ -29,18 +32,40 @@ class CIT(BaseModelWithValidationMessages):
29
32
 
30
33
 
31
34
 
35
+ class ServiceState(Enum):
36
+ ACTIVE = auto()
37
+ INACTIVE = auto()
38
+ UNKNOWN = auto()
32
39
 
33
40
  class Service(BaseModelWithValidationMessages):
34
41
  service_name: str
35
42
  application_intents:list[str]
36
43
  service_type:str
37
44
  url:str
45
+ active:ServiceState =ServiceState.UNKNOWN
38
46
 
47
+ def check_state(self):
48
+ try:
49
+ r = request('get',self.url, timeout=2)
50
+ if r.status_code < 400:
51
+ self.active = ServiceState.ACTIVE
52
+ else:
53
+ self.active = ServiceState.INACTIVE
54
+ except RequestException as e:
55
+ print(f"Request failed: {e}")
56
+ self.active = ServiceState.INACTIVE
57
+
39
58
 
40
59
  class CITEvaluated(BaseModelWithValidationMessages):
41
60
  origin: str = ""
42
61
  services: list[Service] = Field(default_factory=list)
43
62
 
63
+ def update_states(self):
64
+ with ThreadPoolExecutor(max_workers=10) as executor:
65
+ futures = [executor.submit(s.check_state) for s in self.services]
66
+ for _ in as_completed(futures):
67
+ pass # just wait for all to finish
68
+
44
69
  def __str__(self):
45
70
  out = [f'CIT (origin {self.origin})']
46
71
  for s in self.services:
@@ -52,8 +77,9 @@ class CITEvaluated(BaseModelWithValidationMessages):
52
77
 
53
78
  table.add_column("Service Name")
54
79
  table.add_column("URL")
80
+ table.add_column('Reachable')
55
81
 
56
82
  for s in self.services:
57
- table.add_row(s.service_name, s.url)
83
+ table.add_row(s.service_name, s.url, s.active.name)
58
84
 
59
85
  print(table)
@@ -8,7 +8,7 @@ from typing import Annotated, Literal
8
8
  from pydantic import PrivateAttr, RootModel, ValidationError, field_validator, model_validator, Field
9
9
  from labfreed.TREX.unece_units import unece_unit, unece_unit_codes, unece_units, unit_name, unit_symbol
10
10
  from labfreed.utilities.utility_types import DataTable, Quantity, Unit, unece_unit_code_from_quantity
11
- from labfreed.validation import BaseModelWithValidationMessages
11
+ from labfreed.validation import BaseModelWithValidationMessages, ValidationMsgLevel
12
12
  from abc import ABC, abstractmethod
13
13
 
14
14
  from labfreed.PAC_ID.extensions import Extension
@@ -180,7 +180,7 @@ class BoolValue(ValueMixin):
180
180
  if not self.value in ['T', 'F']:
181
181
  self.add_validation_message(
182
182
  source=f"TREX boolean value {self.value}",
183
- type="Error",
183
+ level= ValidationMsgLevel.ERROR,
184
184
  msg=f'{self.value} is no valid boolean. Must be T or F',
185
185
  highlight_pattern = f'{self.value}',
186
186
  highlight_sub=[c for c in self.value]
@@ -207,7 +207,7 @@ class AlphanumericValue(ValueMixin):
207
207
  if re.match(r'[a-z]', self.value):
208
208
  self.add_validation_message(
209
209
  source=f"TREX value {self.value}",
210
- type="Error",
210
+ level= ValidationMsgLevel.ERROR,
211
211
  msg=f"Lower case characters are not allowed.",
212
212
  highlight_pattern = self.value
213
213
  )
@@ -215,7 +215,7 @@ class AlphanumericValue(ValueMixin):
215
215
  if not_allowed_chars := set(re.sub(r'[A-Z0-9\.-]', '', self.value)):
216
216
  self.add_validation_message(
217
217
  source=f"TREX value {self.value}",
218
- type="Error",
218
+ level= ValidationMsgLevel.ERROR,
219
219
  msg=f"Characters {','.join(not_allowed_chars)} are not allowed in alphanumeric segment",
220
220
  highlight_pattern = self.value,
221
221
  highlight_sub=not_allowed_chars
@@ -242,7 +242,7 @@ class TextValue(ValueMixin):
242
242
  if not_allowed_chars := set(re.sub(r'[A-Z0-9]', '', self.value)):
243
243
  self.add_validation_message(
244
244
  source=f"TREX value {self.value}",
245
- type="Error",
245
+ level= ValidationMsgLevel.ERROR,
246
246
  msg=f"Characters {','.join(not_allowed_chars)} are not allowed in text segment. Base36 encoding only allows A-Z0-9",
247
247
  highlight_pattern = self.value,
248
248
  highlight_sub=not_allowed_chars
@@ -268,7 +268,7 @@ class BinaryValue(ValueMixin):
268
268
  if not_allowed_chars := set(re.sub(r'[A-Z0-9]', '', self.value)):
269
269
  self.add_validation_message(
270
270
  source=f"TREX value {self.value}",
271
- type="Error",
271
+ tlevel= ValidationMsgLevel.ERROR,
272
272
  msg=f"Characters {','.join(not_allowed_chars)} are not allowed in text segment. Base36 encoding only allows A-Z0-9",
273
273
  highlight_pattern = self.value,
274
274
  highlight_sub=not_allowed_chars
@@ -286,7 +286,7 @@ class ErrorValue(ValueMixin):
286
286
  if not_allowed_chars := set(re.sub(r'[A-Z0-9\.-]', '', self.value)):
287
287
  self.add_validation_message(
288
288
  source=f"TREX value {self.value}",
289
- type="Error",
289
+ level= ValidationMsgLevel.ERROR,
290
290
  msg=f"Characters {','.join(not_allowed_chars)} are not allowed in error segment",
291
291
  highlight_pattern = self.value,
292
292
  highlight_sub=not_allowed_chars
@@ -309,7 +309,7 @@ class ValueSegment(TREX_Segment, ValueMixin, ABC):
309
309
  if not self.type in valid_types:
310
310
  self.add_validation_message(
311
311
  source=f"TREX value segment {self.key}",
312
- type="Error",
312
+ level= ValidationMsgLevel.ERROR,
313
313
  msg=f"Type {self.type} is invalid. Must be 'T.D', 'T.B', 'T.A', 'T.T', 'T.X', 'E' or a UNECE unit",
314
314
  highlight_pattern = self.type
315
315
  )
@@ -385,7 +385,7 @@ class ColumnHeader(BaseModelWithValidationMessages):
385
385
  if not_allowed_chars := set(re.sub(r'[A-Z0-9\.-]', '', self.key)):
386
386
  self.add_validation_message(
387
387
  source=f"TREX table column {self.key}",
388
- type="Error",
388
+ level= ValidationMsgLevel.ERROR,
389
389
  msg=f"Column header key contains invalid characters: {','.join(not_allowed_chars)}",
390
390
  highlight_pattern = f'{self.key}$',
391
391
  highlight_sub=not_allowed_chars
@@ -398,7 +398,7 @@ class ColumnHeader(BaseModelWithValidationMessages):
398
398
  if not self.type in valid_types:
399
399
  self.add_validation_message(
400
400
  source=f"TREX table column {self.key}",
401
- type="Error",
401
+ level= ValidationMsgLevel.ERROR,
402
402
  msg=f"Type '{self.type}' is invalid. Must be 'T.D', 'T.B', 'T.A', 'T.T', 'T.X', 'E' or a UNECE unit",
403
403
  highlight_pattern = self.type
404
404
  )
@@ -435,7 +435,7 @@ class TREX_Table(TREX_Segment):
435
435
  if len(self.column_headers) != most_common_len:
436
436
  self.add_validation_message(
437
437
  source=f"Table {self.key}",
438
- type="Error",
438
+ level= ValidationMsgLevel.ERROR,
439
439
  msg=f"Size mismatch: Table header contains {self.column_names} keys, while most rows have {most_common_len}",
440
440
  highlight_pattern = self.key
441
441
  )
@@ -448,7 +448,7 @@ class TREX_Table(TREX_Segment):
448
448
  if len(row) != expected_row_len:
449
449
  self.add_validation_message(
450
450
  source=f"Table {self.key}",
451
- type="Error",
451
+ level= ValidationMsgLevel.ERROR,
452
452
  msg=f"Size mismatch: Table row {i} contains {len(row)} elements. Expected size is {expected_row_len}",
453
453
  highlight_pattern = row.serialize_for_trex()
454
454
  )
@@ -480,7 +480,7 @@ class TREX_Table(TREX_Segment):
480
480
  except AssertionError:
481
481
  self.add_validation_message(
482
482
  source=f"Table {self.key}",
483
- type="Error",
483
+ level= ValidationMsgLevel.ERROR,
484
484
  msg=f"Type mismatch: Table row {i}, column {nm} is of wrong type. According to the header it should be {t_expected}",
485
485
  highlight_pattern = row.serialize_for_trex(),
486
486
  highlight_sub=[c for c in e.value]
@@ -490,7 +490,7 @@ class TREX_Table(TREX_Segment):
490
490
  for m in msg:
491
491
  self.add_validation_message(
492
492
  source=f"Table {self.key}",
493
- type="Error",
493
+ level= ValidationMsgLevel.ERROR,
494
494
  msg=m.problem_msg,
495
495
  highlight_pattern = row.serialize_for_trex(),
496
496
  highlight_sub=[c for c in e.value]
labfreed/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
  Python implementation of LabFREED building blocks
3
3
  '''
4
4
 
5
- __version__ = "0.1.0"
5
+ __version__ = "0.1.1"
labfreed/validation.py CHANGED
@@ -1,37 +1,52 @@
1
+ from enum import Enum, auto
2
+ import re
1
3
  from pydantic import BaseModel, Field, PrivateAttr
2
4
  from typing import List, Set, Tuple
3
5
 
4
6
  from rich import print
5
7
  from rich.text import Text
8
+ from rich.table import Table
6
9
 
7
10
 
8
11
  domain_name_pattern = r"(?!-)([A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,63}"
9
12
  hsegment_pattern = r"[A-Za-z0-9_\-\.~!$&'()+,:;=@]|%[0-9A-Fa-f]{2}"
10
13
 
11
14
 
15
+ class ValidationMsgLevel(Enum):
16
+ ERROR = auto()
17
+ ERROR_AUTO_FIX = auto()
18
+ WARNING = auto()
19
+ RECOMMENDATION = auto()
20
+ INFO = auto()
21
+
12
22
  class ValidationMessage(BaseModel):
23
+ source_id:int
13
24
  source:str
14
- type: str
25
+ level: ValidationMsgLevel
15
26
  problem_msg:str
16
27
  recommendation_msg: str = ""
17
28
  highlight:str = "" #this can be used to highlight problematic parts
18
- highlight_sub:list[str] = Field(default_factory=list)
29
+ highlight_sub_patterns:list[str] = Field(default_factory=list)
30
+
31
+
32
+
19
33
 
20
- @property
21
- def emphazised_highlight(self):
22
- fmt = lambda s: f'[emph]{s}[/emph]'
23
34
 
24
- if not self.highlight_sub:
25
- return fmt(self.highlight)
35
+ # @property
36
+ # def emphazised_highlight(self):
37
+ # fmt = lambda s: f'[emph]{s}[/emph]'
38
+
39
+ # if not self.highlight_sub_patterns:
40
+ # return fmt(self.highlight)
26
41
 
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)
42
+ # result = []
43
+ # for c in self.highlight:
44
+ # if c in self.highlight_sub_patterns:
45
+ # result.append(fmt(c))
46
+ # else:
47
+ # result.append(c)
33
48
 
34
- return ''.join(result)
49
+ # return ''.join(result)
35
50
 
36
51
 
37
52
  class LabFREEDValidationError(ValueError):
@@ -51,10 +66,10 @@ class BaseModelWithValidationMessages(BaseModel):
51
66
  The purpose of that is to allow only minimal validation but on top check for stricter recommendations"""
52
67
  _validation_messages: list[ValidationMessage] = PrivateAttr(default_factory=list)
53
68
 
54
- def add_validation_message(self, *, msg: str, type:str, recommendation:str="", source:str="", highlight_pattern="", highlight_sub=None):
69
+ def add_validation_message(self, *, msg: str, level:ValidationMsgLevel, recommendation:str="", source:str="", highlight_pattern="", highlight_sub=None):
55
70
  if not highlight_sub:
56
71
  highlight_sub = []
57
- w = ValidationMessage(problem_msg=msg, recommendation_msg=recommendation, source=source, type=type, highlight=highlight_pattern, highlight_sub=highlight_sub)
72
+ w = ValidationMessage(problem_msg=msg, recommendation_msg=recommendation, source=source, level=level, highlight=highlight_pattern, highlight_sub_patterns=highlight_sub, source_id=id(self))
58
73
 
59
74
  if not w in self._validation_messages:
60
75
  self._validation_messages.append(w)
@@ -113,47 +128,116 @@ class BaseModelWithValidationMessages(BaseModel):
113
128
  return filter_warnings(self.get_nested_validation_messages())
114
129
 
115
130
 
116
- def print_validation_messages(self, str_to_highlight_in=None, target='console'):
117
- if not str_to_highlight_in:
118
- str_to_highlight_in = str(self)
131
+ def str_for_validation_msg(self, validation_msg:ValidationMessage):
132
+ if validation_msg.source_id == id(self):
133
+ return validation_msg.source_id
134
+ #return validation_msg.emphasize_in(self(str))
135
+ else:
136
+ return str(self)
137
+
138
+ def str_highlighted(self):
139
+ raise NotImplementedError("Subclasses must implement format_special()")
140
+
141
+
142
+
143
+ def _emphasize_in(self, validation_msg, validation_node_str:str, fmt, color='black'):
144
+ if validation_msg.highlight_sub_patterns:
145
+ replacements = validation_msg.highlight_sub_patterns
146
+ else:
147
+ replacements = [validation_msg.highlight]
148
+ # Sort patterns by length descending to avoid subpattern clobbering
149
+ sorted_patterns = sorted(replacements, key=len, reverse=True)
150
+ # Escape the patterns for regex safety
151
+ escaped_patterns = [re.escape(p) for p in sorted_patterns]
152
+ # Create one regex pattern with alternation (longest first)
153
+ pattern = re.compile("|".join(escaped_patterns))
154
+
155
+ out = pattern.sub(lambda m: fmt(m.group(0)), validation_node_str)
156
+ return out
157
+
158
+
159
+ def print_validation_messages(self, target='console'):
119
160
  msgs = self.get_nested_validation_messages()
120
- print('\n'.join(['\n',
121
- '=======================================',
122
- 'Validation Results',
123
- '---------------------------------------'
124
- ]
125
- )
126
- )
127
161
 
162
+ table = Table(title=f"Validation Results", show_header=False)
163
+
164
+ col = lambda s: table.add_column(s, vertical='top')
165
+ col("-")
166
+
167
+
128
168
  if not msgs:
129
- print('All clear!')
169
+ table.add_row('All clear!', end_section=True)
130
170
  return
131
171
 
132
172
  for m in msgs:
133
- if m.type.casefold() == "error":
173
+ if m.level == ValidationMsgLevel.ERROR:
134
174
  color = 'red'
135
175
  else:
136
176
  color = 'yellow'
137
177
 
138
- text = Text.from_markup(f'\n [bold {color}]{m.type} [/bold {color}] in \t {m.source}' )
139
- print(text)
140
178
  match target:
141
179
  case 'markdown':
142
- formatted_highlight = m.emphazised_highlight.replace('emph', f'🔸').replace('[/', '').replace('[', '').replace(']', '')
180
+ fmt = lambda s: f'🔸{s}🔸'
143
181
  case 'console':
144
- formatted_highlight = m.emphazised_highlight.replace('emph', f'bold {color}')
182
+ fmt = lambda s: f'[{color} bold]{s}[/{color} bold]'
145
183
  case 'html':
146
- formatted_highlight = m.emphazised_highlight.replace('emph', f'b').replace('[', '<').replace(']', '>')
147
- fmtd = str_to_highlight_in.replace(m.highlight, formatted_highlight)
148
- fmtd = Text.from_markup(fmtd)
149
- print(fmtd)
150
- print(Text.from_markup(f'{m.problem_msg}'))
184
+ fmt = lambda s: f'<span class="val_{color}">{s}</span>'
185
+ case 'html_styled':
186
+ fmt = lambda s: f'<b style="color:{color}>{s}</b>'
187
+
188
+ serialized = str(self)
189
+ emphazised_highlight = self._emphasize_in(m, serialized, fmt=fmt, color=color)
190
+
191
+ txt = f'[bold {color}]{m.level.name} [/bold {color}]'
192
+ txt += '\n' + f'{m.problem_msg}'
193
+ txt += '\n' + emphazised_highlight
194
+
195
+ table.add_row( txt)
196
+ table.add_section()
197
+
198
+ print(table)
199
+
200
+ # def print_validation_messages_(self, str_to_highlight_in=None, target='console'):
201
+ # if not str_to_highlight_in:
202
+ # str_to_highlight_in = str(self)
203
+ # msgs = self.get_nested_validation_messages()
204
+ # print('\n'.join(['\n',
205
+ # '=======================================',
206
+ # 'Validation Results',
207
+ # '---------------------------------------'
208
+ # ]
209
+ # )
210
+ # )
211
+
212
+ # if not msgs:
213
+ # print('All clear!')
214
+ # return
215
+
216
+ # for m in msgs:
217
+ # if m.level.casefold() == "error":
218
+ # color = 'red'
219
+ # else:
220
+ # color = 'yellow'
221
+
222
+ # text = Text.from_markup(f'\n [bold {color}]{m.level} [/bold {color}] in \t {m.source}' )
223
+ # print(text)
224
+ # match target:
225
+ # case 'markdown':
226
+ # formatted_highlight = m.emphazised_highlight.replace('emph', f'🔸').replace('[/', '').replace('[', '').replace(']', '')
227
+ # case 'console':
228
+ # formatted_highlight = m.emphazised_highlight.replace('emph', f'bold {color}')
229
+ # case 'html':
230
+ # formatted_highlight = m.emphazised_highlight.replace('emph', f'b').replace('[', '<').replace(']', '>')
231
+ # fmtd = str_to_highlight_in.replace(m.highlight, formatted_highlight)
232
+ # fmtd = Text.from_markup(fmtd)
233
+ # print(fmtd)
234
+ # print(Text.from_markup(f'{m.problem_msg}'))
151
235
 
152
236
 
153
237
 
154
238
  def filter_errors(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
155
- return [ m for m in val_msg if m.type.casefold() == "error" ]
239
+ return [ m for m in val_msg if m.level == ValidationMsgLevel.ERROR ]
156
240
 
157
241
  def filter_warnings(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
158
- return [ m for m in val_msg if m.type.casefold() != "error" ]
242
+ return [ m for m in val_msg if m.level != ValidationMsgLevel.ERROR ]
159
243
 
@@ -0,0 +1,279 @@
1
+ Metadata-Version: 2.4
2
+ Name: labfreed
3
+ Version: 0.1.1
4
+ Summary: Python implementation of LabFREED building blocks
5
+ Author-email: Reto Thürer <thuerer.r@buchi.com>
6
+ Description-Content-Type: text/markdown
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: numpy>=2.2.4
10
+ Requires-Dist: pydantic>=2.11.3
11
+ Requires-Dist: segno>=1.6.6
12
+ Requires-Dist: typer>=0.15.2
13
+
14
+ # LabFREED for Python
15
+
16
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![PyPI](https://img.shields.io/pypi/v/labfreed.svg)](https://pypi.org/project/labfreed/) ![Python Version](https://img.shields.io/pypi/pyversions/labfreed)
17
+
18
+ <!--
19
+ [![Tests](https://github.com/retothuerer/LabFREED/actions/workflows/ci.yml/badge.svg)](https://github.com/retothuerer/LabFREED/actions/workflows/ci.yml)
20
+ -->
21
+
22
+ This is a Python implementation of [LabFREED](https://labfreed.wega-it.com) building blocks.
23
+
24
+ ## Supported Building Blocks
25
+ - PAC-ID
26
+ - PAC-CAT
27
+ - TREX
28
+ - Display Extension
29
+ - PAC-ID Resolver
30
+
31
+ ## Installation
32
+ You can install LabFREED from [PyPI](https://pypi.org/project/labfreed/) using pip:
33
+
34
+ ```bash
35
+ pip install labfreed
36
+ ```
37
+
38
+
39
+ ## Usage Examples
40
+ > ⚠️ **Note:** These examples are building on each other. Imports and parsing are not repeated in each example.
41
+ <!-- BEGIN EXAMPLES -->
42
+ ```python
43
+ # import built ins
44
+ import os
45
+ ```
46
+ ### Parse a simple PAC-ID
47
+
48
+ ```python
49
+ from labfreed.IO.parse_pac import PAC_Parser
50
+
51
+
52
+ # Parse the PAC-ID
53
+ pac_str = 'HTTPS://PAC.METTORIUS.COM/-MD/bal500/@1234'
54
+ pac_id = PAC_Parser().parse(pac_str).pac_id
55
+
56
+ # Check validity of this PAC-ID
57
+ is_valid = pac_id.is_valid()
58
+ print(f'PAC-ID is valid: {is_valid}')
59
+ ```
60
+ ```text
61
+ >> PAC-ID is valid: True
62
+ ```
63
+ ### Show recommendations:
64
+ Note that the PAC-ID -- while valid -- uses characters which are not recommended (results in larger QR code).
65
+ There is a nice function to highlight problems
66
+
67
+ ```python
68
+ pac_id.print_validation_messages(target='markdown')
69
+ ```
70
+ ```text
71
+ >> Validation Results
72
+ >> ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
73
+ >> │ RECOMMENDATION │
74
+ >> │ Characters l a b should not be used., Characters SHOULD be limited to upper case letters (A-Z), numbers (0-9), '-' and '+' │
75
+ >> │ HTTPS://PAC.METTORIUS.COM/-MD/🔸b🔸🔸a🔸🔸l🔸500/@1234 │
76
+ >> ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
77
+ >> │ RECOMMENDATION │
78
+ >> │ Characters @ should not be used., Characters SHOULD be limited to upper case letters (A-Z), numbers (0-9), '-' and '+' │
79
+ >> │ HTTPS://PAC.METTORIUS.COM/-MD/bal500/🔸@🔸1234 │
80
+ >> └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
81
+ ```
82
+ ### Save as QR Code
83
+
84
+ ```python
85
+ from labfreed.IO.generate_qr import save_qr_with_markers
86
+
87
+ save_qr_with_markers(pac_str, fmt='png')
88
+ ```
89
+ ```text
90
+ >> Large QR: Provided URL is not alphanumeric!
91
+ >> Size: 29
92
+ >> Version: 3
93
+ >> Error Level: M
94
+ ```
95
+ ### PAC-CAT
96
+
97
+ ```python
98
+ from labfreed.PAC_CAT.data_model import PAC_CAT
99
+ pac_str = 'HTTPS://PAC.METTORIUS.COM/-DR/XQ908756/-MD/bal500/@1234'
100
+ pac_id = PAC_Parser().parse(pac_str).pac_id
101
+ if isinstance(pac_id, PAC_CAT):
102
+ pac_id.print_categories()
103
+ ```
104
+ ```text
105
+ >> Categories in
106
+ >> HTTPS://PAC.METTORIUS.COM/-DR/XQ90
107
+ >> 8756/-MD/bal500/@1234
108
+ >> ┌────────────────────┬───────────┐
109
+ >> │ Main Category │ │
110
+ >> │ key () │ -DR │
111
+ >> │ id (21) │ XQ908756 │
112
+ >> ├────────────────────┼───────────┤
113
+ >> │ Category │ │
114
+ >> │ key () │ -MD │
115
+ >> │ model_number (240) │ bal500 │
116
+ >> │ serial_number (21) │ @1234 │
117
+ >> └────────────────────┴───────────┘
118
+ ```
119
+ ### Parse a PAC-ID with extensions
120
+ PAC-ID can have extensions. Here we parse a PAC-ID with attached display names and summary.
121
+
122
+ ```python
123
+ pac_str = 'HTTPS://PAC.METTORIUS.COM/-MD/BAL500/1234*N$N/WM633OV3E5DGJW2BEG0PDM1EA7*SUM$TREX/WEIGHT$GRM:67.89'
124
+ pac_id = PAC_Parser().parse(pac_str)
125
+
126
+ # Display Name
127
+ display_names = pac_id.get_extension('N') # display name has name 'N'
128
+ print(display_names)
129
+ ```
130
+ ```text
131
+ >> Display names: My Balance ❤️
132
+ ```
133
+ ```python
134
+ # TREX
135
+ trexes = pac_id.get_extension_of_type('TREX')
136
+ trex = trexes[0] # there could be multiple trexes. In this example there is only one, though
137
+ v = trex.get_segment('WEIGHT').to_python_type()
138
+ print(f'WEIGHT = {v}')
139
+ ```
140
+ ```text
141
+ >> WEIGHT = 67.89 g
142
+ ```
143
+ ### Create a PAC-ID with Extensions
144
+
145
+ #### Create PAC-ID
146
+
147
+ ```python
148
+ from labfreed.PAC_ID.data_model import PACID, IDSegment
149
+ from labfreed.utilities.well_known_keys import WellKnownKeys
150
+
151
+ pac_id = PACID(issuer='METTORIUS.COM', identifier=[IDSegment(key=WellKnownKeys.SERIAL, value='1234')])
152
+ pac_str = pac_id.serialize()
153
+ print(pac_str)
154
+ ```
155
+ ```text
156
+ >> HTTPS://PAC.METTORIUS.COM/21:1234
157
+ ```
158
+ #### Create a TREX
159
+ TREX can conveniently be created from a python dictionary.
160
+ Note that utility types for Quantity (number with unit) and table are needed
161
+
162
+ ```python
163
+ from datetime import datetime
164
+ from labfreed.TREX.data_model import TREX
165
+ from labfreed.utilities.utility_types import Quantity, DataTable, Unit
166
+
167
+ # Create TREX
168
+ trex = TREX(name_='DEMO')
169
+ # Add value segments of different type
170
+ trex.update(
171
+ {
172
+ 'STOP': datetime(year=2024,month=5,day=5,hour=13,minute=6),
173
+ 'TEMP': Quantity(value=10.15, unit=Unit(name='kelvin', symbol='K')),
174
+ 'OK':False,
175
+ 'COMMENT': 'FOO',
176
+ 'COMMENT2':'£'
177
+ }
178
+ )
179
+
180
+ # Create a table
181
+ table = DataTable(['DURATION', 'Date', 'OK', 'COMMENT'])
182
+ table.append([Quantity(value=1, unit=Unit(symbol='h', name='hour')), datetime.now(), True, 'FOO'])
183
+ table.append([ 1.1, datetime.now(), True, 'BAR'])
184
+ table.append([ 1.3, datetime.now(), False, 'BLUBB'])
185
+ #add the table to the trex
186
+ trex.update({'TABLE': table})
187
+
188
+ # Validation also works the same way for TREX
189
+ trex.print_validation_messages(target='markdown')
190
+ ```
191
+ ```text
192
+ >> Validation Results
193
+ >> ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
194
+ >> │ ERROR │
195
+ >> │ Column header key contains invalid characters: e,a,t │
196
+ >> │ DEMO$TREX/STOP$T.D:20240505T1306+TEMP$KEL:10.15+OK$T.B:F+COMMENT$T.A:FOO+COMMENT2$T.T:12G3+TABLE$$DURATION$HUR:D🔸a🔸🔸t🔸🔸e🔸$T.D:OK$T.B:COMMENT$T.A::1:20250411T090300.103:T:FOO::1… │
197
+ >> └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
198
+ ```
199
+ ```python
200
+ # there is an error. 'Date' uses lower case. Lets fix it
201
+ d = trex.dict()
202
+ d['TABLE'].col_names[1] = 'DATE'
203
+ trex = TREX(name_='DEMO')
204
+ trex.update(d)
205
+ ```
206
+ #### Combine PAC-ID and TREX and serialize
207
+
208
+ ```python
209
+ from labfreed.IO.parse_pac import PACID_With_Extensions
210
+
211
+ pac_with_trex = PACID_With_Extensions(pac_id=pac_id, extensions=[trex])
212
+ pac_str = pac_with_trex.serialize()
213
+ print(pac_str)
214
+ ```
215
+ ```text
216
+ >> HTTPS://PAC.METTORIUS.COM/21:1234*DEMO$TREX/STOP$T.D:20240505T1306+TEMP$KEL:10.15+OK$T.B:F+COMMENT$T.A:FOO+COMMENT2$T.T:12G3+TABLE$$DURATION$HUR:DATE$T.D:OK$T.B:COMMENT$T.A::1:20250411T090300.103:T:FOO::1.1:20250411T090300.103:T:BAR::1.3:20250411T090300.103:F:BLUBB
217
+ ```
218
+ ## PAC-ID Resolver
219
+
220
+ ```python
221
+ from labfreed.PAC_ID_Resolver.resolver import PAC_ID_Resolver, load_cit
222
+ # Get a CIT
223
+ dir = os.path.dirname(__file__)
224
+ p = os.path.join(dir, 'cit_mine.yaml')
225
+ cit = load_cit(p)
226
+
227
+ # validate the CIT
228
+ cit.is_valid()
229
+ cit.print_validation_messages()
230
+ ```
231
+ ```python
232
+ # resolve a pac id
233
+ service_groups = PAC_ID_Resolver(cits=[cit]).resolve(pac_with_trex)
234
+ for sg in service_groups:
235
+ sg.update_states()
236
+ sg.print()
237
+
238
+ 5
239
+ ```
240
+ ```text
241
+ >> Request failed: HTTPSConnectionPool(host='aaaaaaaaaaaa.mettorius.com.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL:
242
+ >> SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1028)')))
243
+ >> Services from origin 'MINE
244
+ >> ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
245
+ >> ┃ Service Name ┃ URL ┃ Reachable ┃
246
+ >> ┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
247
+ >> │ AAAAAAAAAAAAAAAAAAhhhhhh │ https://AAAAAAAAAAAA.METTORIUS.COM.com/ │ INACTIVE │
248
+ >> └──────────────────────────┴─────────────────────────────────────────┴───────────┘
249
+ >> Services from origin 'PAC.METTORIUS.COM
250
+ >> ┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
251
+ >> ┃ Service Name ┃ URL ┃ Reachable ┃
252
+ >> ┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
253
+ >> │ Shop │ https://mettorius.com/shop/an= │ INACTIVE │
254
+ >> │ Manual │ https://mettorius.com/om/an= │ INACTIVE │
255
+ >> │ _3 │ https://mettorius.com/ │ ACTIVE │
256
+ >> └──────────────┴────────────────────────────────┴───────────┘
257
+ ```
258
+ <!-- END EXAMPLES -->
259
+
260
+
261
+
262
+ ## Change Log
263
+
264
+ ### v0.1.1
265
+ - minor internal improvements and bugfixes
266
+
267
+ ### v0.1.0
268
+ - DRAFT Support for PAC-ID Resolver
269
+
270
+ ### v0.0.20
271
+ - bugfix in TREX table to dict conversion
272
+ - markdown compatible validation printing
273
+
274
+ ### v0.0.19
275
+ - supports PAC-ID, PAC-CAT, TREX and DisplayName
276
+ - QR generation
277
+ - ok-ish test coverage
278
+
279
+
@@ -1,24 +1,24 @@
1
- labfreed/__init__.py,sha256=LebroXmoov8_KunUmdQn2n6f793Y9WSU58Q9C_6Dxz4,87
2
- labfreed/validation.py,sha256=PW08_mVpp27Bju3MblACHXupAXbl60GjRaSGXw161pU,6442
1
+ labfreed/__init__.py,sha256=TzQ186YXdEkFSNsduP7q0XO3d8HEiNMNkHMZ6t6Zg-Y,87
2
+ labfreed/validation.py,sha256=29Ys6qrCQ4T689juc19aiNJfHEgggI-hO7gfwrrNKWI,9554
3
3
  labfreed/DisplayNameExtension/DisplayNameExtension.py,sha256=l9JZY2eRS0V-H5h3-WXIHiiBJuljns-_e_t9Bp84_CU,1155
4
4
  labfreed/IO/generate_qr.py,sha256=wdZSf51Id03xSY8liK2RfB7WyGAw9O4s0VhZzrkAa-g,16680
5
- labfreed/IO/parse_pac.py,sha256=fZKNWuVGnYZCY0k6xVB1JOmLN5g6D8RW5CIq6-6Y9KM,6288
5
+ labfreed/IO/parse_pac.py,sha256=1rV-pJ4kUgxQMDl8DJ14SUmHV37ruKYZYBqiJjPtCUw,6381
6
6
  labfreed/PAC_CAT/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
7
- labfreed/PAC_CAT/data_model.py,sha256=qPrpzooI5tmH9P5JxfHWn0Ruung66gJ9KojCu5lXVzY,14320
7
+ labfreed/PAC_CAT/data_model.py,sha256=9cjLA7u4APtiFbZyYp9iuYhl5mOGUPS30gXwYl0G_24,15153
8
8
  labfreed/PAC_ID/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- labfreed/PAC_ID/data_model.py,sha256=vALf8thR4zq7WrVZaKnIiS8hGBJ7UuKoy2heOY97tOs,7481
9
+ labfreed/PAC_ID/data_model.py,sha256=IZB6F2p3E90mEalnYdGjPvMGcuvNWOrqqlKXGuDyJQ4,7601
10
10
  labfreed/PAC_ID/extensions.py,sha256=YKIE-aFf1jdL4sqCqUe3txjnP8-dA2zCJrT5lBSjvuE,1092
11
- labfreed/PAC_ID_Resolver/cit.yaml,sha256=-UcP-vKwhwGmYZVkVOjPZWEwHPzxUDcTFZFVM2_zMF0,2017
12
- labfreed/PAC_ID_Resolver/data_types.py,sha256=i9F4FzLdAUpcFIX9fg9bLFRqQRe0E2ptb17etdtsj_c,1609
11
+ labfreed/PAC_ID_Resolver/cit.yaml,sha256=G_cH1f0Z7MUGgu7YR1cVhTrETaSBHvTVC7sbTbUJ-Rc,1992
12
+ labfreed/PAC_ID_Resolver/data_types.py,sha256=XFUmlw-sGLbYNdHdyb-8SZMK6BwMMQWKuLfIGO-yR3g,2650
13
13
  labfreed/PAC_ID_Resolver/resolver.py,sha256=2EKpDEzOM8Ri-Gy5cKVE6aoHsVFRqZE9fd_llG4wrFI,8178
14
14
  labfreed/TREX/UneceUnits.json,sha256=kwfQSp_nTuWbADfBBgqTWrvPl6XtM5SedEVLbMJrM7M,898953
15
- labfreed/TREX/data_model.py,sha256=tGgjp76wbS1MSS1Ep842CZyintsbecTZrlapj8WwGH8,29704
15
+ labfreed/TREX/data_model.py,sha256=etb4dd2kNFGDU5upQkH1YhlutRsvblMst4FdHQyuDu0,29972
16
16
  labfreed/TREX/parse.py,sha256=86962VEJpkrTcT436iFIB5dNed5WHABzpjxRjkA3PXo,2043
17
17
  labfreed/TREX/unece_units.py,sha256=scPKdsPzY1neAdFOhA08_tRZaR-yplM8mBhIzzDqZBk,3006
18
18
  labfreed/utilities/base36.py,sha256=_yX8aQ1OwrK5tnJU1NUEzQSFGr9xAVnNvPObpNzCPYs,2895
19
19
  labfreed/utilities/utility_types.py,sha256=dM0qZgshF3cHJThVzia7UIAOdkNLKukAaaduLqKSaMY,2195
20
20
  labfreed/utilities/well_known_keys.py,sha256=nqk66kHdSwJTJfMKlP-xQbBglS8F_NoWsGkfOVITFN0,331
21
- labfreed-0.1.0.dist-info/licenses/LICENSE,sha256=gHFOv9FRKHxO8cInP3YXyPoJnuNeqrvcHjaE_wPSsQ8,1100
22
- labfreed-0.1.0.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
23
- labfreed-0.1.0.dist-info/METADATA,sha256=bTILYvmhLnxvXo2HM9EBlwmOQttglU5Hc-Bk9dKKWcY,7625
24
- labfreed-0.1.0.dist-info/RECORD,,
21
+ labfreed-0.1.1.dist-info/licenses/LICENSE,sha256=gHFOv9FRKHxO8cInP3YXyPoJnuNeqrvcHjaE_wPSsQ8,1100
22
+ labfreed-0.1.1.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
23
+ labfreed-0.1.1.dist-info/METADATA,sha256=uxdO6fEUnZPsWnW_sTZD34ptdj9xBPIXlUPk0mJQI6c,13103
24
+ labfreed-0.1.1.dist-info/RECORD,,
@@ -1,266 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: labfreed
3
- Version: 0.1.0
4
- Summary: Python implementation of LabFREED building blocks
5
- Author-email: Reto Thürer <thuerer.r@buchi.com>
6
- Description-Content-Type: text/markdown
7
- License-Expression: MIT
8
- License-File: LICENSE
9
- Requires-Dist: numpy>=2.2.4
10
- Requires-Dist: pydantic>=2.11.3
11
- Requires-Dist: segno>=1.6.6
12
- Requires-Dist: typer>=0.15.2
13
-
14
- # LabFREED for Python
15
-
16
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![PyPI](https://img.shields.io/pypi/v/labfreed.svg)](https://pypi.org/project/labfreed/) ![Python Version](https://img.shields.io/pypi/pyversions/labfreed)
17
-
18
- <!--
19
- [![Tests](https://github.com/retothuerer/LabFREED/actions/workflows/ci.yml/badge.svg)](https://github.com/retothuerer/LabFREED/actions/workflows/ci.yml)
20
- -->
21
-
22
- This is a Python implementation of [LabFREED](https://labfreed.wega-it.com) building blocks.
23
-
24
- ## Supported Building Blocks
25
- - PAC-ID
26
- - PAC-CAT
27
- - TREX
28
- - Display Extension
29
-
30
- ## Installation
31
- You can install LabFREED from [PyPI](https://pypi.org/project/labfreed/) using pip:
32
-
33
- ```bash
34
- pip install labfreed
35
- ```
36
-
37
-
38
- ## Usage Examples
39
- > ⚠️ **Note:** These examples are building on each other. Imports and parsing are not repeated in each example.
40
- <!-- BEGIN EXAMPLES -->
41
- ```python
42
- # import built ins
43
- import os
44
- ```
45
- ### Parse a simple PAC-ID
46
-
47
- ```python
48
- from labfreed.IO.parse_pac import PAC_Parser
49
-
50
-
51
- # Parse the PAC-ID
52
- pac_str = 'HTTPS://PAC.METTORIUS.COM/-MD/bal500/@1234'
53
- pac_id = PAC_Parser().parse(pac_str).pac_id
54
-
55
- # Check validity of this PAC-ID
56
- pac_id = PAC_Parser().parse(pac_str).pac_id
57
- is_valid = pac_id.is_valid()
58
- print(f'PAC-ID is valid: {is_valid}')
59
- ```
60
- ```text
61
- >> PAC-ID is valid: True
62
- ```
63
- ### Show recommendations:
64
- Note that the PAC-ID -- while valid -- uses characters which are not recommended (results in larger QR code).
65
- There is a nice function to highlight problems
66
-
67
- ```python
68
- pac_id.print_validation_messages(target='markdown')
69
- ```
70
- ```text
71
- >> =======================================
72
- >> Validation Results
73
- >> ---------------------------------------
74
- >>
75
- >> Recommendation in id segment value bal500
76
- >> HTTPS://PAC.METTORIUS.COM/-MD/🔸b🔸🔸a🔸🔸l🔸500/@1234
77
- >> Characters a b l should not be used.
78
- >>
79
- >> Recommendation in id segment value @1234
80
- >> HTTPS://PAC.METTORIUS.COM/-MD/bal500/🔸@🔸1234
81
- >> Characters @ should not be used.
82
- >>
83
- >> Warning in Category -MD
84
- >> HTTPS://PAC.METTORIUS.COM/🔸-MD🔸/bal500/@1234
85
- >> Category key -MD is not a well known key. It is recommended to use well known keys only
86
- ```
87
- ### Save as QR Code
88
-
89
- ```python
90
- from labfreed.IO.generate_qr import save_qr_with_markers
91
-
92
- save_qr_with_markers(pac_str, fmt='png')
93
- ```
94
- ```text
95
- >> Large QR: Provided URL is not alphanumeric!
96
- >> Size: 29
97
- >> Version: 3
98
- >> Error Level: M
99
- ```
100
- ### PAC-CAT
101
-
102
- ```python
103
- from labfreed.PAC_CAT.data_model import PAC_CAT
104
- pac_str = 'HTTPS://PAC.METTORIUS.COM/-DR/XQ908756/-MD/bal500/@1234'
105
- pac_id = PAC_Parser().parse(pac_str).pac_id
106
- if isinstance(pac_id, PAC_CAT):
107
- pac_id.print_categories()
108
- ```
109
- ```text
110
- >> Main Category
111
- >> ----------
112
- >> key (): -DR
113
- >> id (21): XQ908756
114
- >> Category
115
- >> ------
116
- >> key (): -MD
117
- >> model_number (240): bal500
118
- >> serial_number (21): @1234
119
- ```
120
- ### Parse a PAC-ID with extensions
121
- PAC-ID can have extensions. Here we parse a PAC-ID with attached display names and summary.
122
-
123
- ```python
124
- pac_str = 'HTTPS://PAC.METTORIUS.COM/-MD/BAL500/1234*N$N/WM633OV3E5DGJW2BEG0PDM1EA7*SUM$TREX/WEIGHT$GRM:67.89'
125
- pac_id = PAC_Parser().parse(pac_str)
126
-
127
- # Display Name
128
- display_names = pac_id.get_extension('N') # display name has name 'N'
129
- print(display_names)
130
- ```
131
- ```text
132
- >> Display names: My Balance ❤️
133
- ```
134
- ```python
135
- # TREX
136
- trexes = pac_id.get_extension_of_type('TREX')
137
- trex = trexes[0] # there could be multiple trexes. In this example there is only one, though
138
- v = trex.get_segment('WEIGHT').to_python_type()
139
- print(f'WEIGHT = {v}')
140
- ```
141
- ```text
142
- >> WEIGHT = 67.89 g
143
- ```
144
- ### Create a PAC-ID with Extensions
145
-
146
- #### Create PAC-ID
147
-
148
- ```python
149
- from labfreed.PAC_ID.data_model import PACID, IDSegment
150
- from labfreed.utilities.well_known_keys import WellKnownKeys
151
-
152
- pac_id = PACID(issuer='METTORIUS.COM', identifier=[IDSegment(key=WellKnownKeys.SERIAL, value='1234')])
153
- pac_str = pac_id.serialize()
154
- print(pac_str)
155
- ```
156
- ```text
157
- >> HTTPS://PAC.METTORIUS.COM/21:1234
158
- ```
159
- #### Create a TREX
160
- TREX can conveniently be created from a python dictionary.
161
- Note that utility types for Quantity (number with unit) and table are needed
162
-
163
- ```python
164
- from datetime import datetime
165
- from labfreed.TREX.data_model import TREX
166
- from labfreed.utilities.utility_types import Quantity, DataTable, Unit
167
-
168
- # Create TREX
169
- trex = TREX(name_='DEMO')
170
- # Add value segments of different type
171
- trex.update(
172
- {
173
- 'STOP': datetime(year=2024,month=5,day=5,hour=13,minute=6),
174
- 'TEMP': Quantity(value=10.15, unit=Unit(name='kelvin', symbol='K')),
175
- 'OK':False,
176
- 'COMMENT': 'FOO',
177
- 'COMMENT2':'£'
178
- }
179
- )
180
-
181
- # Create a table
182
- table = DataTable(['DURATION', 'Date', 'OK', 'COMMENT'])
183
- table.append([Quantity(value=1, unit=Unit(symbol='h', name='hour')), datetime.now(), True, 'FOO'])
184
- table.append([ 1.1, datetime.now(), True, 'BAR'])
185
- table.append([ 1.3, datetime.now(), False, 'BLUBB'])
186
- #add the table to the trex
187
- trex.update({'TABLE': table})
188
-
189
- # Validation also works the same way for TREX
190
- trex.print_validation_messages(target='markdown')
191
- ```
192
- ```text
193
- >> =======================================
194
- >> Validation Results
195
- >> ---------------------------------------
196
- >>
197
- >> Error in TREX table column Date
198
- >> DEMO$TREX/STOP$T.D:20240505T1306+TEMP$KEL:10.15+OK$T.B:F+COMMENT$T.A:FOO+COMMENT2$T.T:12G3+TABLE$$DURATION$HUR:D🔸a🔸🔸t🔸🔸e🔸$T.D:OK$T.B:COMMENT$T.A::1:20250410T094130.847:T:FOO::1.1:20250410T094130.847:T:BAR::1.3
199
- >> :20250410T094130.847:F:BLUBB
200
- >> Column header key contains invalid characters: t,a,e
201
- ```
202
- ```python
203
- # there is an error. 'Date' uses lower case. Lets fix it
204
- d = trex.dict()
205
- d['TABLE'].col_names[1] = 'DATE'
206
- trex = TREX(name_='DEMO')
207
- trex.update(d)
208
- ```
209
- #### Combine PAC-ID and TREX and serialize
210
-
211
- ```python
212
- from labfreed.IO.parse_pac import PACID_With_Extensions
213
-
214
- pac_with_trex = PACID_With_Extensions(pac_id=pac_id, extensions=[trex])
215
- pac_str = pac_with_trex.serialize()
216
- print(pac_str)
217
- ```
218
- ```text
219
- >> HTTPS://PAC.METTORIUS.COM/21:1234*DEMO$TREX/STOP$T.D:20240505T1306+TEMP$KEL:10.15+OK$T.B:F+COMMENT$T.A:FOO+COMMENT2$T.T:12G3+TABLE$$DURATION$HUR:DATE$T.D:OK$T.B:COMMENT$T.A::1:20250410T094130.847:T:FOO::1.1:20250410T094130.847:T:BAR::1.3:20250410T094130.847:F:BLUBB
220
- ```
221
- ## PAC-ID Resolver
222
-
223
- ```python
224
- from labfreed.PAC_ID_Resolver.resolver import PAC_ID_Resolver, load_cit
225
- # Get a CIT
226
- dir = os.path.dirname(__file__)
227
- p = os.path.join(dir, 'cit_mine.yaml')
228
- cit = load_cit(p)
229
-
230
- # validate the CIT
231
- cit.is_valid()
232
- cit.print_validation_messages()
233
- ```
234
- ```text
235
- >> [Error during execution: name '__file__' is not defined]
236
- ```
237
- ```python
238
- # resolve a pac id
239
- service_groups = PAC_ID_Resolver(cits=[cit]).resolve(pac_with_trex)
240
- for sg in service_groups:
241
- sg.print()
242
-
243
- 5
244
- ```
245
- ```text
246
- >> [Error during execution: name 'cit' is not defined]
247
- ```
248
- <!-- END EXAMPLES -->
249
-
250
-
251
-
252
- ## Change Log
253
-
254
- ### v0.1.0
255
- - DRAFT Support for PAC-ID Resolver
256
-
257
- ### v0.0.20
258
- - bugfix in TREX table to dict conversion
259
- - markdown compatible validation printing
260
-
261
- ### v0.0.19
262
- - supports PAC-ID, PAC-CAT, TREX and DisplayName
263
- - QR generation
264
- - ok-ish test coverage
265
-
266
-