labfreed 1.0.0a3__py3-none-any.whl → 1.0.0a8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- labfreed/__init__.py +1 -1
- labfreed/labfreed_extended/app/app_infrastructure.py +3 -3
- labfreed/labfreed_extended/app/pac_info/html_renderer/external-link.svg +7 -0
- labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html +188 -0
- labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css +176 -0
- labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info.jinja.html +46 -0
- labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html +7 -0
- labfreed/labfreed_extended/app/{pac_info.py → pac_info/pac_info.py} +52 -7
- labfreed/pac_attributes/api_data_models/response.py +54 -32
- labfreed/pac_attributes/client/attribute_cache.py +8 -21
- labfreed/pac_attributes/client/client.py +4 -2
- labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +1 -2
- labfreed/pac_attributes/pythonic/py_attributes.py +82 -50
- labfreed/pac_attributes/server/attribute_data_sources.py +7 -11
- labfreed/pac_attributes/server/server.py +11 -2
- labfreed/pac_cat/category_base.py +1 -1
- labfreed/pac_cat/predefined_categories.py +52 -4
- {labfreed-1.0.0a3.dist-info → labfreed-1.0.0a8.dist-info}/METADATA +1 -1
- {labfreed-1.0.0a3.dist-info → labfreed-1.0.0a8.dist-info}/RECORD +21 -16
- {labfreed-1.0.0a3.dist-info → labfreed-1.0.0a8.dist-info}/WHEEL +0 -0
- {labfreed-1.0.0a3.dist-info → labfreed-1.0.0a8.dist-info}/licenses/LICENSE +0 -0
labfreed/__init__.py
CHANGED
|
@@ -3,7 +3,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
3
3
|
|
|
4
4
|
import requests
|
|
5
5
|
|
|
6
|
-
from labfreed.labfreed_extended.app.pac_info import PacInfo
|
|
6
|
+
from labfreed.labfreed_extended.app.pac_info.pac_info import PacInfo
|
|
7
7
|
from labfreed.pac_attributes.client.attribute_cache import MemoryAttributeCache
|
|
8
8
|
from labfreed.pac_attributes.client.client import AttributeClient, http_attribute_request_default_callback_factory
|
|
9
9
|
from labfreed.pac_attributes.pythonic.py_attributes import pyAttributeGroup
|
|
@@ -32,7 +32,7 @@ class Labfreed_App_Infrastructure():
|
|
|
32
32
|
self._http_client= http_client
|
|
33
33
|
callback = http_attribute_request_default_callback_factory(http_client)
|
|
34
34
|
|
|
35
|
-
self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache())
|
|
35
|
+
self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache(), always_use_cached_value_for_minutes=1)
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def add_cit(self, cit:str):
|
|
@@ -71,7 +71,7 @@ class Labfreed_App_Infrastructure():
|
|
|
71
71
|
ags = {ag.key: pyAttributeGroup.from_attribute_group(ag) for ag in self._attribute_client.get_attributes(url, pac_id=pac.to_url(include_extensions=False), language_preferences=self._language_preferences)}
|
|
72
72
|
if ags:
|
|
73
73
|
attribute_groups.update(ags)
|
|
74
|
-
pac_info.
|
|
74
|
+
pac_info.attribute_groups = attribute_groups
|
|
75
75
|
|
|
76
76
|
return pac_info
|
|
77
77
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
3
|
+
<title>icons/external-link</title>
|
|
4
|
+
<g id="icons/external-link" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
5
|
+
<path d="M5.10570888,18.8942911 C4.83722343,18.6258057 4.81484965,18.204406 5.03858752,17.9104349 L5.10570888,17.8336309 L15.9992174,6.94117952 L10.8105793,6.93942074 L10.7088087,6.93257412 C10.3427331,6.8829117 10.0605793,6.5691165 10.0605793,6.18942074 C10.0605793,5.81286295 10.3380887,5.50112097 10.6997498,5.44755266 L10.8105793,5.43942074 L18.5605793,5.43942074 L18.5605793,13.1894207 L18.5537326,13.2911913 C18.5090365,13.6206593 18.2503927,13.8821507 17.9223127,13.9311544 L17.8105793,13.9394207 L17.7088087,13.9325741 C17.3793407,13.8878779 17.1178493,13.6292342 17.0688456,13.3011542 L17.0605793,13.1894207 L17.0598776,8.00183969 L6.16636906,18.8942911 C5.87347584,19.1871843 5.3986021,19.1871843 5.10570888,18.8942911 Z" id="Icons/Navigation/External-Link/Dark" fill="#1C1847" fill-rule="nonzero"></path>
|
|
6
|
+
</g>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
{# ---------- PAC-INFO MACROS ---------- #}
|
|
2
|
+
{# These macros render semantic HTML with light class hooks. #}
|
|
3
|
+
|
|
4
|
+
{%- macro info_card(pac_info) -%}
|
|
5
|
+
<section class="lf-info-card">
|
|
6
|
+
<div class="lf-info-title">{{ pac_info.display_name }}</div>
|
|
7
|
+
<div class="lf-info-grid">
|
|
8
|
+
{% if pac_info.image_url %}
|
|
9
|
+
<img src="{{ pac_info.image_url }}" class="prod_img lf-info-item" alt="">
|
|
10
|
+
{% endif %}
|
|
11
|
+
|
|
12
|
+
{% if pac_info.main_category %}
|
|
13
|
+
{% for c in pac_info.pac_id.categories %}
|
|
14
|
+
{% for k, v in c.segments_as_dict().items() %}
|
|
15
|
+
{% if k != "key" %}
|
|
16
|
+
<div class="lf-info-item">
|
|
17
|
+
{% if k %}
|
|
18
|
+
<div class="lf-info-key">{{ k }}</div>
|
|
19
|
+
{% else %}
|
|
20
|
+
<div class="lf-info-key"> <i>{{"No Key" }}</i></div>
|
|
21
|
+
{% endif %}
|
|
22
|
+
<div class="lf-info-val">{{ v }}</div>
|
|
23
|
+
</div>
|
|
24
|
+
{% endif %}
|
|
25
|
+
{% endfor %}
|
|
26
|
+
{% endfor %}
|
|
27
|
+
{% endif %}
|
|
28
|
+
|
|
29
|
+
</div>
|
|
30
|
+
{% if pac_info.safety_pictograms %}
|
|
31
|
+
{% for p in pac_info.safety_pictograms.values() %}
|
|
32
|
+
<figure class="lf-figure lf-figure--image">
|
|
33
|
+
<img src="{{ p.value|e }}" alt="{{ p.label|e }}" class="lf-attr-image">
|
|
34
|
+
<figcaption class="lf-figure__caption sr-only">{{ p.label }}</figcaption>
|
|
35
|
+
</figure>
|
|
36
|
+
{% endfor %}
|
|
37
|
+
{% endif %}
|
|
38
|
+
|
|
39
|
+
</section>
|
|
40
|
+
{%- endmacro -%}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
{%- macro key_value_table(pairs) -%}
|
|
44
|
+
{# pairs: dict-like object #}
|
|
45
|
+
<table class="lf-table lf-table--kv">
|
|
46
|
+
<tbody>
|
|
47
|
+
{%- for k, v in pairs.items() -%}
|
|
48
|
+
{%- if k != "key" -%}
|
|
49
|
+
<tr>
|
|
50
|
+
<th class="lf-th lf-th--key">{{ k }}</th>
|
|
51
|
+
<td class="lf-td lf-td--val">{{ v }}</td>
|
|
52
|
+
</tr>
|
|
53
|
+
{%- endif -%}
|
|
54
|
+
{%- endfor -%}
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
{%- endmacro -%}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
{%- macro category_block(category) -%}
|
|
61
|
+
<div class="lf-section lf-section--category">
|
|
62
|
+
<h5 class="lf-section__title">{{ category.__class__.__name__ }}</h5>
|
|
63
|
+
{{ key_value_table(category.segments_as_dict()) }}
|
|
64
|
+
</div>
|
|
65
|
+
{%- endmacro -%}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
{%- macro services_table(user_handover_group) -%}
|
|
69
|
+
<div class="lf-section lf-section--services">
|
|
70
|
+
<div class="lf-origin-hint">from {{ user_handover_group.origin }}</div>
|
|
71
|
+
<div class="lf-services">
|
|
72
|
+
{%- for s in user_handover_group.services -%}
|
|
73
|
+
<a href="{{ s.url }}" target="_blank" rel="noopener" class="lf-service">
|
|
74
|
+
<span class="lf-service__icon">
|
|
75
|
+
{% include "external-link.svg" %}
|
|
76
|
+
</span>
|
|
77
|
+
<span class="lf-service__name">{{ s.service_name }}</span>
|
|
78
|
+
</a>
|
|
79
|
+
{%- endfor -%}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
{%- endmacro -%}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
{%- macro reference_value(value) -%}
|
|
86
|
+
<span class="lf-reference">{{ value }}</span>
|
|
87
|
+
{%- endmacro -%}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
{%- macro attribute_row(a) -%}
|
|
91
|
+
<tr>
|
|
92
|
+
<th class="lf-th lf-th--key">{{ a.label }}</th>
|
|
93
|
+
<td class="lf-td lf-td--val">
|
|
94
|
+
{% set value = a.value %}
|
|
95
|
+
{%- if is_image(value) -%}
|
|
96
|
+
<figure class="lf-figure lf-figure--image">
|
|
97
|
+
<img src="{{ value|e }}" alt="{{ label|e }}" class="lf-attr-image">
|
|
98
|
+
<figcaption class="lf-figure__caption sr-only">{{ label }}</figcaption>
|
|
99
|
+
</figure>
|
|
100
|
+
{%- elif is_url(value) -%}
|
|
101
|
+
<a href="{{ value|e }}" target="_blank" rel="noopener" class="lf-link">{{ value }}</a>
|
|
102
|
+
{%- elif is_reference(value) -%}
|
|
103
|
+
{{ reference_value(value)}}
|
|
104
|
+
{%- else -%}
|
|
105
|
+
<span class="lf-text">{{ value }}</span>
|
|
106
|
+
{%- endif -%}
|
|
107
|
+
</td>
|
|
108
|
+
</tr>
|
|
109
|
+
{%- endmacro -%}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
{%- macro attribute_group_block(ag) -%}
|
|
113
|
+
<div class="lf-section lf-section--attributes">
|
|
114
|
+
<h5 class="lf-section__title">
|
|
115
|
+
{{ ag.label }} <span class="lf-origin-hint">(from {{ ag.origin }})</span>
|
|
116
|
+
</h5>
|
|
117
|
+
<table class="lf-table lf-table--kv">
|
|
118
|
+
<tbody>
|
|
119
|
+
{%- for a in ag.attributes.values() -%}
|
|
120
|
+
{{ attribute_row(a) }}
|
|
121
|
+
{%- endfor -%}
|
|
122
|
+
</tbody>
|
|
123
|
+
</table>
|
|
124
|
+
</div>
|
|
125
|
+
{%- endmacro -%}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
{%- macro data_table_inline(dt) -%}
|
|
129
|
+
{# Renders your DataTable structure; expects filters: is_data_table(dt) #}
|
|
130
|
+
{%- if dt.data | length == 1 -%}
|
|
131
|
+
<table class="lf-table lf-table--kv">
|
|
132
|
+
<tbody>
|
|
133
|
+
{%- for i in range(dt.col_names | length) -%}
|
|
134
|
+
<tr>
|
|
135
|
+
<th class="lf-th lf-th--key">{{ dt.col_names[i] }}</th>
|
|
136
|
+
<td class="lf-td lf-td--val">{{ dt.data[0][i] }}</td>
|
|
137
|
+
</tr>
|
|
138
|
+
{%- endfor -%}
|
|
139
|
+
</tbody>
|
|
140
|
+
</table>
|
|
141
|
+
{%- else -%}
|
|
142
|
+
<table class="lf-table lf-table--grid">
|
|
143
|
+
<thead>
|
|
144
|
+
<tr>
|
|
145
|
+
{%- for rn in dt.col_names -%}
|
|
146
|
+
<th class="lf-th">{{ rn }}</th>
|
|
147
|
+
{%- endfor -%}
|
|
148
|
+
</tr>
|
|
149
|
+
</thead>
|
|
150
|
+
<tbody>
|
|
151
|
+
{%- for row in dt.data -%}
|
|
152
|
+
<tr>
|
|
153
|
+
{%- for e in row -%}
|
|
154
|
+
<td class="lf-td">{{ e }}</td>
|
|
155
|
+
{%- endfor -%}
|
|
156
|
+
</tr>
|
|
157
|
+
{%- endfor -%}
|
|
158
|
+
</tbody>
|
|
159
|
+
</table>
|
|
160
|
+
{%- endif -%}
|
|
161
|
+
{%- endmacro -%}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
{%- macro attached_data_block(attached_data) -%}
|
|
165
|
+
<div class="lf-section lf-section--attached">
|
|
166
|
+
{%- for name, trex in attached_data.items() -%}
|
|
167
|
+
<div class="lf-subsection">
|
|
168
|
+
<h5 class="lf-subsection__title">{{ name }}</h5>
|
|
169
|
+
<table class="lf-table lf-table--kv">
|
|
170
|
+
<tbody>
|
|
171
|
+
{%- for k, v in trex.items() -%}
|
|
172
|
+
<tr>
|
|
173
|
+
<th class="lf-th lf-th--key">{{ k }}</th>
|
|
174
|
+
<td class="lf-td lf-td--val">
|
|
175
|
+
{%- if is_data_table(v) -%}
|
|
176
|
+
{{ data_table_inline(v) }}
|
|
177
|
+
{%- else -%}
|
|
178
|
+
{{ v }}
|
|
179
|
+
{%- endif -%}
|
|
180
|
+
</td>
|
|
181
|
+
</tr>
|
|
182
|
+
{%- endfor -%}
|
|
183
|
+
</tbody>
|
|
184
|
+
</table>
|
|
185
|
+
</div>
|
|
186
|
+
{%- endfor -%}
|
|
187
|
+
</div>
|
|
188
|
+
{%- endmacro -%}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
|
|
2
|
+
.lf-section {
|
|
3
|
+
margin-top: 0.5rem;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.lf-origin-hint {
|
|
7
|
+
font-style: italic; /* cursive */
|
|
8
|
+
font-size: 1rem; /* reset to regular text size */
|
|
9
|
+
font-weight: normal; /* not bold, even if inside a heading */
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
.lf-results {
|
|
14
|
+
display: grid;
|
|
15
|
+
gap: 1.5rem;
|
|
16
|
+
padding: 1rem; /* space around the whole grid */
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.lf-info-card {
|
|
20
|
+
width:100%;
|
|
21
|
+
border: solid 1px #ccc;
|
|
22
|
+
border-radius: 8px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.lf-info-card {
|
|
26
|
+
padding: 1rem;
|
|
27
|
+
border: 1px solid #e5e7eb;
|
|
28
|
+
border-radius: 8px;
|
|
29
|
+
background: #fff;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* title as "slightly bigger text" */
|
|
33
|
+
.lf-info-title {
|
|
34
|
+
font-size: 1.25rem; /* a bit larger than normal text */
|
|
35
|
+
font-weight: 600;
|
|
36
|
+
margin-bottom: 1rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* responsive grid for key/value */
|
|
40
|
+
.lf-info-grid {
|
|
41
|
+
display: grid;
|
|
42
|
+
gap: 1rem;
|
|
43
|
+
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.lf-info-item {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
gap: 0.25rem;
|
|
50
|
+
padding: 0.5rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.lf-info-key {
|
|
54
|
+
font-size: 0.875rem;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
color: #374151; /* slate-700 */
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.lf-info-val {
|
|
60
|
+
font-size: 1rem;
|
|
61
|
+
color: #111827; /* slate-900 */
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
.prod_img {
|
|
67
|
+
height: 10vw;
|
|
68
|
+
width: auto;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
/* Apply to all PAC macro tables */
|
|
73
|
+
.lf-table {
|
|
74
|
+
width: 100%;
|
|
75
|
+
border-collapse: collapse;
|
|
76
|
+
table-layout: fixed; /* ensures consistent column widths */
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.lf-table th,
|
|
80
|
+
.lf-table td {
|
|
81
|
+
border: 1px solid #ccc;
|
|
82
|
+
padding: 0.4rem 0.6rem;
|
|
83
|
+
vertical-align: top;
|
|
84
|
+
text-align: left;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Key/value style: 1/3 vs 2/3 columns */
|
|
88
|
+
.lf-table--kv th.lf-th--key {
|
|
89
|
+
width: 33%;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
}
|
|
92
|
+
.lf-table--kv td.lf-td--val {
|
|
93
|
+
width: 67%;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.lf-attr-image {
|
|
97
|
+
width: 30px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Services laid out as equal-width grid columns */
|
|
101
|
+
.lf-services {
|
|
102
|
+
display: grid;
|
|
103
|
+
gap: 0.75rem;
|
|
104
|
+
|
|
105
|
+
/* 2 columns on small, 3 on md, 4 on lg — tweak as you like */
|
|
106
|
+
grid-template-columns: repeat(2, 1fr);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@media (min-width: 640px) {
|
|
110
|
+
.lf-services { grid-template-columns: repeat(3, 1fr); }
|
|
111
|
+
}
|
|
112
|
+
@media (min-width: 1024px) {
|
|
113
|
+
.lf-services { grid-template-columns: repeat(4, 1fr); }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Each service card fills its grid cell */
|
|
117
|
+
.lf-service {
|
|
118
|
+
display: flex; /* still use flex inside for icon + text */
|
|
119
|
+
align-items: center;
|
|
120
|
+
gap: 0.4rem;
|
|
121
|
+
padding: 0.4rem 0.6rem;
|
|
122
|
+
height: 100%; /* same height across a row (since your content height is uniform) */
|
|
123
|
+
width: 100%; /* ensure full cell width */
|
|
124
|
+
text-decoration: none;
|
|
125
|
+
color: var(--lf-link, #2563eb);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.lf-service:hover {
|
|
129
|
+
font-size: 1.05em; /* ~5% larger */
|
|
130
|
+
text-decoration: none;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.lf-service__icon { width: 1rem; height: 1rem; flex-shrink: 0; }
|
|
134
|
+
.lf-service__name {
|
|
135
|
+
flex: 1;
|
|
136
|
+
overflow: hidden;
|
|
137
|
+
text-overflow: ellipsis;
|
|
138
|
+
white-space: nowrap;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
/* Position context */
|
|
144
|
+
.lf-ref-card-wrap {
|
|
145
|
+
position: relative;
|
|
146
|
+
display: inline-block;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* The floating card is hidden by default */
|
|
150
|
+
.lf-ref-card {
|
|
151
|
+
display: none;
|
|
152
|
+
position: absolute;
|
|
153
|
+
top: 100%; /* appear below the reference text */
|
|
154
|
+
left: 0;
|
|
155
|
+
z-index: 1000;
|
|
156
|
+
width: min(420px, 90vw);
|
|
157
|
+
margin-top: .5rem;
|
|
158
|
+
padding: 1rem;
|
|
159
|
+
background: #fff;
|
|
160
|
+
border: 1px solid #e5e7eb;
|
|
161
|
+
border-radius: 8px;
|
|
162
|
+
box-shadow: 0 10px 20px rgba(0,0,0,.08);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Show on hover */
|
|
166
|
+
.lf-ref-card-wrap:hover .lf-ref-card {
|
|
167
|
+
display: block;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* Style the inline reference */
|
|
171
|
+
.lf-reference {
|
|
172
|
+
color: var(--lf-link, #2563eb);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{% import "macros.jinja.html" as pac with context %}
|
|
2
|
+
|
|
3
|
+
<style>
|
|
4
|
+
{% include "pac-info-style.css" %}
|
|
5
|
+
</style>
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
{% if pac_info %}
|
|
9
|
+
<section class="lf-results">
|
|
10
|
+
{# ----- Info Card ---- #}
|
|
11
|
+
{{ pac.info_card(pac_info) }}
|
|
12
|
+
|
|
13
|
+
{# ---- Services ---- #}
|
|
14
|
+
{% if pac_info.user_handovers %}
|
|
15
|
+
<section>
|
|
16
|
+
<!--<h4>Services</h4>-->
|
|
17
|
+
{% for sg in pac_info.user_handovers %}
|
|
18
|
+
{{ pac.services_table(sg) }}
|
|
19
|
+
{% endfor %}
|
|
20
|
+
</section>
|
|
21
|
+
{% endif %}
|
|
22
|
+
|
|
23
|
+
{# ---- Attributes ---- #}
|
|
24
|
+
{% if pac_info.attributes %}
|
|
25
|
+
<section>
|
|
26
|
+
<!--<h4>Attributes</h4>-->
|
|
27
|
+
{% for ag in pac_info.attributes.values() %}
|
|
28
|
+
{% if ag.key not in hide_attribute_groups %}
|
|
29
|
+
{{ pac.attribute_group_block(ag) }}
|
|
30
|
+
{% endif %}
|
|
31
|
+
{% endfor %}
|
|
32
|
+
</section>
|
|
33
|
+
|
|
34
|
+
{% endif %}
|
|
35
|
+
|
|
36
|
+
{# ---- Attached Data ---- #}
|
|
37
|
+
{% if pac_info.attached_data %}
|
|
38
|
+
<section>
|
|
39
|
+
<!--<h4 class="lf-section__title">Attached Data</h4>-->
|
|
40
|
+
{{ pac.attached_data_block(pac_info.attached_data) }}
|
|
41
|
+
</section>
|
|
42
|
+
{% endif %}
|
|
43
|
+
|
|
44
|
+
</section>
|
|
45
|
+
|
|
46
|
+
{% endif %}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
3
|
from functools import cached_property
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
4
7
|
from pydantic import BaseModel, Field
|
|
5
|
-
from labfreed.pac_attributes.pythonic.py_attributes import pyAttribute, pyAttributeGroup, pyAttributes
|
|
8
|
+
from labfreed.pac_attributes.pythonic.py_attributes import pyAttribute, pyAttributeGroup, pyAttributes, pyReference, pyResource
|
|
6
9
|
from labfreed.pac_attributes.well_knonw_attribute_keys import MetaAttributeKeys
|
|
7
10
|
from labfreed.pac_cat.pac_cat import PAC_CAT
|
|
8
11
|
from labfreed.pac_id.pac_id import PAC_ID
|
|
9
12
|
from labfreed.pac_id_resolver.services import ServiceGroup
|
|
10
13
|
from labfreed.labfreed_extended.app.formatted_print import StringIOLineBreak
|
|
14
|
+
from labfreed.trex.pythonic.data_table import DataTable
|
|
11
15
|
from labfreed.trex.pythonic.pyTREX import pyTREX
|
|
12
16
|
from labfreed.well_known_extensions.display_name_extension import DisplayNameExtension
|
|
13
17
|
|
|
@@ -16,7 +20,7 @@ class PacInfo(BaseModel):
|
|
|
16
20
|
"""A convenient collection of information about a PAC-ID"""
|
|
17
21
|
pac_id:PAC_ID
|
|
18
22
|
user_handovers: list[ServiceGroup] = Field(default_factory=list)
|
|
19
|
-
|
|
23
|
+
attribute_groups:dict[str, pyAttributeGroup] = Field(default_factory=dict)
|
|
20
24
|
|
|
21
25
|
@property
|
|
22
26
|
def pac_url(self):
|
|
@@ -40,8 +44,10 @@ class PacInfo(BaseModel):
|
|
|
40
44
|
|
|
41
45
|
@property
|
|
42
46
|
def image_url(self) -> str:
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
image_attr = self._all_attributes.get(MetaAttributeKeys.IMAGE.value)
|
|
48
|
+
if isinstance(image_attr.value, pyResource):
|
|
49
|
+
return image_attr.value.root
|
|
50
|
+
if isinstance(image_attr.value, str):
|
|
45
51
|
return image_attr.value
|
|
46
52
|
|
|
47
53
|
|
|
@@ -64,12 +70,18 @@ class PacInfo(BaseModel):
|
|
|
64
70
|
def safety_pictograms(self) -> dict[str, pyAttribute]:
|
|
65
71
|
pictogram_attributes = {k: a for k, a in self._all_attributes.items() if "https://labfreed.org/ghs/pictogram/" in a.key}
|
|
66
72
|
return pictogram_attributes
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def qualification_state(self) -> pyAttribute:
|
|
77
|
+
if state := self._all_attributes.get("https://labfreed.org/qualification/status"):
|
|
78
|
+
return state
|
|
67
79
|
|
|
68
80
|
|
|
69
81
|
@cached_property
|
|
70
82
|
def _all_attributes(self) -> dict[str, pyAttribute]:
|
|
71
83
|
out = {}
|
|
72
|
-
for ag in self.
|
|
84
|
+
for ag in self.attribute_groups.values():
|
|
73
85
|
out.update(ag.attributes)
|
|
74
86
|
return out
|
|
75
87
|
|
|
@@ -101,17 +113,50 @@ class PacInfo(BaseModel):
|
|
|
101
113
|
|
|
102
114
|
|
|
103
115
|
printout.title1("Attributes")
|
|
104
|
-
for ag in self.
|
|
116
|
+
for ag in self.attribute_groups.values():
|
|
105
117
|
printout.title2(f'{ag.label} (from {ag.origin})')
|
|
106
118
|
for v in ag.attributes.values():
|
|
107
119
|
v:pyAttribute
|
|
108
120
|
#print(f'{k}: ({v.label}) :: {v.value} ')
|
|
109
|
-
printout.key_value(v.label, v.
|
|
121
|
+
printout.key_value(v.label, ', '.join([str(e) for e in v.value_list]))
|
|
110
122
|
|
|
111
123
|
out = printout.getvalue()
|
|
112
124
|
|
|
113
125
|
return out
|
|
114
126
|
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def render_html(self, hide_attribute_groups:list[str]=[]) -> str:
|
|
130
|
+
return PACInfo_HTMLRenderer.render_template('pac_info_main.jinja.html',
|
|
131
|
+
pac_info = self,
|
|
132
|
+
hide_attribute_groups=hide_attribute_groups
|
|
133
|
+
)
|
|
115
134
|
|
|
135
|
+
def render_html_card(self) -> str:
|
|
136
|
+
return PACInfo_HTMLRenderer.render_template('pac_info_card.jinja.html',
|
|
137
|
+
pac_info = self
|
|
138
|
+
)
|
|
139
|
+
|
|
116
140
|
|
|
141
|
+
class PACInfo_HTMLRenderer():
|
|
142
|
+
TEMPLATES_DIR = Path(__file__).parent / "html_renderer"
|
|
143
|
+
jinja_env = Environment(
|
|
144
|
+
loader=FileSystemLoader(str(TEMPLATES_DIR), encoding="utf-8"),
|
|
145
|
+
autoescape=select_autoescape(enabled_extensions=("html", "jinja", "jinja2", "jinja.html")),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def render_template(cls, template_name:str, pac_info:PacInfo, hide_attribute_groups):
|
|
150
|
+
# --- Jinja env pointing at /html_renderer ---
|
|
151
|
+
template = cls.jinja_env.get_template("pac_info.jinja.html")
|
|
152
|
+
html = template.render(
|
|
153
|
+
pac=pac_info.pac_id,
|
|
154
|
+
pac_info=pac_info, # your object
|
|
155
|
+
hide_attribute_groups=hide_attribute_groups,
|
|
156
|
+
is_data_table = lambda value: isinstance(value, DataTable),
|
|
157
|
+
is_url = lambda s: isinstance(s, str) and urlparse(s).scheme in ('http', 'https') and bool(urlparse(s).netloc),
|
|
158
|
+
is_image = lambda s: isinstance(s, str) and s.lower().startswith('http') and s.lower().endswith(('.jpg','.jpeg','.png','.gif','.bmp','.webp','.svg','.tif','.tiff')),
|
|
159
|
+
is_reference = lambda s: isinstance(s, pyReference) ,
|
|
160
|
+
)
|
|
161
|
+
return html
|
|
117
162
|
|
|
@@ -3,6 +3,8 @@ from abc import ABC
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
import re
|
|
5
5
|
from typing import Annotated, Any, Literal, Union, get_args
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
6
8
|
from labfreed.utilities.ensure_utc_time import ensure_utc
|
|
7
9
|
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
|
|
8
10
|
from pydantic import Field, field_validator, model_validator
|
|
@@ -12,22 +14,13 @@ class AttributeBase(LabFREED_BaseModel, ABC):
|
|
|
12
14
|
key: str
|
|
13
15
|
value: Any
|
|
14
16
|
label: str = ""
|
|
15
|
-
|
|
16
|
-
observed_at: datetime | None = None
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
def __init__(self, **data):
|
|
19
19
|
# Automatically inject the Literal value for `type`
|
|
20
20
|
discriminator_value = self._get_discriminator_value()
|
|
21
21
|
data["type"] = discriminator_value
|
|
22
22
|
super().__init__(**data)
|
|
23
23
|
|
|
24
|
-
@field_validator('observed_at', mode='before')
|
|
25
|
-
def set_utc_observed_at_if_naive(cls, value):
|
|
26
|
-
if isinstance(value, datetime):
|
|
27
|
-
return ensure_utc(value)
|
|
28
|
-
else:
|
|
29
|
-
return value
|
|
30
|
-
|
|
31
24
|
@classmethod
|
|
32
25
|
def _get_discriminator_value(cls) -> str:
|
|
33
26
|
"""Extract the Literal value from the 'type' annotation."""
|
|
@@ -40,16 +33,10 @@ class AttributeBase(LabFREED_BaseModel, ABC):
|
|
|
40
33
|
f"{cls.__name__} must define `type: Literal[<value>]` annotation"
|
|
41
34
|
) from e
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class ReferenceAttribute(AttributeBase):
|
|
47
|
-
type: Literal["reference"]
|
|
48
|
-
value: str
|
|
49
36
|
|
|
50
37
|
class DateTimeAttribute(AttributeBase):
|
|
51
38
|
type: Literal["datetime"]
|
|
52
|
-
value: datetime
|
|
39
|
+
value: datetime | list[datetime]
|
|
53
40
|
|
|
54
41
|
@field_validator('value', mode='before')
|
|
55
42
|
def set_utc__if_naive(cls, value):
|
|
@@ -60,15 +47,59 @@ class DateTimeAttribute(AttributeBase):
|
|
|
60
47
|
|
|
61
48
|
class BoolAttribute(AttributeBase):
|
|
62
49
|
type: Literal["bool"]
|
|
63
|
-
value: bool
|
|
50
|
+
value: bool | list[bool]
|
|
64
51
|
|
|
65
52
|
class TextAttribute(AttributeBase):
|
|
66
53
|
type: Literal["text"]
|
|
67
|
-
value: str
|
|
54
|
+
value: str | list[str]
|
|
55
|
+
|
|
56
|
+
@model_validator(mode='after')
|
|
57
|
+
def _validate_value(self):
|
|
58
|
+
l = [self.value] if isinstance(self.value, str) else self.value
|
|
59
|
+
for v in l:
|
|
60
|
+
if len(v) > 5000:
|
|
61
|
+
self._add_validation_message(
|
|
62
|
+
source="Text Attribute",
|
|
63
|
+
level=ValidationMsgLevel.WARNING, # noqa: F821
|
|
64
|
+
msg=f"Text attribute {v} exceeds 5000 characters. It is recommended to stay below",
|
|
65
|
+
highlight_pattern = f'{v}'
|
|
66
|
+
)
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ReferenceAttribute(AttributeBase):
|
|
71
|
+
type: Literal["reference"]
|
|
72
|
+
value: str | list[str]
|
|
68
73
|
|
|
74
|
+
|
|
75
|
+
class ResourceAttribute(AttributeBase):
|
|
76
|
+
type: Literal["resource"]
|
|
77
|
+
value: str | list[str]
|
|
69
78
|
|
|
79
|
+
@model_validator(mode='after')
|
|
80
|
+
def _validate_value(self):
|
|
81
|
+
value_list = self.value if isinstance(self.value, list) else [self.value]
|
|
82
|
+
for v in value_list:
|
|
83
|
+
r = urlparse(v)
|
|
84
|
+
if not all([r.scheme, r.netloc]):
|
|
85
|
+
self._add_validation_message(
|
|
86
|
+
source="Resource Attribute",
|
|
87
|
+
level=ValidationMsgLevel.ERROR, # noqa: F821
|
|
88
|
+
msg=f"Must be a valid url",
|
|
89
|
+
highlight_pattern = f'{v}'
|
|
90
|
+
)
|
|
91
|
+
pattern = re.compile(r"\.\w{1,3}$", re.IGNORECASE)
|
|
92
|
+
if not bool(pattern.search(v)):
|
|
93
|
+
self._add_validation_message(
|
|
94
|
+
source="Resource Attribute",
|
|
95
|
+
level=ValidationMsgLevel.WARNING, # noqa: F821
|
|
96
|
+
msg=f"It is RECOMMENDED resource links end with a file extension",
|
|
97
|
+
highlight_pattern = f'{v}'
|
|
98
|
+
)
|
|
99
|
+
return self
|
|
70
100
|
|
|
71
101
|
|
|
102
|
+
|
|
72
103
|
class NumericValue(LabFREED_BaseModel):
|
|
73
104
|
numerical_value: str
|
|
74
105
|
unit: str
|
|
@@ -120,11 +151,11 @@ class NumericValue(LabFREED_BaseModel):
|
|
|
120
151
|
|
|
121
152
|
class NumericAttribute(AttributeBase):
|
|
122
153
|
type: Literal["numeric"]
|
|
123
|
-
value: NumericValue
|
|
154
|
+
value: NumericValue | list[NumericValue]
|
|
124
155
|
|
|
125
156
|
class ObjectAttribute(AttributeBase):
|
|
126
157
|
type: Literal["object"]
|
|
127
|
-
value: dict[str, Any]
|
|
158
|
+
value: dict[str, Any] |list[dict[str, Any]]
|
|
128
159
|
|
|
129
160
|
|
|
130
161
|
|
|
@@ -136,27 +167,18 @@ Attribute = Annotated[
|
|
|
136
167
|
BoolAttribute,
|
|
137
168
|
TextAttribute,
|
|
138
169
|
NumericAttribute,
|
|
170
|
+
ResourceAttribute,
|
|
139
171
|
ObjectAttribute
|
|
140
172
|
],
|
|
141
173
|
Field(discriminator="type")
|
|
142
174
|
]
|
|
143
175
|
|
|
144
|
-
VALID_FOREVER = "forever"
|
|
145
176
|
|
|
146
177
|
class AttributeGroup(LabFREED_BaseModel):
|
|
147
178
|
key: str
|
|
148
179
|
label: str = ""
|
|
149
180
|
attributes: list[Attribute]
|
|
150
|
-
|
|
151
|
-
state_of: datetime
|
|
152
|
-
valid_until: datetime | Literal["forever"] | None = None
|
|
153
|
-
|
|
154
|
-
@field_validator('valid_until', mode='before')
|
|
155
|
-
def set_utc_valid_until_if_naive(cls, value):
|
|
156
|
-
if isinstance(value, datetime):
|
|
157
|
-
return ensure_utc(value)
|
|
158
|
-
else:
|
|
159
|
-
return value
|
|
181
|
+
|
|
160
182
|
|
|
161
183
|
|
|
162
184
|
class AttributesOfPACID(LabFREED_BaseModel):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
from datetime import datetime
|
|
3
|
+
from datetime import UTC, datetime, timedelta
|
|
4
4
|
from typing import Literal, Protocol
|
|
5
5
|
|
|
6
6
|
|
|
@@ -12,28 +12,15 @@ from labfreed.pac_id.pac_id import PAC_ID
|
|
|
12
12
|
class CacheableAttributeGroup(AttributeGroup):
|
|
13
13
|
origin:str
|
|
14
14
|
language:str
|
|
15
|
-
|
|
15
|
+
value_from: datetime | None = None
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# vals = [a.valid_until for a in self.attributes]
|
|
20
|
-
# if all(e == 'forever' for e in vals):
|
|
21
|
-
# self.valid_until = 'forever'
|
|
22
|
-
# elif any(e is None for e in vals):
|
|
23
|
-
# self.valid_until = None
|
|
24
|
-
# else:
|
|
25
|
-
# self.valid_until = min(v for v in vals if isinstance(v, datetime))
|
|
26
|
-
# return self
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@property
|
|
30
|
-
def still_valid(self):
|
|
31
|
-
if self.valid_until is None:
|
|
17
|
+
def still_valid(self, accept_cache_for_minutes):
|
|
18
|
+
if self.value_from is None:
|
|
32
19
|
return False
|
|
33
|
-
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
else:
|
|
21
|
+
return ( datetime.now(tz=UTC) - timedelta(minutes=accept_cache_for_minutes)) > self.value_from
|
|
22
|
+
|
|
23
|
+
|
|
37
24
|
|
|
38
25
|
|
|
39
26
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from datetime import UTC, datetime
|
|
4
5
|
from typing import Protocol, runtime_checkable
|
|
5
6
|
|
|
6
7
|
import requests
|
|
@@ -85,6 +86,7 @@ class AttributeClient():
|
|
|
85
86
|
|
|
86
87
|
http_post_callback:AttributeRequestCallback
|
|
87
88
|
cache_store:AttributeCache
|
|
89
|
+
always_use_cached_value_for_minutes:int
|
|
88
90
|
|
|
89
91
|
def get_attributes(self,
|
|
90
92
|
server_url:str,
|
|
@@ -120,7 +122,7 @@ class AttributeClient():
|
|
|
120
122
|
else:
|
|
121
123
|
attribute_groups = self.cache_store.get_all(server_url, pac_id)
|
|
122
124
|
|
|
123
|
-
if attribute_groups and all([ag.still_valid for ag in attribute_groups]):
|
|
125
|
+
if attribute_groups and all([ag.still_valid(accept_cache_for_minutes=self.always_use_cached_value_for_minutes) for ag in attribute_groups]):
|
|
124
126
|
return attribute_groups
|
|
125
127
|
|
|
126
128
|
# no valid data found in cache > request to server
|
|
@@ -154,7 +156,7 @@ class AttributeClient():
|
|
|
154
156
|
origin=server_url,
|
|
155
157
|
language=r.language,
|
|
156
158
|
label=ag.label,
|
|
157
|
-
|
|
159
|
+
value_from=datetime.now(tz=UTC))
|
|
158
160
|
for ag in ag_for_pac.attribute_groups
|
|
159
161
|
]
|
|
160
162
|
self.cache_store.update(server_url, pac_from_response, ags)
|
|
@@ -113,8 +113,7 @@ class _BaseExcelAttributeDataSource(AttributeGroupDataSource):
|
|
|
113
113
|
attributes = [pyAttribute(key=k, value=v) for k, v in d.items()]
|
|
114
114
|
return AttributeGroup(
|
|
115
115
|
key=self._attribute_group_key,
|
|
116
|
-
attributes=pyAttributes(attributes).to_payload_attributes()
|
|
117
|
-
state_of=last_changed,
|
|
116
|
+
attributes=pyAttributes(attributes).to_payload_attributes()
|
|
118
117
|
)
|
|
119
118
|
|
|
120
119
|
|
|
@@ -3,34 +3,58 @@ from datetime import date, datetime, time
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Literal
|
|
5
5
|
import warnings
|
|
6
|
-
from pydantic import RootModel
|
|
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, TextAttribute
|
|
9
|
+
from labfreed.pac_attributes.api_data_models.response import AttributeBase, AttributeGroup, BoolAttribute, DateTimeAttribute, NumericAttribute, NumericValue, ObjectAttribute, ReferenceAttribute, ResourceAttribute, TextAttribute
|
|
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
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class pyReference(RootModel[str]):
|
|
16
|
-
pass
|
|
17
16
|
|
|
18
17
|
def __str__(self):
|
|
19
18
|
return str(self.root)
|
|
19
|
+
|
|
20
|
+
class pyResource(RootModel[str]):
|
|
21
|
+
|
|
22
|
+
def __str__(self):
|
|
23
|
+
return str(self.root)
|
|
24
|
+
|
|
20
25
|
|
|
26
|
+
# the allowed scalar types
|
|
27
|
+
AllowedValue = str | bool | datetime | pyReference | pyResource | Quantity | int | float | dict | object
|
|
28
|
+
# homogeneous list of those
|
|
29
|
+
AllowedList = list[AllowedValue]
|
|
21
30
|
|
|
22
31
|
class pyAttribute(LabFREED_BaseModel):
|
|
23
32
|
key:str
|
|
24
33
|
label:str = ""
|
|
25
|
-
value:
|
|
26
|
-
valid_until: datetime | Literal["forever"] | None = None
|
|
27
|
-
observed_at: datetime | None = None
|
|
34
|
+
value: AllowedValue | AllowedList
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
@property
|
|
37
|
+
def value_list(self):
|
|
38
|
+
'''helper function to more conveniently iterate over value elements, even if it's scalar'''
|
|
39
|
+
return self.value if isinstance(self.value, list) else [self.value]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@field_validator('value', mode='before')
|
|
43
|
+
def handle_one_element_list(v):
|
|
44
|
+
if isinstance(v, list) and len(v)==1:
|
|
45
|
+
return v[0]
|
|
46
|
+
else:
|
|
47
|
+
return v
|
|
30
48
|
|
|
31
49
|
class pyAttributes(RootModel[list[pyAttribute]]):
|
|
32
50
|
def to_payload_attributes(self) -> list[AttributeBase]:
|
|
33
|
-
|
|
51
|
+
out = []
|
|
52
|
+
for e in self.root:
|
|
53
|
+
apt = self._attribute_to_attribute_payload_type(e)
|
|
54
|
+
if isinstance(apt.value, list) and len(apt.value) ==1:
|
|
55
|
+
apt.value = apt.value[0]
|
|
56
|
+
out.append(apt)
|
|
57
|
+
return out
|
|
34
58
|
|
|
35
59
|
|
|
36
60
|
@staticmethod
|
|
@@ -38,85 +62,93 @@ class pyAttributes(RootModel[list[pyAttribute]]):
|
|
|
38
62
|
common_args = {
|
|
39
63
|
"key": attribute.key,
|
|
40
64
|
"label": attribute.label,
|
|
41
|
-
"observed_at": attribute.observed_at
|
|
42
65
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if isinstance(
|
|
46
|
-
return
|
|
66
|
+
value_list = attribute.value_list
|
|
67
|
+
first_value = value_list[0]
|
|
68
|
+
if isinstance(first_value, bool):
|
|
69
|
+
return BoolAttribute(value=value_list, **common_args)
|
|
47
70
|
|
|
48
|
-
elif isinstance(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
71
|
+
elif isinstance(first_value, datetime | date | time):
|
|
72
|
+
for v in value_list:
|
|
73
|
+
if not v.tzinfo:
|
|
74
|
+
warnings.warn(f'No timezone given for {v}. Assuming it is in UTC.')
|
|
75
|
+
return DateTimeAttribute(value=value_list, **common_args)
|
|
52
76
|
# return DateTimeAttribute(value =_date_value_from_python_type(value).value, **common_args)
|
|
53
77
|
|
|
54
|
-
|
|
55
|
-
elif isinstance(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
|
|
79
|
+
elif isinstance(first_value, Quantity|int|float):
|
|
80
|
+
values = []
|
|
81
|
+
for v in value_list:
|
|
82
|
+
if not isinstance(v, Quantity):
|
|
83
|
+
v = Quantity(value=v, unit='dimensionless')
|
|
84
|
+
values.append(NumericValue(numerical_value=v.value_as_str(),
|
|
85
|
+
unit = v.unit))
|
|
86
|
+
num_attribute = NumericAttribute(value = values, **common_args)
|
|
61
87
|
num_attribute.print_validation_messages()
|
|
62
88
|
return num_attribute
|
|
63
89
|
|
|
64
|
-
elif isinstance(
|
|
90
|
+
elif isinstance(first_value, str):
|
|
65
91
|
# capture quantities in the form of "100.0e5 g/L"
|
|
66
|
-
if
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
92
|
+
if Quantity.from_str_with_unit(first_value):
|
|
93
|
+
values = []
|
|
94
|
+
for v in value_list:
|
|
95
|
+
q = Quantity.from_str_with_unit(v)
|
|
96
|
+
values.append( NumericValue(numerical_value=q.value_as_str(), unit = q.unit) )
|
|
97
|
+
return NumericAttribute(value = values,
|
|
98
|
+
**common_args)
|
|
99
|
+
|
|
70
100
|
else:
|
|
71
|
-
return TextAttribute(value =
|
|
101
|
+
return TextAttribute(value = value_list, **common_args)
|
|
72
102
|
|
|
73
|
-
elif isinstance(
|
|
74
|
-
return ReferenceAttribute(value =
|
|
75
|
-
|
|
76
|
-
elif isinstance(value, PAC_ID):
|
|
77
|
-
return ReferenceAttribute(value = value.to_url(include_extensions=False), **common_args)
|
|
78
|
-
|
|
103
|
+
elif isinstance(first_value, pyReference):
|
|
104
|
+
return ReferenceAttribute(value = [v.root for v in value_list], **common_args)
|
|
79
105
|
|
|
106
|
+
elif isinstance(first_value, pyResource):
|
|
107
|
+
return ResourceAttribute(value = [v.root for v in value_list], **common_args)
|
|
108
|
+
|
|
109
|
+
elif isinstance(first_value, PAC_ID):
|
|
110
|
+
return ReferenceAttribute(value = [v.to_url(include_extensions=False) for v in value_list], **common_args)
|
|
80
111
|
|
|
81
112
|
else: #this covers the last resort case of arbitrary objects. Must be json serializable.
|
|
82
113
|
try :
|
|
83
|
-
|
|
84
|
-
return ObjectAttribute(value=
|
|
114
|
+
values = [json.loads(json.dumps(v)) for v in value_list]
|
|
115
|
+
return ObjectAttribute(value=values, **common_args)
|
|
85
116
|
except TypeError as e: # noqa: F841
|
|
86
|
-
raise ValueError(f'Invalid Type: {type(
|
|
117
|
+
raise ValueError(f'Invalid Type: {type(first_value)} cannot be converted to attribute. You may want to use ObjectAttribute, but would have to implement the conversion from your python type yourself.')
|
|
118
|
+
|
|
87
119
|
|
|
88
120
|
|
|
89
121
|
@staticmethod
|
|
90
122
|
def from_payload_attributes(attributes:list[AttributeBase]) -> 'pyAttributes':
|
|
91
123
|
out = list()
|
|
92
124
|
for a in attributes:
|
|
125
|
+
value_list = a.value if isinstance(a.value, list) else [a.value]
|
|
93
126
|
match a:
|
|
94
|
-
|
|
95
127
|
case ReferenceAttribute():
|
|
96
|
-
|
|
128
|
+
values = [pyReference(v) for v in value_list]
|
|
129
|
+
|
|
130
|
+
case ResourceAttribute():
|
|
131
|
+
values = [pyResource(v) for v in value_list]
|
|
97
132
|
|
|
98
133
|
case NumericAttribute():
|
|
99
|
-
|
|
134
|
+
values = [ Quantity.from_str_value(value=v.numerical_value, unit=v.unit) for v in value_list]
|
|
100
135
|
|
|
101
136
|
case BoolAttribute():
|
|
102
|
-
|
|
137
|
+
values = value_list
|
|
103
138
|
|
|
104
139
|
case TextAttribute():
|
|
105
|
-
|
|
140
|
+
values = value_list
|
|
106
141
|
|
|
107
142
|
case DateTimeAttribute():
|
|
108
|
-
|
|
143
|
+
values = value_list
|
|
109
144
|
|
|
110
145
|
case ObjectAttribute():
|
|
111
|
-
|
|
146
|
+
values = value_list
|
|
112
147
|
|
|
113
148
|
|
|
114
149
|
attr = pyAttribute(key=a.key,
|
|
115
150
|
label=a.label,
|
|
116
|
-
value=
|
|
117
|
-
observed_at=a.observed_at
|
|
118
|
-
# valid_until=datetime(**_parse_date_time_str(a.valid_until)),
|
|
119
|
-
# observed_at=datetime(**_parse_date_time_str(a.value))
|
|
151
|
+
value=values
|
|
120
152
|
)
|
|
121
153
|
out.append(attr )
|
|
122
154
|
return out
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod, abstractproperty
|
|
2
2
|
from datetime import datetime, timezone
|
|
3
|
-
from labfreed.pac_attributes.api_data_models.response import
|
|
3
|
+
from labfreed.pac_attributes.api_data_models.response import AttributeBase, AttributeGroup
|
|
4
4
|
from labfreed.pac_cat.pac_cat import PAC_CAT
|
|
5
5
|
from labfreed.pac_id.pac_id import PAC_ID
|
|
6
6
|
|
|
@@ -31,13 +31,13 @@ class AttributeGroupDataSource(ABC):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class Dict_DataSource(AttributeGroupDataSource):
|
|
34
|
-
def __init__(self, data:dict[str, list[AttributeBase]], uses_pac_cat_short_form=True, *args, **kwargs):
|
|
34
|
+
def __init__(self, data:dict[str, list[AttributeBase]], uses_pac_cat_short_form=True, pac_to_key: callable = None, *args, **kwargs):
|
|
35
35
|
if not all([isinstance(e, list) for e in data.values()]):
|
|
36
36
|
raise ValueError('Invalid data')
|
|
37
37
|
|
|
38
38
|
self._data = data
|
|
39
|
-
self._state_of = datetime.now(tz=timezone.utc)
|
|
40
39
|
self.uses_pac_cat_short_form = uses_pac_cat_short_form
|
|
40
|
+
self._pac_to_key = pac_to_key
|
|
41
41
|
|
|
42
42
|
super().__init__(*args, **kwargs)
|
|
43
43
|
|
|
@@ -54,18 +54,14 @@ class Dict_DataSource(AttributeGroupDataSource):
|
|
|
54
54
|
except:
|
|
55
55
|
... # might as well try to match the original input
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
|
|
58
|
+
lookup_key = self._pac_to_key(pac_url) if self._pac_to_key else pac_url
|
|
59
|
+
attributes = self._data.get(lookup_key)
|
|
58
60
|
if not attributes:
|
|
59
61
|
return None
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
valid_until = VALID_FOREVER if self._is_static else None
|
|
63
|
-
|
|
64
|
-
|
|
65
63
|
return AttributeGroup(key=self._attribute_group_key,
|
|
66
|
-
attributes=attributes
|
|
67
|
-
state_of=self._state_of,
|
|
68
|
-
valid_until=valid_until)
|
|
64
|
+
attributes=attributes)
|
|
69
65
|
|
|
70
66
|
|
|
71
67
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import string
|
|
1
3
|
import traceback
|
|
2
4
|
import warnings
|
|
3
5
|
|
|
@@ -132,15 +134,22 @@ class AttributeServerRequestHandler():
|
|
|
132
134
|
if dn := self._get_display_name_for_key(ag.key, language):
|
|
133
135
|
ag.label = dn
|
|
134
136
|
else:
|
|
135
|
-
ag.label = ag.key
|
|
137
|
+
ag.label = self.fallback_label(ag.key)
|
|
136
138
|
rich.print(f"[yellow]WARNING:[/yellow] No translation for '{ag.key}' in '{language}'. Falling back to '{ag.label}'")
|
|
137
139
|
for a in ag.attributes:
|
|
138
140
|
if dn := self._get_display_name_for_key(a.key, language):
|
|
139
141
|
a.label = dn
|
|
140
142
|
else:
|
|
141
|
-
a.label = a.key
|
|
143
|
+
a.label = self.fallback_label(a.key)
|
|
142
144
|
rich.print(f"[yellow]WARNING:[/yellow] No translation for '{a.key}' in '{language}'. Falling back to '{a.label}' ")
|
|
143
145
|
|
|
146
|
+
|
|
147
|
+
def fallback_label(self, key:str):
|
|
148
|
+
l = key.split('/')[-1]
|
|
149
|
+
l = re.sub(r'([a-z])([A-Z])', r'\1 \2', l)
|
|
150
|
+
l = re.sub(r'[-_]', ' ', l)
|
|
151
|
+
l = string.capwords(l)
|
|
152
|
+
return l
|
|
144
153
|
|
|
145
154
|
|
|
146
155
|
def _get_display_name_for_key(self, key, language:str):
|
|
@@ -116,9 +116,9 @@ class Material_Consumable(PredefinedCategory):
|
|
|
116
116
|
return self
|
|
117
117
|
|
|
118
118
|
class Material_Misc(Material_Consumable):
|
|
119
|
-
'''Represents the -
|
|
119
|
+
'''Represents the -MX category'''
|
|
120
120
|
# same fields as Consumable
|
|
121
|
-
key: str = Field(default='-
|
|
121
|
+
key: str = Field(default='-MX', frozen=True)
|
|
122
122
|
product_number:str|None = Field( alias='240')
|
|
123
123
|
batch_number:str|None = Field(default=None, alias='10')
|
|
124
124
|
packaging_size:str|None = Field(default=None, alias='20')
|
|
@@ -187,15 +187,63 @@ class Data_Static(Data_Abstract):
|
|
|
187
187
|
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
188
188
|
''' Category segments, which are not defined in the specification'''
|
|
189
189
|
|
|
190
|
+
class Data_Misc(Data_Abstract):
|
|
191
|
+
'''Represents the -DX category'''
|
|
192
|
+
key: str = Field(default='-DX', frozen=True)
|
|
193
|
+
id:str|None = Field( alias='21')
|
|
194
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
195
|
+
''' Category segments, which are not defined in the specification'''
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Processor_Abstract(PredefinedCategory, ABC):
|
|
201
|
+
'''@private'''
|
|
202
|
+
key: str
|
|
203
|
+
processor_instance:str|None = Field( alias='21')
|
|
204
|
+
processor_code:str|None = Field( alias='240')
|
|
205
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
206
|
+
''' Category segments, which are not defined in the specification'''
|
|
207
|
+
|
|
208
|
+
@model_validator(mode='after')
|
|
209
|
+
def _validate_mandatory_fields(self):
|
|
210
|
+
if not self.id:
|
|
211
|
+
self._add_validation_message(
|
|
212
|
+
source=f"Category {self.key}",
|
|
213
|
+
level = ValidationMsgLevel.ERROR,
|
|
214
|
+
msg=f"Category key {self.key} is missing mandatory field 'processor instance'",
|
|
215
|
+
highlight_pattern = f"{self.key}"
|
|
216
|
+
)
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
class Processor_Software(Processor_Abstract):
|
|
220
|
+
'''Represents the -PS category'''
|
|
221
|
+
key: str = Field(default='-PS', frozen=True)
|
|
222
|
+
processor_instance:str|None = Field( alias='21')
|
|
223
|
+
processor_code:str|None = Field( alias='240')
|
|
224
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
225
|
+
''' Category segments, which are not defined in the specification'''
|
|
226
|
+
|
|
227
|
+
class Processor_Misc(Processor_Abstract):
|
|
228
|
+
'''Represents the -PX category'''
|
|
229
|
+
key: str = Field(default='-PX', frozen=True)
|
|
230
|
+
processor_instance:str|None = Field( alias='21')
|
|
231
|
+
processor_code:str|None = Field( alias='240')
|
|
232
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
233
|
+
''' Category segments, which are not defined in the specification'''
|
|
234
|
+
|
|
190
235
|
|
|
191
236
|
category_key_to_class_map = {
|
|
192
237
|
'-MD': Material_Device,
|
|
193
238
|
'-MS': Material_Substance,
|
|
194
239
|
'-MC': Material_Consumable,
|
|
195
|
-
'-
|
|
240
|
+
'-MX': Material_Misc,
|
|
196
241
|
'-DM': Data_Method,
|
|
197
242
|
'-DR': Data_Result,
|
|
198
243
|
'-DC': Data_Calibration,
|
|
199
244
|
'-DP': Data_Progress,
|
|
200
|
-
'-DS': Data_Static
|
|
245
|
+
'-DS': Data_Static,
|
|
246
|
+
'-DX': Data_Misc,
|
|
247
|
+
'-PS': Processor_Software,
|
|
248
|
+
'-PX': Processor_Misc
|
|
201
249
|
}
|
|
@@ -1,28 +1,33 @@
|
|
|
1
|
-
labfreed/__init__.py,sha256=
|
|
1
|
+
labfreed/__init__.py,sha256=o4WCCcQiGKDepTJYMe5O_-YuSrMEGJLhI1Dq2UmJzFw,338
|
|
2
2
|
labfreed/labfreed_infrastructure.py,sha256=YZmU-kgopyB1tvpTR_k_uIt1Q2ezexMrWvu-HaP65IE,10104
|
|
3
|
-
labfreed/labfreed_extended/app/app_infrastructure.py,sha256=
|
|
3
|
+
labfreed/labfreed_extended/app/app_infrastructure.py,sha256=F5UHHt8-r7jigQudOsJ-yV4lWKHFsxNhI4uGP_h53lE,4035
|
|
4
4
|
labfreed/labfreed_extended/app/formatted_print.py,sha256=DcwWP0ix1e_wYNIdceIp6cETkJdG2DqpU8Gs3aZAL40,1930
|
|
5
|
-
labfreed/labfreed_extended/app/pac_info.py,sha256=
|
|
5
|
+
labfreed/labfreed_extended/app/pac_info/pac_info.py,sha256=hsJunO4adKcLi7PBZNAJYeqzOH31_j95JzuvEV8lNVY,6427
|
|
6
|
+
labfreed/labfreed_extended/app/pac_info/html_renderer/external-link.svg,sha256=H5z9s4VvHq09UnHdqfrYNsx-Whljc0gE4qKJ6-3kfgQ,1158
|
|
7
|
+
labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html,sha256=1S-dxibPwJshtdelsmyA4LpgOm84L6RTXPNO93gmPfg,5964
|
|
8
|
+
labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css,sha256=C5pyD956fd6pJgUBjGxvxgL0Wbgq0v7ZLY4Vr-sJZ7A,4169
|
|
9
|
+
labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info.jinja.html,sha256=njXE9xexFdMlX65eDiwqE8PzbMATUOJAbwBCh5JvahY,1293
|
|
10
|
+
labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html,sha256=6UdsZtqJ3soSAYTKN43OB1Onm93WYz23TfM2GBYzJ-U,152
|
|
6
11
|
labfreed/pac_attributes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
12
|
labfreed/pac_attributes/well_knonw_attribute_keys.py,sha256=axE81MeJ3G_Wy1PbmNAXH6SfPtl96NXvQJMyrvK10t4,324
|
|
8
13
|
labfreed/pac_attributes/api_data_models/request.py,sha256=-CI3rU_Bzw2DZGSS06Jl4zajrxMkfPGhKHWmIfnmWlk,1868
|
|
9
|
-
labfreed/pac_attributes/api_data_models/response.py,sha256=
|
|
14
|
+
labfreed/pac_attributes/api_data_models/response.py,sha256=eGh474ILEcBC1ijhs1ZZfdhNWRxiPeccGS8aw0zzt0U,6934
|
|
10
15
|
labfreed/pac_attributes/api_data_models/server_capabilities_response.py,sha256=ypDm4f8xZZl036fp8PuIe6lJHNW5Zg1fItgUlnV75V0,178
|
|
11
16
|
labfreed/pac_attributes/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
labfreed/pac_attributes/client/attribute_cache.py,sha256=
|
|
13
|
-
labfreed/pac_attributes/client/client.py,sha256=
|
|
17
|
+
labfreed/pac_attributes/client/attribute_cache.py,sha256=ThUadWqQ5oM8DnAnvZuY4jeA3Mg06ePNEcRP5wCsadc,2222
|
|
18
|
+
labfreed/pac_attributes/client/client.py,sha256=DcD9wG3Zc7Iznr5Wlrwp17QgE2tjPxITgmjmSQonvAo,7506
|
|
14
19
|
labfreed/pac_attributes/pythonic/attribute_server_factory.py,sha256=_wasafjBlwvzOaM6-uPgqPethsDQHEpaXoiRW7w9aV0,5759
|
|
15
|
-
labfreed/pac_attributes/pythonic/excel_attribute_data_source.py,sha256=
|
|
16
|
-
labfreed/pac_attributes/pythonic/py_attributes.py,sha256=
|
|
20
|
+
labfreed/pac_attributes/pythonic/excel_attribute_data_source.py,sha256=oP4OHj0DTlH4dD7OlL1qxtX4y9KcuDTCd9Bi_FruP6A,7276
|
|
21
|
+
labfreed/pac_attributes/pythonic/py_attributes.py,sha256=FXSp9_P0o-GuZSDvXtD2fU4g82lglMu9f_-8KPMkEP0,6821
|
|
17
22
|
labfreed/pac_attributes/pythonic/py_dict_data_source.py,sha256=nAz6GA7Xx_0IORPPpt_Wl3sFJa1Q5Fnq5vdf1uQiJF8,531
|
|
18
23
|
labfreed/pac_attributes/server/__init__.py,sha256=JvQ2kpQx62OUwP18bGhOWYU9an_nQW59Y8Lh7HyfVxY,301
|
|
19
|
-
labfreed/pac_attributes/server/attribute_data_sources.py,sha256=
|
|
20
|
-
labfreed/pac_attributes/server/server.py,sha256=
|
|
24
|
+
labfreed/pac_attributes/server/attribute_data_sources.py,sha256=7-YQeBcn5ndsZWeeW_-YgG7obF5qvXoH-AFPpmXWn1I,2337
|
|
25
|
+
labfreed/pac_attributes/server/server.py,sha256=tPOPezRC3YEE0i-MJIc23Me6EARaSqzyFdUNjUzqtdI,9117
|
|
21
26
|
labfreed/pac_attributes/server/translation_data_sources.py,sha256=axALOqfP840sOSdVCRYtrens97mm-hpfONMUyuVlCrY,2145
|
|
22
27
|
labfreed/pac_cat/__init__.py,sha256=KNPtQzBD1XVohvG_ucOs7RJj-oi6biUTGB1k-T2o6pk,568
|
|
23
|
-
labfreed/pac_cat/category_base.py,sha256=
|
|
28
|
+
labfreed/pac_cat/category_base.py,sha256=D7BzsdF0-JIgag5L2XZJRF4T2LOH5RLh1MMszflkmV8,2526
|
|
24
29
|
labfreed/pac_cat/pac_cat.py,sha256=wcb_fhvgjS2xmqTsxS8_Oibvr1nsQt5zr8aUajLfK1E,5578
|
|
25
|
-
labfreed/pac_cat/predefined_categories.py,sha256=
|
|
30
|
+
labfreed/pac_cat/predefined_categories.py,sha256=U1Phgy-q4VKbMv3ruoa8uKKTlxkpZOKnWPqtHLzYUqU,11105
|
|
26
31
|
labfreed/pac_id/__init__.py,sha256=NGMbzkwQ4txKeT5pxdIZordwHO8J3_q84jzPanjKoHg,675
|
|
27
32
|
labfreed/pac_id/extension.py,sha256=NgLexs1LbRMMm4ETrn5m4EY2iWoMDgOTb0UV556jatQ,2227
|
|
28
33
|
labfreed/pac_id/id_segment.py,sha256=r5JU1SJuRXhZJJxy5T3xjrb598wIDTLpivSJhIUAzjQ,4526
|
|
@@ -59,7 +64,7 @@ labfreed/well_known_keys/labfreed/well_known_keys.py,sha256=p-hXwEEIs7p2SKn9DQeL
|
|
|
59
64
|
labfreed/well_known_keys/unece/UneceUnits.json,sha256=kwfQSp_nTuWbADfBBgqTWrvPl6XtM5SedEVLbMJrM7M,898953
|
|
60
65
|
labfreed/well_known_keys/unece/__init__.py,sha256=MSP9lmjg9_D9iqG9Yq2_ajYfQSNS9wIT7FXA1c--59M,122
|
|
61
66
|
labfreed/well_known_keys/unece/unece_units.py,sha256=J20d64H69qKDE3XlGdJoXIIh0G-d0jKoiIDsg9an5pk,1655
|
|
62
|
-
labfreed-1.0.
|
|
63
|
-
labfreed-1.0.
|
|
64
|
-
labfreed-1.0.
|
|
65
|
-
labfreed-1.0.
|
|
67
|
+
labfreed-1.0.0a8.dist-info/licenses/LICENSE,sha256=gHFOv9FRKHxO8cInP3YXyPoJnuNeqrvcHjaE_wPSsQ8,1100
|
|
68
|
+
labfreed-1.0.0a8.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
69
|
+
labfreed-1.0.0a8.dist-info/METADATA,sha256=oCIx5jAp8nDRU5twMpqYDj4qfm9E0Qv-0IidHY31Oqk,19740
|
|
70
|
+
labfreed-1.0.0a8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|