labfreed 1.0.0a13__py3-none-any.whl → 1.0.0a15__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.
@@ -1,12 +1,12 @@
1
1
 
2
- from datetime import date, datetime, time
2
+ from datetime import UTC, date, datetime, time
3
3
  import json
4
4
  from typing import Literal
5
5
  import warnings
6
6
  from pydantic import RootModel, field_validator
7
7
 
8
8
  from labfreed.labfreed_infrastructure import LabFREED_BaseModel
9
- from labfreed.pac_attributes.api_data_models.response import AttributeBase, AttributeGroup, BoolAttribute, DateTimeAttribute, NumericAttribute, NumericValue, ObjectAttribute, ReferenceAttribute, ResourceAttribute, TextAttribute
9
+ from labfreed.pac_attributes.api_data_models.response import AttributeBase, AttributeGroup, BoolAttribute, BoolListAttribute, DateTimeAttribute, DateTimeListAttribute, NumericAttribute, NumericListAttribute, NumericValue, ObjectAttribute, ReferenceAttribute, ReferenceListAttribute, ResourceAttribute, ResourceListAttribute, TextAttribute, TextListAttribute
10
10
  from labfreed.pac_attributes.client.attribute_cache import CacheableAttributeGroup
11
11
  from labfreed.pac_id.pac_id import PAC_ID
12
12
  from labfreed.trex.pythonic.quantity import Quantity
@@ -66,13 +66,21 @@ class pyAttributes(RootModel[list[pyAttribute]]):
66
66
  value_list = attribute.value_list
67
67
  first_value = value_list[0]
68
68
  if isinstance(first_value, bool):
69
- return BoolAttribute(value=value_list, **common_args)
69
+ if len(value_list) == 1:
70
+ return BoolAttribute(value=value_list[0], **common_args)
71
+ else:
72
+ return BoolListAttribute(value=value_list, **common_args)
73
+
70
74
 
71
75
  elif isinstance(first_value, datetime | date | time):
72
76
  for v in value_list:
73
77
  if not v.tzinfo:
74
78
  warnings.warn(f'No timezone given for {v}. Assuming it is in UTC.')
75
- return DateTimeAttribute(value=value_list, **common_args)
79
+ v.replace(tzinfo=UTC)
80
+ if len(value_list) == 1:
81
+ return DateTimeAttribute(value=value_list[0], **common_args)
82
+ else:
83
+ return DateTimeListAttribute(value=value_list, **common_args)
76
84
  # return DateTimeAttribute(value =_date_value_from_python_type(value).value, **common_args)
77
85
 
78
86
 
@@ -83,9 +91,13 @@ class pyAttributes(RootModel[list[pyAttribute]]):
83
91
  v = Quantity(value=v, unit='dimensionless')
84
92
  values.append(NumericValue(numerical_value=v.value_as_str(),
85
93
  unit = v.unit))
86
- num_attribute = NumericAttribute(value = values, **common_args)
87
- num_attribute.print_validation_messages()
88
- return num_attribute
94
+ if len(values) == 1:
95
+ num_attr = NumericAttribute(value=values[0], **common_args)
96
+ else:
97
+ num_attr = NumericListAttribute(value=values, **common_args)
98
+ num_attr.print_validation_messages()
99
+
100
+ return num_attr
89
101
 
90
102
  elif isinstance(first_value, str):
91
103
  # capture quantities in the form of "100.0e5 g/L"
@@ -94,20 +106,37 @@ class pyAttributes(RootModel[list[pyAttribute]]):
94
106
  for v in value_list:
95
107
  q = Quantity.from_str_with_unit(v)
96
108
  values.append( NumericValue(numerical_value=q.value_as_str(), unit = q.unit) )
97
- return NumericAttribute(value = values,
98
- **common_args)
99
-
109
+ if len(values) == 1:
110
+ return NumericAttribute(value=values[0], **common_args)
111
+ else:
112
+ return NumericListAttribute(value=values, **common_args)
113
+
100
114
  else:
101
- return TextAttribute(value = value_list, **common_args)
115
+ if len(value_list) == 1:
116
+ return TextAttribute(value=value_list[0], **common_args)
117
+ else:
118
+ return TextListAttribute(value=value_list, **common_args)
102
119
 
103
120
  elif isinstance(first_value, pyReference):
104
- return ReferenceAttribute(value = [v.root for v in value_list], **common_args)
121
+ values = [v.root for v in value_list]
122
+ if len(values) == 1:
123
+ return ReferenceAttribute(value=values[0], **common_args)
124
+ else:
125
+ return ReferenceListAttribute(value=values, **common_args)
105
126
 
106
127
  elif isinstance(first_value, pyResource):
107
- return ResourceAttribute(value = [v.root for v in value_list], **common_args)
128
+ values = [v.root for v in value_list]
129
+ if len(values) == 1:
130
+ return ResourceAttribute(value=values[0], **common_args)
131
+ else:
132
+ return ResourceListAttribute(value=values, **common_args)
108
133
 
109
134
  elif isinstance(first_value, PAC_ID):
110
- return ReferenceAttribute(value = [v.to_url(include_extensions=False) for v in value_list], **common_args)
135
+ values = [v.to_url(include_extensions=False) for v in value_list]
136
+ if len(values) == 1:
137
+ return ReferenceAttribute(value=values[0], **common_args)
138
+ else:
139
+ return ReferenceListAttribute(value=values, **common_args)
111
140
 
112
141
  else: #this covers the last resort case of arbitrary objects. Must be json serializable.
113
142
  try :
@@ -124,25 +153,25 @@ class pyAttributes(RootModel[list[pyAttribute]]):
124
153
  for a in attributes:
125
154
  value_list = a.value if isinstance(a.value, list) else [a.value]
126
155
  match a:
127
- case ReferenceAttribute():
156
+ case ReferenceAttribute() | ReferenceListAttribute():
128
157
  values = [pyReference(v) for v in value_list]
129
158
 
130
- case ResourceAttribute():
159
+ case ResourceAttribute() | ResourceListAttribute():
131
160
  values = [pyResource(v) for v in value_list]
132
161
 
133
- case NumericAttribute():
162
+ case NumericAttribute() | NumericListAttribute():
134
163
  values = [ Quantity.from_str_value(value=v.numerical_value, unit=v.unit) for v in value_list]
135
164
 
136
- case BoolAttribute():
165
+ case BoolAttribute() | BoolAttribute():
137
166
  values = value_list
138
167
 
139
- case TextAttribute():
168
+ case TextAttribute() | TextListAttribute():
140
169
  values = value_list
141
170
 
142
- case DateTimeAttribute():
171
+ case DateTimeAttribute() | DateTimeAttribute():
143
172
  values = value_list
144
173
 
145
- case ObjectAttribute():
174
+ case ObjectAttribute() | ObjectAttribute():
146
175
  values = value_list
147
176
 
148
177
 
@@ -55,7 +55,7 @@ class Category(LabFREED_BaseModel):
55
55
  k = f"{field_name} ({ field_info.alias})"
56
56
  else:
57
57
  k = f"{field_name}"
58
- out.update({k : v } )
58
+ out.update({k : v } )
59
59
 
60
60
  for s in getattr(self, 'additional_segments', []):
61
61
  out.update( {s.key or '' : s.value })
@@ -1,5 +1,5 @@
1
1
  ## Materials
2
- from abc import ABC
2
+ from abc import ABC, abstractproperty
3
3
  from pydantic import Field, computed_field, model_validator
4
4
 
5
5
  from labfreed.labfreed_infrastructure import ValidationMsgLevel
@@ -18,6 +18,10 @@ class PredefinedCategory(Category, ABC):
18
18
  def segments(self) -> list[IDSegment]:
19
19
  return self._get_segments(use_short_notation=False)
20
20
 
21
+ @abstractproperty
22
+ def is_serialized(self) -> bool:
23
+ pass
24
+
21
25
  def _get_segments(self, use_short_notation=False) -> list[IDSegment]:
22
26
  segments = []
23
27
  can_omit_keys = use_short_notation # keeps track of whether keys can still be omitted. That is the case when the segment recommendation is followed
@@ -65,12 +69,17 @@ class Material_Device(PredefinedCategory):
65
69
  if not self.serial_number:
66
70
  self._add_validation_message(
67
71
  source=f"Category {self.key}",
68
- level = ValidationMsgLevel.ERROR,
69
- msg=f'Category key {self.key} is missing mandatory field Serial Number',
72
+ level = ValidationMsgLevel.WARNING,
73
+ msg=f'Category key {self.key} is missing field Serial Number. Check that you are indeed to a product and not a specific device.',
70
74
  highlight_pattern = f"{self.key}"
71
75
  )
72
76
  return self
73
77
 
78
+ @property
79
+ def is_serialized(self) -> bool:
80
+ return bool(self.serial_number)
81
+
82
+
74
83
  class Material_Substance(PredefinedCategory):
75
84
  '''Represents the -MS category'''
76
85
  key: str = Field(default='-MS', frozen=True)
@@ -93,6 +102,11 @@ class Material_Substance(PredefinedCategory):
93
102
  )
94
103
  return self
95
104
 
105
+ @property
106
+ def is_serialized(self) -> bool:
107
+ return bool(self.batch_number or self.container_number or self.aliquot)
108
+
109
+
96
110
  class Material_Consumable(PredefinedCategory):
97
111
  '''Represents the -MC category'''
98
112
  key: str = Field(default='-MC', frozen=True)
@@ -115,6 +129,11 @@ class Material_Consumable(PredefinedCategory):
115
129
  )
116
130
  return self
117
131
 
132
+ @property
133
+ def is_serialized(self) -> bool:
134
+ return bool(self.batch_number or self.serial_number or self.aliquot)
135
+
136
+
118
137
  class Material_Misc(Material_Consumable):
119
138
  '''Represents the -MX category'''
120
139
  # same fields as Consumable
@@ -128,6 +147,8 @@ class Material_Misc(Material_Consumable):
128
147
  ''' Category segments, which are not defined in the specification'''
129
148
 
130
149
 
150
+
151
+
131
152
 
132
153
  ## Data
133
154
  class Data_Abstract(PredefinedCategory, ABC):
@@ -147,6 +168,11 @@ class Data_Abstract(PredefinedCategory, ABC):
147
168
  highlight_pattern = f"{self.key}"
148
169
  )
149
170
  return self
171
+
172
+ @property
173
+ def is_serialized(self) -> bool:
174
+ return True
175
+
150
176
 
151
177
  class Data_Result(Data_Abstract):
152
178
  '''Represents the -DR category'''
@@ -215,6 +241,11 @@ class Processor_Abstract(PredefinedCategory, ABC):
215
241
  highlight_pattern = f"{self.key}"
216
242
  )
217
243
  return self
244
+
245
+ @property
246
+ def is_serialized(self) -> bool:
247
+ return bool(self.processor_instance)
248
+
218
249
 
219
250
  class Processor_Software(Processor_Abstract):
220
251
  '''Represents the -PS category'''
@@ -252,6 +283,11 @@ class Misc(Category, ABC):
252
283
  )
253
284
  return self
254
285
 
286
+ @property
287
+ def is_serialized(self) -> bool:
288
+ return bool(self.id)
289
+
290
+
255
291
 
256
292
  category_key_to_class_map = {
257
293
  '-MD': Material_Device,
@@ -1,7 +1,7 @@
1
1
 
2
2
  from abc import ABC, abstractproperty
3
3
 
4
- from pydantic import model_validator
4
+ from pydantic import computed_field, model_validator
5
5
 
6
6
  from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel
7
7
 
@@ -29,6 +29,7 @@ class Extension(LabFREED_BaseModel,ExtensionBase):
29
29
  type:str|None
30
30
  data_:str
31
31
 
32
+ @computed_field
32
33
  @property
33
34
  def data(self) -> str:
34
35
  return self.data_
@@ -1,25 +1,27 @@
1
1
 
2
2
  import re
3
3
 
4
+ from deprecated import deprecated
5
+
4
6
  from pydantic import Field, model_validator
5
7
  from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMessage, ValidationMsgLevel
6
8
  from labfreed.pac_id.pac_id import PAC_ID
7
9
  from labfreed.pac_id_resolver.services import Service, ServiceGroup
8
- from labfreed.pac_id_resolver.cit_common import ( _add_msg_to_cit_entry_model,
10
+ from labfreed.pac_id_resolver.resolver_config_common import ( _add_msg_to_cit_entry_model,
9
11
  _validate_service_name,
10
12
  _validate_application_intent,
11
13
  _validate_service_type,
12
14
  ServiceType)
13
15
 
14
16
 
15
-
17
+ @deprecated("Use ResolverConfig")
16
18
  class CITEntry_v1(LabFREED_BaseModel):
17
19
  applicable_if: str = Field(..., min_length=1)
18
20
  service_name: str = Field(..., min_length=1)
19
21
  application_intent:str = Field(..., min_length=1)
20
22
  service_type:ServiceType|str
21
23
  template_url:str = Field(..., min_length=1)
22
-
24
+
23
25
 
24
26
  @model_validator(mode='after')
25
27
  def _validate_model(self):
@@ -67,7 +69,7 @@ class CITEntry_v1(LabFREED_BaseModel):
67
69
 
68
70
 
69
71
 
70
-
72
+ @deprecated("Use ResolverConfig")
71
73
  class CIT_v1(LabFREED_BaseModel):
72
74
  origin:str = ''
73
75
  entries:list[CITEntry_v1]
@@ -2,14 +2,13 @@ from functools import lru_cache
2
2
  import logging
3
3
  from typing import Self
4
4
  from requests import get
5
-
6
-
5
+ from deprecated import deprecated
7
6
 
8
7
  from labfreed.pac_cat.pac_cat import PAC_CAT
9
8
  from labfreed.pac_id.pac_id import PAC_ID
10
9
  from labfreed.pac_id_resolver.services import ServiceGroup
11
10
  from labfreed.pac_id_resolver.cit_v1 import CIT_v1
12
- from labfreed.pac_id_resolver.cit_v2 import CIT_v2
11
+ from labfreed.pac_id_resolver.resolver_config import ResolverConfig
13
12
 
14
13
 
15
14
 
@@ -22,49 +21,67 @@ def load_cit(path):
22
21
  return cit_from_str(s)
23
22
 
24
23
 
25
- def cit_from_str(s:str, origin:str='') -> CIT_v1|CIT_v2:
24
+ @deprecated("cit version 1 is deprecated. use resolvber config and load with ResolverConfig.from_yaml(s)")
25
+ def cit_from_str(s:str, origin:str='') -> CIT_v1|ResolverConfig:
26
26
  try:
27
- cit2 = CIT_v2.from_yaml(s)
27
+ cit2 = ResolverConfig.from_yaml(s)
28
28
  cit_version = 'v2'
29
- except Exception:
29
+ except Exception as e1:
30
30
  cit2 = None
31
31
  try:
32
32
  cit1 = CIT_v1.from_csv(s, origin)
33
33
  cit_version = 'v1' # noqa: F841
34
- except Exception:
34
+ except Exception as e2:
35
35
  cit1 = None
36
36
 
37
37
  cit = cit2 or cit1 or None
38
38
  return cit
39
39
 
40
40
  @lru_cache
41
- def _get_issuer_cit(issuer:str):
41
+ def _get_issuer_resolver_config(issuer:str):
42
42
  '''Gets the issuer's cit.'''
43
+ # V2
44
+ url = 'HTTPS://PAC.' + issuer + '/resolver_config.yaml'
45
+ try:
46
+ r = get(url, timeout=2)
47
+ if r.status_code < 400:
48
+ config_str = r.text
49
+ resolver_config = ResolverConfig.from_yaml(config_str)
50
+ return resolver_config
51
+ else:
52
+ logging.error(f"Could not get CIT V2 form {issuer}")
53
+ except Exception:
54
+ logging.error(f"Could not get CIT V2 form {issuer}")
55
+
56
+
57
+ # V1 (as fallback)
43
58
  url = 'HTTPS://PAC.' + issuer + '/coupling-information-table'
44
59
  try:
45
60
  r = get(url, timeout=2)
46
61
  if r.status_code < 400:
47
- cit_str = r.text
62
+ config_str = r.text
63
+ cit = CIT_v1.from_csv(config_str, '')
64
+ return cit
48
65
  else:
49
66
  logging.error(f"Could not get CIT form {issuer}")
50
- cit_str = None
51
67
  except Exception:
52
68
  logging.error(f"Could not get CIT form {issuer}")
53
- cit_str = None
54
- cit = cit_from_str(cit_str, origin=issuer)
55
- return cit
69
+
70
+ # no cit found
71
+ return None
72
+
56
73
 
57
74
 
58
75
 
59
76
  class PAC_ID_Resolver():
60
- def __init__(self, cits:list[CIT_v2|CIT_v1]=None) -> Self:
77
+ def __init__(self, resolver_configs:list[ResolverConfig|CIT_v1]=None) -> Self:
61
78
  '''Initialize the resolver with coupling information tables'''
62
- if not cits:
63
- cits = []
64
- self._cits = set(cits)
79
+ if not resolver_configs:
80
+ resolver_configs = []
81
+ self._resolver_configs = set(resolver_configs)
65
82
 
66
83
 
67
- def resolve(self, pac_id:PAC_ID|str, check_service_status=True, use_issuer_cit=True) -> list[ServiceGroup]:
84
+ def resolve(self, pac_id:PAC_ID|str, check_service_status=True, use_issuer_resolver_config=True) -> list[ServiceGroup]:
68
85
  '''Resolve a PAC-ID'''
69
86
  if isinstance(pac_id, str):
70
87
  pac_id_catless = PAC_ID.from_url(pac_id, try_pac_cat=False)
@@ -77,18 +94,21 @@ class PAC_ID_Resolver():
77
94
  raise ValueError('pac_id is invalid. Should be a PAC-ID in url form or a PAC-ID object')
78
95
 
79
96
 
80
- cits = self._cits.copy()
81
- if use_issuer_cit:
82
- if issuer_cit := _get_issuer_cit(pac_id.issuer):
83
- cits.add(issuer_cit)
97
+ resolver_configs = self._resolver_configs.copy()
98
+ if use_issuer_resolver_config:
99
+ if issuer_resolver_config := _get_issuer_resolver_config(pac_id.issuer):
100
+ resolver_configs.add(issuer_resolver_config)
84
101
 
85
102
  matches = []
86
- for cit in cits:
87
- if isinstance(cit, CIT_v1):
88
- # cit v1 has no concept of categories and implied keys. It would treat these segments as value segment
89
- matches.append(cit.evaluate_pac_id(pac_id_catless))
90
- else:
91
- matches.append(cit.evaluate_pac_id(pac_id))
103
+ for cit in resolver_configs:
104
+ try:
105
+ if isinstance(cit, CIT_v1):
106
+ # cit v1 has no concept of categories and implied keys. It would treat these segments as value segment
107
+ matches.append(cit.evaluate_pac_id(pac_id_catless))
108
+ else:
109
+ matches.append(cit.evaluate_pac_id(pac_id))
110
+ except Exception as e:
111
+ logging.error(f'Failed to resolve pac {pac_id.to_url()} with cit {cit.origin}')
92
112
 
93
113
  if check_service_status:
94
114
  for m in matches:
@@ -8,18 +8,18 @@ import jsonpath_ng.ext as jsonpath
8
8
 
9
9
  from labfreed.pac_id_resolver.services import Service, ServiceGroup
10
10
  from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
11
- from labfreed.pac_id_resolver.cit_common import ( ServiceType)
11
+ from labfreed.pac_id_resolver.resolver_config_common import ( ServiceType)
12
12
 
13
13
 
14
14
  __all__ = [
15
- "CIT_v2",
16
- "CITBlock_v2",
17
- "CITEntry_v2"
15
+ "ResolverConfig",
16
+ "ResolverConfigBlock",
17
+ "ResolverConfigEntry"
18
18
  ]
19
19
 
20
20
 
21
21
 
22
- class CITEntry_v2(LabFREED_BaseModel):
22
+ class ResolverConfigEntry(LabFREED_BaseModel):
23
23
  service_name: str
24
24
  application_intents:list[str]
25
25
  service_type:ServiceType |str
@@ -92,9 +92,9 @@ class CITEntry_v2(LabFREED_BaseModel):
92
92
 
93
93
 
94
94
 
95
- class CITBlock_v2(LabFREED_BaseModel):
95
+ class ResolverConfigBlock(LabFREED_BaseModel):
96
96
  applicable_if: str = Field(default='True', alias='if')
97
- entries: list[CITEntry_v2]
97
+ entries: list[ResolverConfigEntry]
98
98
 
99
99
  @field_validator('applicable_if', mode='before')
100
100
  @classmethod
@@ -104,21 +104,21 @@ class CITBlock_v2(LabFREED_BaseModel):
104
104
 
105
105
 
106
106
 
107
- class CIT_v2(LabFREED_BaseModel):
107
+ class ResolverConfig(LabFREED_BaseModel):
108
108
  schema_version: str = Field(default='2.0')
109
- '''Coupling Information Table (CIT)'''
109
+ '''Resolver Configuration'''
110
110
  origin: str = ''
111
111
  model_config = {
112
112
  "extra": "allow"
113
113
  }
114
114
  '''@private'''
115
- cit: list[CITBlock_v2] = Field(default_factory=list)
115
+ config: list[ResolverConfigBlock] = Field(default_factory=list)
116
116
 
117
117
  @model_validator(mode='after')
118
118
  def _validate_origin(self):
119
119
  if len(self.origin) == 0:
120
120
  self._add_validation_message(level=ValidationMsgLevel.WARNING,
121
- source='CIT origin',
121
+ source='ResolverConfig origin',
122
122
  msg='Origin should not be empty'
123
123
  )
124
124
  return self
@@ -137,37 +137,37 @@ class CIT_v2(LabFREED_BaseModel):
137
137
  yml = yaml.dump(self.model_dump() )
138
138
  return yml
139
139
 
140
- # hash and equal are only used to avoid adding the same cit multiple times.
140
+ # hash and equal are only used to avoid adding the same resolver config multiple times.
141
141
  # we can live with some instances, where it does not work
142
142
  def __hash__(self):
143
143
  return self.model_dump_json().__hash__()
144
144
 
145
145
  def __eq__(self, other):
146
- if not isinstance(other, CIT_v2):
146
+ if not isinstance(other, ResolverConfig):
147
147
  return False
148
148
  return self.model_dump() == other.model_dump()
149
149
 
150
150
 
151
151
  def evaluate_pac_id(self, pac):
152
152
  pac_id_json = pac.to_dict()
153
- cit_evaluated = ServiceGroup(origin=self.origin)
154
- for block in self.cit:
153
+ resolver_config_evaluated = ServiceGroup(origin=self.origin)
154
+ for block in self.config:
155
155
  _, is_applicable = self._evaluate_applicable_if(pac_id_json, block.applicable_if)
156
156
  if not is_applicable:
157
157
  continue
158
158
 
159
159
  for e in block.entries:
160
160
  if e.errors():
161
- continue #make this stable against errors in the cit
161
+ continue #make this stable against errors in the resolver config
162
162
  url = self._eval_url_template(pac_id_json, e.template_url)
163
- cit_evaluated.services.append(Service(
163
+ resolver_config_evaluated.services.append(Service(
164
164
  service_name=e.service_name,
165
165
  application_intents=e.application_intents,
166
166
  service_type=e.service_type,
167
167
  url = url
168
168
  )
169
169
  )
170
- return cit_evaluated
170
+ return resolver_config_evaluated
171
171
 
172
172
 
173
173
  def _evaluate_applicable_if(self, pac_id_json:str, expression) -> tuple[str, bool]:
@@ -1,7 +1,7 @@
1
- from .display_name_extension import DisplayNameExtension
1
+ from .text_base36_extension import TextBase36Extension
2
2
  from .trex_extension import TREX_Extension
3
3
 
4
4
  default_extension_interpreters = {
5
5
  'TREX': TREX_Extension,
6
- 'N': DisplayNameExtension
6
+ 'TEXT': TextBase36Extension
7
7
  }
@@ -1,27 +1,35 @@
1
1
  import logging
2
2
  from typing import Literal, Self
3
- from pydantic import computed_field
3
+ from pydantic import model_validator
4
4
  from labfreed.labfreed_infrastructure import LabFREED_BaseModel
5
5
  from labfreed.pac_id.extension import ExtensionBase
6
- from labfreed.utilities.base36 import from_base36, to_base36
6
+ from labfreed.well_known_extensions.text_base36_extension import TextBase36Extension
7
7
 
8
+ from labfreed.utilities.base36 import from_base36
8
9
 
9
- class DisplayNameExtension(ExtensionBase, LabFREED_BaseModel):
10
+
11
+ class DisplayNameExtension(TextBase36Extension, LabFREED_BaseModel):
10
12
  name:Literal['N'] = 'N'
11
13
  type:Literal['TEXT'] = 'TEXT'
12
- display_name: str
14
+ display_name: str
13
15
 
14
- @computed_field
15
- @property
16
- def data(self)->str:
17
- # return '/'.join([to_base36(dn) for dn in self.display_name])
18
- return to_base36(self.display_name).root
16
+ @model_validator(mode='before')
17
+ def move_display_name_to_text(cls, data):
18
+ # if display_name provided, move it to text
19
+ if isinstance(data, dict) and 'display_name' in data:
20
+ data['text'] = data.pop('display_name')
21
+ return data
19
22
 
20
23
  @staticmethod
21
24
  def from_extension(ext:ExtensionBase) -> Self:
22
25
  return DisplayNameExtension.create(name=ext.name,
23
26
  type=ext.type,
24
27
  data=ext.data)
28
+
29
+ @property
30
+ def display_name(self) -> str:
31
+ return self.text
32
+
25
33
 
26
34
  @staticmethod
27
35
  def create(*, name, type, data):
@@ -0,0 +1,38 @@
1
+ import logging
2
+ from typing import Literal, Self
3
+ from pydantic import computed_field
4
+ from labfreed.labfreed_infrastructure import LabFREED_BaseModel
5
+ from labfreed.pac_id.extension import ExtensionBase
6
+ from labfreed.utilities.base36 import from_base36, to_base36
7
+
8
+
9
+ class TextBase36Extension(ExtensionBase, LabFREED_BaseModel):
10
+ name:str
11
+ type:Literal['TEXT'] = 'TEXT'
12
+ text: str
13
+
14
+ @computed_field
15
+ @property
16
+ def data(self)->str:
17
+ # return '/'.join([to_base36(dn) for dn in self.display_name])
18
+ return to_base36(self.text).root
19
+
20
+ @staticmethod
21
+ def from_extension(ext:ExtensionBase) -> Self:
22
+ return TextBase36Extension.create(name=ext.name,
23
+ type=ext.type,
24
+ data=ext.data)
25
+
26
+ @staticmethod
27
+ def create(*, name, type, data):
28
+
29
+ if type != 'TEXT':
30
+ logging.warning(f'Type {name} was given, but this extension should only be used with type "TEXT". Will try to parse data as display names')
31
+
32
+ text = from_base36(data)
33
+
34
+ return TextBase36Extension(name=name, text=text)
35
+
36
+ def __str__(self):
37
+ return 'Text: '+ self.text
38
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labfreed
3
- Version: 1.0.0a13
3
+ Version: 1.0.0a15
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.11
@@ -25,6 +25,7 @@ Requires-Dist: jsonpath-ng>=1.7.0
25
25
  Requires-Dist: requests>=2.32.3
26
26
  Requires-Dist: requests_cache>=1.2.1
27
27
  Requires-Dist: cachetools>=6.1.0
28
+ Requires-Dist: deprecated>=1.2.18
28
29
  Requires-Dist: pytest>=8.3.5 ; extra == "dev"
29
30
  Requires-Dist: pdoc>=15.0.1 ; extra == "dev"
30
31
  Requires-Dist: flit>=3.12.0 ; extra == "dev"