labfreed 0.0.4__py3-none-any.whl → 0.0.5__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.
@@ -65,23 +65,23 @@ class Material_Misc(Material_Consumable):
65
65
 
66
66
  class Data_Result(CATBase):
67
67
  category_key: str = Field(default='-DR', frozen=True)
68
- id:str = Field( alias='240', min_length=1)
68
+ id:str = Field( alias='21', min_length=1)
69
69
 
70
70
  class Data_Method(CATBase):
71
71
  category_key: str = Field(default='-DM', frozen=True)
72
- id:str = Field( alias='240', min_length=1)
72
+ id:str = Field( alias='21', min_length=1)
73
73
 
74
74
  class Data_Calibration(CATBase):
75
75
  category_key: str = Field(default='-DC', frozen=True)
76
- id:str = Field( alias='240', min_length=1)
76
+ id:str = Field( alias='21', min_length=1)
77
77
 
78
78
  class Data_Progress(CATBase):
79
79
  category_key: str = Field(default='-DP', frozen=True)
80
- id:str = Field( alias='240', min_length=1)
80
+ id:str = Field( alias='21', min_length=1)
81
81
 
82
82
  class Data_Static(CATBase):
83
83
  category_key: str = Field(default='-DS', frozen=True)
84
- id:str = Field( alias='240', min_length=1)
84
+ id:str = Field( alias='21', min_length=1)
85
85
 
86
86
 
87
87
 
@@ -1,20 +1,90 @@
1
+ import re
1
2
  from typing import Optional
2
3
  from typing_extensions import Self
3
- from pydantic import BaseModel, Field, computed_field, conlist, model_validator
4
+ from pydantic import Field, ValidationInfo, computed_field, conlist, model_validator, field_validator
5
+ from ..validation import BaseModelWithWarnings, ValidationWarning, hsegment_pattern, domain_name_pattern
4
6
  from abc import ABC, abstractproperty, abstractstaticmethod
7
+ from .well_known_segment_keys import WellKnownSegmentKeys
5
8
 
6
- class IDSegment(BaseModel):
7
- key:Optional[str] = Field(None, pattern=r'^[A-Z0-9-+]+$', min_length=1)
8
- value:str = Field(..., pattern=r'^[A-Z0-9-+]+$', min_length=1)
9
9
 
10
+ class IDSegment(BaseModelWithWarnings):
11
+ key:str|None = None
12
+ value:str
13
+ @model_validator(mode="after")
14
+ def validate_segment(cls, model):
15
+ key = model.key or ""
16
+ value = model.value
17
+
18
+ # MUST be a valid hsegment according to RFC 1738, but without * (see PAC-ID Extension)
19
+ # This means it must be true for both, key and value
20
+ if not_allowed_chars := set(re.sub(hsegment_pattern, '', key)):
21
+ raise ValueError(f"id segment key {key} contains invalid characters {' '.join(not_allowed_chars)}.")
22
+
23
+ if not_allowed_chars := set(re.sub(hsegment_pattern, '', value)):
24
+ raise ValueError(f"id segment key {value} contains invalid characters {' '.join(not_allowed_chars)}.")
10
25
 
11
- class Category(BaseModel):
26
+ # Segment key SHOULD be limited to A-Z, 0-9, and -+..
27
+ if not_recommended_chars := set(re.sub(r'[A-Z0-9-:+]', '', key)):
28
+ model.add_warning(
29
+ source=f"id segment key {key}",
30
+ type="Recommendation",
31
+ msg=f"{' '.join(not_recommended_chars)} should not be used.",
32
+ recommendation = "SHOULD be limited to A-Z, 0-9, and -+",
33
+ highlight_pattern = key,
34
+ highlight_sub = not_recommended_chars
35
+ )
36
+
37
+ # Segment key should be in Well know keys
38
+ if key and key not in [k.value for k in WellKnownSegmentKeys]:
39
+ model.add_warning(
40
+ source=f"id segment key {key}",
41
+ type="Recommendation",
42
+ msg=f"{key} is not a well known segment key.",
43
+ recommendation = "RECOMMENDED to be a well-known id segment key.",
44
+ highlight_pattern = key
45
+ )
46
+
47
+
48
+ # Segment value SHOULD be limited to A-Z, 0-9, and -+..
49
+ if not_recommended_chars := set(re.sub(r'[A-Z0-9-:+]', '', value)):
50
+ model.add_warning(
51
+ source=f"id segment value {value}",
52
+ type="Recommendation",
53
+ msg=f"Characters {' '.join(not_recommended_chars)} should not be used.",
54
+ recommendation = "SHOULD be limited to A-Z, 0-9, and -+",
55
+ highlight_pattern = value,
56
+ highlight_sub = not_recommended_chars
57
+ )
58
+
59
+ # Segment value SHOULD be limited to A-Z, 0-9, and :-+ for new designs.
60
+ # this means that ":" in key or value is problematic
61
+ if ':' in key:
62
+ model.add_warning(
63
+ source=f"id segment key {key}",
64
+ type="Recommendation",
65
+ 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.",
66
+ highlight_pattern = key
67
+ )
68
+ if ':' in value:
69
+ model.add_warning(
70
+ source=f"id segment value {value}",
71
+ type="Recommendation",
72
+ 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.",
73
+ highlight_pattern = value
74
+ )
75
+
76
+ return model
77
+
78
+
79
+
80
+
81
+ class Category(BaseModelWithWarnings):
12
82
  key:str|None = None
13
83
  segments: list[IDSegment]
14
84
 
15
85
 
16
- class Identifier(BaseModel):
17
- segments: conlist(IDSegment, min_length=1) = Field(..., exclude=True) # exclude=True prevents this from being serialized by Pydantic
86
+ class Identifier(BaseModelWithWarnings):
87
+ segments: conlist(IDSegment, min_length=1) = Field(..., exclude=True) # type: ignore # exclude=True prevents this from being serialized by Pydantic
18
88
 
19
89
  @computed_field
20
90
  @property
@@ -45,6 +115,20 @@ class Identifier(BaseModel):
45
115
  if duplicate_keys:
46
116
  raise ValueError(f'Duplicate keys {",".join(duplicate_keys)} in category {c.key}')
47
117
  return self
118
+
119
+ @model_validator(mode='after')
120
+ def check_length(self) -> Self:
121
+ l = 0
122
+ for s in self.segments:
123
+ if s.key:
124
+ l += len(s.key)
125
+ l += 1 # for ":"
126
+ l += len(s.value)
127
+ l += len(self.segments) - 1 # account for "/" separating the segments
128
+
129
+ if l > 256:
130
+ raise ValueError(f'Identifier is {l} characters long, Identifier must not exceed 256 characters.')
131
+ return self
48
132
 
49
133
  @staticmethod
50
134
  def from_categories(categories:list[Category]) :
@@ -57,7 +141,7 @@ class Identifier(BaseModel):
57
141
 
58
142
 
59
143
 
60
- class Extension(ABC, BaseModel):
144
+ class Extension(ABC, BaseModelWithWarnings):
61
145
 
62
146
  @abstractproperty
63
147
  def name(self)->str:
@@ -99,12 +183,29 @@ class UnknownExtension(Extension):
99
183
 
100
184
 
101
185
 
102
- class PACID(BaseModel):
186
+ class PACID(BaseModelWithWarnings):
103
187
  issuer:str
104
188
  identifier: Identifier
105
189
 
190
+ @model_validator(mode="after")
191
+ def validate_issuer(cls, model):
192
+ if not re.fullmatch(domain_name_pattern, model.issuer):
193
+ raise ValueError("Issuer must be a valid domain name.")
194
+
195
+
196
+ # recommendation that A-Z, 0-9, -, and . should be used
197
+ if not_recommended_chars := set(re.sub(r'[A-Z0-9\.-]', '', model.issuer)):
198
+ model.add_warning(
199
+ source="PAC-ID",
200
+ type="Recommendation",
201
+ highlight_pattern=model.issuer,
202
+ highlight_sub=not_recommended_chars,
203
+ msg=f"Characters {' '.join(not_recommended_chars)} should not be used. Issuer SHOULD contain only the characters A-Z, 0-9, -, and . "
204
+ )
205
+ return model
206
+
106
207
 
107
- class PACID_With_Extensions(BaseModel):
208
+ class PACID_With_Extensions(BaseModelWithWarnings):
108
209
  pac_id: PACID
109
210
  extensions: list[Extension] = Field(default_factory=list)
110
211
 
labfreed/PAC_ID/parse.py CHANGED
@@ -4,6 +4,8 @@ import re
4
4
  from types import MappingProxyType
5
5
  from .data_model import *
6
6
 
7
+ from ..validation import extract_warnings, ValidationWarning
8
+
7
9
 
8
10
  category_conventions = MappingProxyType(
9
11
  {
@@ -29,7 +31,7 @@ class PAC_Parser():
29
31
  def __init__(self, extension_interpreters:dict[str, Extension]=None):
30
32
  self.extension_interpreters = extension_interpreters or {}
31
33
 
32
- def parse_pac_url(self, pac_url:str) -> PACID_With_Extensions:
34
+ def parse_pac_url(self, pac_url:str) -> tuple[PACID_With_Extensions, list[ValidationWarning] ]:
33
35
  if '*' in pac_url:
34
36
  id_str, ext_str = pac_url.split('*', 1)
35
37
  else:
@@ -37,8 +39,12 @@ class PAC_Parser():
37
39
  ext_str = ""
38
40
 
39
41
  pac_id = self.parse_pac_id(id_str)
40
- extensions = self.parse_extensions(ext_str)
41
- return PACID_With_Extensions(pac_id=pac_id, extensions=extensions)
42
+ extensions = self.parse_extensions(ext_str)
43
+
44
+ pac_with_extension = PACID_With_Extensions(pac_id=pac_id, extensions=extensions)
45
+ warnings = extract_warnings(pac_with_extension)
46
+
47
+ return pac_with_extension, warnings
42
48
 
43
49
 
44
50
  def parse_id_segments(self, identifier:str):
@@ -46,6 +52,8 @@ class PAC_Parser():
46
52
  return []
47
53
 
48
54
  id_segments = list()
55
+ if len(identifier) > 0 and identifier[0] == '/':
56
+ identifier = identifier[1:]
49
57
  for s in identifier.split('/'):
50
58
  tmp = s.split(':')
51
59
 
@@ -85,10 +93,11 @@ class PAC_Parser():
85
93
  default_keys = None
86
94
  id_segments = self.parse_id_segments(d.get('identifier'))
87
95
  id_segments = self._apply_category_defaults(id_segments)
88
-
89
- return PACID(issuer= d.get('issuer'),
96
+
97
+ pac = PACID(issuer= d.get('issuer'),
90
98
  identifier=Identifier(segments=id_segments)
91
99
  )
100
+ return pac
92
101
 
93
102
 
94
103
  def parse_extensions(self, extensions_str:str|None) -> list[Extension]:
@@ -4,7 +4,7 @@ from .data_model import *
4
4
 
5
5
 
6
6
  class PAC_Serializer():
7
- def to_url(self, pac:PACID|PACID_With_Extensions, extensions:list[Extension]=None, use_short_notation_for_extensions=False) -> str:
7
+ def to_url(self, pac:PACID|PACID_With_Extensions, extensions:list[Extension]=None, use_short_notation_for_extensions=False, uppercase_only=False) -> str:
8
8
  if isinstance(pac, PACID_With_Extensions):
9
9
  if extensions:
10
10
  raise ValueError('Extensions were given twice, as part of PACID_With_Extension and as method parameter.')
@@ -13,7 +13,10 @@ class PAC_Serializer():
13
13
  issuer = pac.issuer
14
14
  extensions_str = self._serialize_extensions(extensions, use_short_notation_for_extensions)
15
15
  id_segments = self._serialize_id_segments(pac.identifier.segments)
16
- return f"HTTPS://PAC.{issuer}{id_segments}{extensions_str}".upper()
16
+ out = f"HTTPS://PAC.{issuer}{id_segments}{extensions_str}"
17
+ if uppercase_only:
18
+ out = out.upper()
19
+ return out
17
20
 
18
21
 
19
22
  def _serialize_id_segments(self, segments):
@@ -0,0 +1,16 @@
1
+ from enum import Enum
2
+
3
+
4
+ class WellKnownSegmentKeys(Enum):
5
+ GTIN = '01'
6
+ BATCH = '10'
7
+ SERIAL = '21'
8
+ ADDITIONAL_IDINTIFIER = '240'
9
+ RUN_ID_ABSOLUTE = 'RNR'
10
+ SAMPLE_ID = 'SMP'
11
+ EXPERIMENT_ID = 'EXP'
12
+ RESULT_ID = 'RST'
13
+ METHOD_ID = 'MTD'
14
+ REPORT_ID = 'RPT'
15
+ TIMESTAMP = 'TS'
16
+ VERSION = 'V'
@@ -60,26 +60,35 @@ class PydanticUncertainQuantity(BaseModel):
60
60
 
61
61
  unit_map = [
62
62
  ('MGM', units.milligram),
63
+ ('GRM', units.gram),
64
+ ('KGM', units.kilogram),
65
+
63
66
  ('CEL', units.celsius),
67
+
64
68
  ('LTR', units.liter),
65
69
  ('MLT', units.milliliter),
66
- ('GRM', units.gram),
67
- ('KGM', units.kilogram),
70
+
68
71
  ('C34', units.mole),
69
72
  ('D43',units.atomic_mass_unit),
73
+
70
74
  ('1', units.dimensionless),
71
75
  ('C62', units.dimensionless),
76
+
72
77
  ('BAR',units.bar),
73
78
  ('MBR',units.millibar),
74
79
  ('KBA',units.kilobar),
80
+
75
81
  ('RPM', units.rpm),
76
- ('HUR', units.hour),
82
+
77
83
  ('HTZ', units.hertz),
78
84
  ('KHZ', units.kilohertz),
79
85
  ('MHZ',units.megahertz),
86
+
80
87
  ('SEC', units.second),
81
88
  ('MIN', units.minute),
82
- ('URH', units.hour)
89
+ ('HUR', units.hour),
90
+
91
+ ('MTR', units.meter)
83
92
 
84
93
  ]
85
94
 
labfreed/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
  Python implementation of LabFREED building blocks
3
3
  '''
4
4
 
5
- __version__ = "0.0.4"
5
+ __version__ = "0.0.5"
labfreed/validation.py ADDED
@@ -0,0 +1,71 @@
1
+ from pydantic import BaseModel, Field, PrivateAttr
2
+ from typing import List, Set, Tuple
3
+
4
+
5
+ domain_name_pattern = r"(?!-)([A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,63}"
6
+ hsegment_pattern = r"[A-Za-z0-9_\-\.~!$&'()+,:;=@]|%[0-9A-Fa-f]{2}"
7
+
8
+
9
+ class ValidationWarning(BaseModel):
10
+ source:str
11
+ type: str
12
+ problem_msg:str
13
+ recommendation_msg: str = ""
14
+ highlight:str = "" #this can be used to highlight problematic parts
15
+ highlight_sub:list[str] = Field(default_factory=list())
16
+
17
+
18
+
19
+
20
+ class BaseModelWithWarnings(BaseModel):
21
+ """ Extension of Pydantic BaseModel, so that validator can issue warnings.
22
+ 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)
24
+
25
+ def add_warning(self, *, msg: str, type:str, recommendation:str="", source:str="", highlight_pattern="", highlight_sub=None):
26
+ if not highlight_sub:
27
+ 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)
31
+
32
+ def get_warnings(self) -> list[ValidationWarning]:
33
+ return self._warnings
34
+
35
+ def clear_warnings(self):
36
+ self._warnings.clear()
37
+
38
+
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.
43
+
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()]
57
+
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)
61
+
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))
69
+
70
+ return warnings_list
71
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labfreed
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Python implementation of LabFREED building blocks
5
5
  Author-email: Reto Thürer <thuerer.r@buchi.com>
6
6
  Requires-Python: >=3.10
@@ -13,3 +13,22 @@ License-File: LICENSE
13
13
  # LabFREED for Python
14
14
 
15
15
  A python implementation of [LabFREED](www.labfreed.com).
16
+
17
+
18
+
19
+ # Examples:
20
+
21
+ ## Parse a PAC ID
22
+
23
+
24
+ ## Parse a PAC-ID with extensions
25
+
26
+
27
+
28
+ ## Create a PAC-ID
29
+
30
+ ## Create a PAC-ID and T-REX for a titration curve
31
+
32
+
33
+
34
+
@@ -0,0 +1,19 @@
1
+ labfreed/__init__.py,sha256=z3yAWKlMQqYGwnI20F528XNm0hlpE6qhvvfIkhRuSoI,87
2
+ labfreed/validation.py,sha256=dG-SubAguub67RTvw51xlErkqXbzzq_5rwxJwWg6SfY,2869
3
+ labfreed/DisplayNameExtension/DisplayNameExtension.py,sha256=FlT53u1EntpsLmho6GZtgIWBZBNWkl9STxzJBvojR6M,1033
4
+ labfreed/DisplayNameExtension/base36.py,sha256=2lwmEMWm8qrFJkcrP-nMPwS0eCm2THhCJ3Vk-TdGQg0,2455
5
+ labfreed/PAC_CAT/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
6
+ labfreed/PAC_CAT/data_model.py,sha256=hob-WNs2-633LmxQ7Ot3RBpcvStYFzdj20QDQZOQyqY,4306
7
+ labfreed/PAC_ID/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ labfreed/PAC_ID/data_model.py,sha256=DKU9Rptl9DcxiUdFf28XMyy4YcGUG7-a1EWuEpIFXVc,7931
9
+ labfreed/PAC_ID/parse.py,sha256=t38ABXZ0siUVDW5oEDmkF7uQ6iSX8Dbkeg-lWNlOgWA,5011
10
+ labfreed/PAC_ID/serialize.py,sha256=0BhF7aXGlLpr312lkBvl1O5fXDFZeLLPgSBddO9Y86Q,1963
11
+ labfreed/PAC_ID/well_known_segment_keys.py,sha256=zrzMvvS42urPpiwinI-IhHPgT3r86zEBl4TlEMOfzbU,338
12
+ labfreed/TREXExtension/data_model.py,sha256=eT4KyQklTO6m-wA28KyJ8wzT8ONhG3fOB3JU6b19ScY,8011
13
+ labfreed/TREXExtension/parse.py,sha256=Y04UK1KlMLG9tE_d7cQOiAJpm8Zh49UoJYjY7Lfqa4Y,1812
14
+ labfreed/TREXExtension/uncertainty.py,sha256=l3WxrLnWTQYfX28gFisXwDcVPvT8bCAd4q6Xl02dRdE,1117
15
+ labfreed/TREXExtension/unit_utilities.py,sha256=WzrC1CZMgBccxADXFP6nLkMWVDqCCkNb3trPyt3BvF8,3826
16
+ labfreed-0.0.5.dist-info/licenses/LICENSE,sha256=gHFOv9FRKHxO8cInP3YXyPoJnuNeqrvcHjaE_wPSsQ8,1100
17
+ labfreed-0.0.5.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82
18
+ labfreed-0.0.5.dist-info/METADATA,sha256=T_mGIHKlIMASuQnbuW_L4XXqUuxUc0DB6yGOcI-N-wE,594
19
+ labfreed-0.0.5.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- labfreed/__init__.py,sha256=PyF18K_9O9PbwpUrHsMwMVLHyepmCT4UIeVP5iCns90,87
2
- labfreed/DisplayNameExtension/DisplayNameExtension.py,sha256=FlT53u1EntpsLmho6GZtgIWBZBNWkl9STxzJBvojR6M,1033
3
- labfreed/DisplayNameExtension/base36.py,sha256=2lwmEMWm8qrFJkcrP-nMPwS0eCm2THhCJ3Vk-TdGQg0,2455
4
- labfreed/PAC_CAT/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
5
- labfreed/PAC_CAT/data_model.py,sha256=y1cTFZknObi5XOk4aqfgeJv6_G8Rjd9mxTRwNCQR0M4,4311
6
- labfreed/PAC_ID/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- labfreed/PAC_ID/data_model.py,sha256=oVVqspdZ4B_C4V1rOmcde2_EqWUpDJc6vDUnX4Z1ZPw,3123
8
- labfreed/PAC_ID/parse.py,sha256=ZmCpuht7o6RgDZCweF7Y0k7QFTd0gLtUpmjFsIlz_wA,4668
9
- labfreed/PAC_ID/serialize.py,sha256=ujI4SFs-TsFb4FVU2hzwardEWu8-2MYjewE0IyC1yok,1871
10
- labfreed/TREXExtension/data_model.py,sha256=eT4KyQklTO6m-wA28KyJ8wzT8ONhG3fOB3JU6b19ScY,8011
11
- labfreed/TREXExtension/parse.py,sha256=Y04UK1KlMLG9tE_d7cQOiAJpm8Zh49UoJYjY7Lfqa4Y,1812
12
- labfreed/TREXExtension/uncertainty.py,sha256=l3WxrLnWTQYfX28gFisXwDcVPvT8bCAd4q6Xl02dRdE,1117
13
- labfreed/TREXExtension/unit_utilities.py,sha256=ZhivJnEXMvz7HxLJjI1oJ8y_pcPxW2abKkouiqTo4I4,3775
14
- labfreed-0.0.4.dist-info/licenses/LICENSE,sha256=gHFOv9FRKHxO8cInP3YXyPoJnuNeqrvcHjaE_wPSsQ8,1100
15
- labfreed-0.0.4.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82
16
- labfreed-0.0.4.dist-info/METADATA,sha256=KKfAscBUcBVVNUSHT3KjlqXjA3_fFd5APom23P_Boh0,446
17
- labfreed-0.0.4.dist-info/RECORD,,