python-materialsdb 0.3.0__py3-none-any.whl → 0.3.2__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.
materialsdb/cache.py CHANGED
@@ -2,6 +2,7 @@ import os
2
2
  import pathlib
3
3
  import urllib.request
4
4
  from collections import namedtuple
5
+ from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
5
6
 
6
7
  from lxml import etree
7
8
 
@@ -85,25 +86,77 @@ def _plan_index(index):
85
86
  return _IndexPlan(index, new_index, unchanged, todos)
86
87
 
87
88
 
88
- def update_producers_data(url_list=MATERIALSDBINDEXURLLIST, on_progress=None):
89
- """on_progress(done, total, name) is called after each producer download
90
- with a global running total; returning a truthy value stops the update
91
- between two downloads (the partial Report is returned as-is)."""
89
+ def update_producers_data(url_list=MATERIALSDBINDEXURLLIST, on_progress=None, max_workers: int = 8):
90
+ """Update the cached producer files from the index URLs.
91
+
92
+ Downloads run on a bounded thread pool (max_workers). on_progress(done,
93
+ total, name) is called once per completed download from the calling
94
+ thread with a monotonic running total; returning a truthy value stops
95
+ NEW submissions (queued jobs are abandoned, in-flight ones finish but
96
+ no longer count toward the partial Report). A download error stops the
97
+ run and the first exception is re-raised after the pool drains.
98
+ """
92
99
  plans = [_plan_index(index) for index in url_list]
93
- total = sum(len(plan.todos) for plan in plans)
94
- existing, updated, deleted = [], [], []
100
+ jobs = [
101
+ (plan, company, cached_producer, producer_path)
102
+ for plan in plans
103
+ for (company, cached_producer, producer_path) in plan.todos
104
+ ]
105
+ total = len(jobs)
106
+ workers = max(1, min(int(max_workers), total or 1))
107
+
108
+ existing: list = []
109
+ updated: list = []
110
+ deleted: list = []
95
111
  done = 0
112
+ cancel_requested = False
113
+ failure = None
114
+
115
+ def _download(job):
116
+ _, company, cached_producer, producer_path = job
117
+ deleted_path = None
118
+ if cached_producer is not None:
119
+ deleted_path = get_producers_dir() / pathlib.Path(cached_producer.get("href")).name
120
+ deleted_path.unlink(True)
121
+ urllib.request.urlretrieve(company.get("href"), producer_path)
122
+ return deleted_path
123
+
124
+ pool = ThreadPoolExecutor(max_workers=workers)
125
+ pending = list(jobs) # pops from the FRONT, submission order preserved
126
+ futures: dict = {}
127
+ try:
128
+ while futures or pending:
129
+ while pending and len(futures) < workers:
130
+ job = pending.pop(0)
131
+ futures[pool.submit(_download, job)] = job
132
+ if not futures:
133
+ break
134
+ ready, _ = wait(set(futures), return_when=FIRST_COMPLETED)
135
+ for future in ready:
136
+ job = futures.pop(future)
137
+ try:
138
+ deleted_path = future.result()
139
+ except Exception as err: # noqa: BLE001 - re-raised below, caller decides
140
+ if failure is None:
141
+ failure = err
142
+ break
143
+ if deleted_path is not None:
144
+ deleted.append(deleted_path)
145
+ updated.append(job[3])
146
+ done += 1
147
+ if on_progress is not None and on_progress(done, total, pathlib.Path(job[3]).name):
148
+ cancel_requested = True
149
+ break
150
+ if cancel_requested or failure is not None:
151
+ break
152
+ finally:
153
+ pool.shutdown(wait=True, cancel_futures=True)
154
+
155
+ if failure is not None:
156
+ raise failure
157
+ if cancel_requested:
158
+ return Report(existing, updated, deleted)
96
159
  for plan in plans:
97
- for company, cached_producer, producer_path in plan.todos:
98
- if cached_producer is not None:
99
- cached_path = get_producers_dir() / pathlib.Path(cached_producer.get("href")).name
100
- deleted.append(cached_path)
101
- cached_path.unlink(True)
102
- urllib.request.urlretrieve(company.get("href"), producer_path)
103
- updated.append(producer_path)
104
- done += 1
105
- if on_progress is not None and on_progress(done, total, pathlib.Path(company.get("href")).name):
106
- return Report(existing, updated, deleted)
107
160
  if plan.todos:
108
161
  plan.new_index.write(str(get_cached_index_path(plan.index)))
109
162
  existing.extend(plan.unchanged)
@@ -5,7 +5,7 @@ MaterialBuilder would create (one entry per IfcMaterial); the Bonsai add-on
5
5
  maps the payload onto its open file without importing this library."""
6
6
 
7
7
  from materialsdb import config, utils
8
- from materialsdb.ifc.material_builder import PSETS, get_value
8
+ from materialsdb.ifc.material_builder import PSETS, get_value, style_for
9
9
 
10
10
 
11
11
  def _resolved_psets(layer, country) -> dict:
@@ -50,6 +50,10 @@ def build_add_materials_payload(store_, items, country=None, lang=None):
50
50
  description = str(utils.get_material_description(material, lang))
51
51
  category = str(getattr(material.information, "group", "") or "")
52
52
  color = getattr(material.information, "color", None)
53
+ color = int(color) if color else None
54
+ # Resolve the surface style server-side: the add-on runs in Blender's
55
+ # interpreter without access to the materialsdb colour scheme.
56
+ style_name, style_rgb = style_for(color, category)
53
57
  entries = []
54
58
  for layer in utils.get_material_layers(material):
55
59
  if wanted is not None and str(layer.id) not in wanted:
@@ -63,7 +67,9 @@ def build_add_materials_payload(store_, items, country=None, lang=None):
63
67
  "name": name,
64
68
  "description": description,
65
69
  "category": category,
66
- "color": int(color) if color else None,
70
+ "color": color,
71
+ "style_name": style_name,
72
+ "style_color": [round(component, 6) for component in style_rgb],
67
73
  "identity": {
68
74
  "material_id": material_id,
69
75
  "company_id": str(summary.company_id or ""),
@@ -120,6 +126,9 @@ def build_add_construction_payload(store_, body, country=None, lang=None):
120
126
  problems.append(f"unknown material id: {layer.material_id}")
121
127
  continue
122
128
  color = getattr(material.information, "color", None)
129
+ color = int(color) if color else None
130
+ category = str(getattr(material.information, "group", "") or "")
131
+ style_name, style_rgb = style_for(color, category)
123
132
  layers.append(
124
133
  {
125
134
  "material_id": layer.material_id,
@@ -128,8 +137,10 @@ def build_add_construction_payload(store_, body, country=None, lang=None):
128
137
  "source_id": layer.material_id,
129
138
  "name": str(utils.get_material_name(material, lang)),
130
139
  "description": str(utils.get_material_description(material, lang)),
131
- "category": str(getattr(material.information, "group", "") or ""),
132
- "color": int(color) if color else None,
140
+ "category": category,
141
+ "color": color,
142
+ "style_name": style_name,
143
+ "style_color": [round(component, 6) for component in style_rgb],
133
144
  "identity": {
134
145
  "material_id": layer.material_id,
135
146
  "company_id": str(summary.company_id or ""),
@@ -2,12 +2,21 @@
2
2
 
3
3
  import json
4
4
  from pathlib import Path
5
+ from typing import TypedDict
5
6
 
6
7
  from materialsdb import config, utils
7
8
 
8
9
  MATERIALSDB_PSET = "materialsdb"
9
10
 
10
- CATEGORIES = {
11
+
12
+ class CategoryStyle(TypedDict):
13
+ """One palette entry. `hatch` is reserved for future pattern support."""
14
+
15
+ hatch: str
16
+ color: tuple[int, int, int]
17
+
18
+
19
+ CATEGORIES: dict[str, CategoryStyle] = {
11
20
  "Others": {"hatch": "", "color": (255, 255, 255)},
12
21
  "Water_Proof": {"hatch": "", "color": (255, 255, 255)},
13
22
  "Vapour_Proof": {"hatch": "", "color": (0, 0, 0)},
@@ -27,6 +36,29 @@ CATEGORIES = {
27
36
  "Soil": {"hatch": "", "color": (142, 84, 52)},
28
37
  }
29
38
 
39
+ # Named colour schemes: a category -> palette. Only the default ships today;
40
+ # more schemes (and a GUI selector) are planned.
41
+ SCHEMES: dict[str, dict[str, CategoryStyle]] = {"Lesosai": CATEGORIES}
42
+ DEFAULT_SCHEME = "Lesosai"
43
+
44
+
45
+ def _rgb_int_components(color: int) -> tuple[float, float, float]:
46
+ """Decimal 0xRRGGBB -> normalised (r, g, b) in 0..1 (IfcColourRgb units)."""
47
+ return (((color >> 16) & 255) / 255, ((color >> 8) & 255) / 255, (color & 255) / 255)
48
+
49
+
50
+ def style_for(color, category, scheme=None):
51
+ """Resolve a material's (IfcSurfaceStyle.Name, (r, g, b) in 0..1).
52
+
53
+ The producer colour wins when present; otherwise the category colour from
54
+ the scheme (default 'Lesosai' = CATEGORIES). Unknown categories fall back
55
+ to 'Others'."""
56
+ if color:
57
+ return f"color {int(color)}", _rgb_int_components(int(color))
58
+ palette = SCHEMES.get(scheme or DEFAULT_SCHEME, CATEGORIES)
59
+ category = category if category in palette else "Others"
60
+ return f"category {category}", tuple(component / 255 for component in palette[category]["color"])
61
+
30
62
 
31
63
  def clean_psets(psets):
32
64
  new_dict = {}
@@ -71,6 +103,7 @@ class MaterialBuilder:
71
103
  name = utils.get_material_name(material, self.lang)
72
104
  description = utils.get_material_description(material, self.lang)
73
105
  category = str(material.information.group or "")
106
+ color = material.information.color
74
107
  if not with_layers:
75
108
  # reuse an already-present material instead of duplicating it;
76
109
  # its identity pset is already in place
@@ -79,15 +112,9 @@ class MaterialBuilder:
79
112
  return [existing]
80
113
  ifc_material = self.file.createIfcMaterial(name, str(description), str(category))
81
114
  self._create_identity_pset(ifc_material, material, company_id, company, verxml)
115
+ self._add_style_chain(color, category)
82
116
  return [ifc_material]
83
- color = material.information.color
84
- surface_style = self.get_surface_style(color, category)
85
- styled_item = self.file.createIfcStyledItem(Styles=[surface_style])
86
- self.file.createIfcStyledRepresentation(
87
- ContextOfItems=self._get_context(),
88
- RepresentationIdentifier="Body",
89
- Items=[styled_item],
90
- )
117
+ self._add_style_chain(color, category)
91
118
  created = []
92
119
  wanted = {str(g) for g in layer_ids} if layer_ids is not None else None
93
120
  for layer in utils.get_material_layers(material):
@@ -191,29 +218,32 @@ class MaterialBuilder:
191
218
  Material=ifc_material,
192
219
  )
193
220
 
194
- def get_surface_style(self, color, category):
195
- if color:
196
- name = f"color {color}"
197
- else:
198
- if not category or category not in CATEGORIES:
199
- category = "Others"
200
- name = f"category {category}"
221
+ def _add_style_chain(self, color, category, scheme=None):
222
+ """Create the floating styled chain MaterialBuilder uses for a
223
+ material's colour (shared style + one styled item per build)."""
224
+ surface_style = self.get_surface_style(color, category, scheme=scheme)
225
+ styled_item = self.file.createIfcStyledItem(Styles=[surface_style])
226
+ self.file.createIfcStyledRepresentation(
227
+ ContextOfItems=self._get_context(),
228
+ RepresentationIdentifier="Body",
229
+ Items=[styled_item],
230
+ )
231
+
232
+ def get_surface_style(self, color, category, scheme=None):
233
+ name, rgb = style_for(color, category, scheme=scheme)
201
234
  if name in self._styles and self._styles[name] in self.file:
202
235
  return self._styles[name]
203
- if color:
204
- style = self.file.createIfcSurfaceStyleShading(SurfaceColour=self.color_xml_to_ifc(color))
205
- else:
206
- style = self.file.createIfcSurfaceStyleShading(
207
- SurfaceColour=self.file.createIfcColourRgb(None, *CATEGORIES[category]["color"]),
208
- )
209
- surface_style = self.file.createIfcSurfaceStyle(Name=name, Side="BOTH", Styles=[style])
236
+ shading = self.file.createIfcSurfaceStyleShading(
237
+ SurfaceColour=self.file.createIfcColourRgb(None, *rgb),
238
+ )
239
+ surface_style = self.file.createIfcSurfaceStyle(Name=name, Side="BOTH", Styles=[shading])
210
240
  self._styles[name] = surface_style
211
241
  return surface_style
212
242
 
213
243
  def color_xml_to_ifc(self, color: int):
214
244
  """Color definition in xml is obscur. We assume that it is a decimal color.
215
245
  See: https://stackoverflow.com/a/2262152/4098083"""
216
- return self.file.createIfcColourRgb(Blue=color & 255, Green=(color >> 8) & 255, Red=(color >> 16) & 255)
246
+ return self.file.createIfcColourRgb(None, *_rgb_int_components(int(color)))
217
247
 
218
248
 
219
249
  def _materials_of(pset):
materialsdb/serialiser.py CHANGED
@@ -25,9 +25,31 @@ def get_xml_schema() -> str:
25
25
  return str(Path(__file__).parent / "schema/materialsdb103.xsd")
26
26
 
27
27
 
28
+ @cache
29
+ def cached_type_hints(cls) -> dict:
30
+ return typing.get_type_hints(cls)
31
+
32
+
33
+ @cache
34
+ def _tag_local_name(tag: str) -> str:
35
+ m = re.search("{.*}(.*)", tag)
36
+ return m.group(1) if m else tag
37
+
38
+
28
39
  def get_element_name(element: objectify.ObjectifiedElement) -> str:
29
- m = re.search("{.*}(.*)", element.tag)
30
- return m.group(1) if m else element.tag
40
+ return _tag_local_name(element.tag)
41
+
42
+
43
+ @cache
44
+ def _is_optional(type_hint):
45
+ return typing.get_origin(type_hint) is typing.Union and typing.get_args(type_hint)[1] is type(None)
46
+
47
+
48
+ @cache
49
+ def _strip_optional(type_hint):
50
+ if _is_optional(type_hint):
51
+ return typing.get_args(type_hint)[0]
52
+ return type_hint
31
53
 
32
54
 
33
55
  def create_element_maker():
@@ -49,11 +71,6 @@ def get_valid_root(tree: objectify.ObjectifiedElement) -> objectify.ObjectifiedE
49
71
  return root
50
72
 
51
73
 
52
- @cache
53
- def cached_type_hints(cls) -> dict:
54
- return typing.get_type_hints(cls)
55
-
56
-
57
74
  class XmlDeserialiser:
58
75
  def __init__(self):
59
76
  self.schema = etree.XMLSchema(file=get_xml_schema())
@@ -93,7 +110,7 @@ class XmlDeserialiser:
93
110
  value = element.get(attrib)
94
111
  if value is None:
95
112
  continue
96
- base_class = self.strip_optional(type_hints[attrib])
113
+ base_class = _strip_optional(type_hints[attrib])
97
114
 
98
115
  kwargs[attrib] = base_class(value)
99
116
  for child_name in getattr(element_class, "xml_elements", ()):
@@ -127,12 +144,10 @@ class XmlDeserialiser:
127
144
  return instance
128
145
 
129
146
  def strip_optional(self, type_hint):
130
- if self.is_optional(type_hint):
131
- return typing.get_args(type_hint)[0]
132
- return type_hint
147
+ return _strip_optional(type_hint)
133
148
 
134
149
  def is_optional(self, type_hint):
135
- return typing.get_origin(type_hint) is typing.Union and typing.get_args(type_hint)[1] is type(None)
150
+ return _is_optional(type_hint)
136
151
 
137
152
  def cls_name(self, name: str) -> str:
138
153
  return name if name[0].isupper() else name.title()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-materialsdb
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: A library to work with materialsdb.org open standard for building materials.
5
5
  Author-email: Cyril Waechter <cyrwae@hotmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -1,17 +1,17 @@
1
1
  materialsdb/__init__.py,sha256=lml7ffWFF3-0MD-Z7YbPZNireUpDO7tR9B3KNamrI0k,127
2
- materialsdb/cache.py,sha256=9XpJ7I5mLNpQZsVZSggIdUwS3votombJ9PfjbVbl-OA,4751
2
+ materialsdb/cache.py,sha256=wPCksig6hidUssjRx_KaPk9V5ZeWbHfbs0Qt2XTuLPk,6594
3
3
  materialsdb/classes.py,sha256=WdXNiCqFcevgWrqDZKLR8HPio3BwcmOJ1NyVGArQ0hw,21549
4
4
  materialsdb/config.py,sha256=Tp14G_Y_XEkYshwrp4gd9XjGb5e_mO-hxYOevCTVnmI,1291
5
5
  materialsdb/construction.py,sha256=29H5gt3EX0LFNJ7ftE0Ob7huQgyZzx3ygW7nNcUSo5s,17817
6
6
  materialsdb/query.py,sha256=h9kP3Zc41l4h9bcxjwp-UUb8ezsG0A3ZkraJjaQZVVI,620
7
- materialsdb/serialiser.py,sha256=GL_e1FW6inuvfLvqQzp81L8XgoByRtVtd7nWtrYKWmY,7056
7
+ materialsdb/serialiser.py,sha256=J6bUc7eKYD569TrOWVAK5ZDeb7cdD2YbF0nD6jf3iPY,7263
8
8
  materialsdb/store.py,sha256=_Iu425X7-sYE7A2vvQVrR1R6ju2R63m_BK43c9y3mhM,11000
9
9
  materialsdb/summary.py,sha256=vl6LhVIfN4KHN-KZxU6oVBYut0FPA3E_baGp_fegnBU,2558
10
10
  materialsdb/utils.py,sha256=2xGwr6b4NgOtjT5lxpFVN9jnOSlDsmZPnelSGEs5MDs,3068
11
11
  materialsdb/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  materialsdb/gui/__main__.py,sha256=Rha9z_Lxmf2mnCfLUV8s8ZqMRankDhtRrype1v5cm48,1405
13
13
  materialsdb/gui/discovery.py,sha256=au_X0ULkIxjAMV1EWgVAs1aLcl902wi4Kzw6y7M26Ag,792
14
- materialsdb/gui/listener.py,sha256=45DUK4tJALful0lVWX2x0VtfNVGkZ-pwmaWQwmHN0CE,6495
14
+ materialsdb/gui/listener.py,sha256=tfHjQyvu-YkTfXpFUoKwZZQzfm9y6Jo87LCOZkXbJOk,7107
15
15
  materialsdb/gui/server.py,sha256=yMYy_EDPp5Y7kiQiRDc4rGJ_xbSda3RiycWs3CZyN5w,33553
16
16
  materialsdb/gui/static/app-constructions.js,sha256=59oO_oo9li6SJacA1XYQHVG3rx37MJG7ySaM8Mpn5s4,31343
17
17
  materialsdb/gui/static/app.js,sha256=eDb53OkMhslgp5vVghaDZstSK_ooenu0cGumjaMZcOY,22707
@@ -19,15 +19,15 @@ materialsdb/gui/static/constructions.html,sha256=kozoWdVHWrKTiB08kcchXLMuL6jijGE
19
19
  materialsdb/gui/static/index.html,sha256=4pXwmXLIwUIosa7wVBRzVbeuLf_WzlRUs5dxwZsSeNU,3208
20
20
  materialsdb/gui/static/picker-core.js,sha256=bK8308VJcrTgEZECblELwtrU3QW1VZHuqtM3pBPTaIg,4145
21
21
  materialsdb/ifc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- materialsdb/ifc/material_builder.py,sha256=PESV1ov1BL4aFrurOkI7CMe3i6WEWfK4kgBEDm52-sQ,12245
22
+ materialsdb/ifc/material_builder.py,sha256=PGpznk02_wHcCH4o0jC6jRDoDmugA5kqJO6XL6wL2UY,13523
23
23
  materialsdb/ifc/material_psets.json,sha256=lQ0zQgBq3xnxyYItkmu7BzB5KE9ysFof-rrHawb5T-M,10897
24
24
  materialsdb/ifc/project_library.py,sha256=hOvirB73ZiTYiP6rOu6eN1dU2ZXFRgSOGpQrH2DY4RM,6324
25
25
  materialsdb/schema/MaterialsDBIndex100.xsd,sha256=oK81n2b3_82Mt5G31yEm9vwkMjUmNyM6jglt_zpUQZc,2199
26
26
  materialsdb/schema/materialsdb102.xsd,sha256=IaC6E_B3N1VaU9lhrl6ZGwi8m2rk2p7HTEZKN_Yj2AY,32880
27
27
  materialsdb/schema/materialsdb103.xsd,sha256=iX7Q1ejWEN_ZSuRVCX40zjYt5UraY3f_JxBzqZUFu7Q,37762
28
- python_materialsdb-0.3.0.dist-info/licenses/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
29
- python_materialsdb-0.3.0.dist-info/METADATA,sha256=dugVsiclmutXpwQj_CI8tkEe0oGNus2Amc8vnZqit_w,7673
30
- python_materialsdb-0.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
31
- python_materialsdb-0.3.0.dist-info/entry_points.txt,sha256=7bCySVv6oV9hiva7Fn5Dm9oboPRIC6DU6DRlsjCLO6k,66
32
- python_materialsdb-0.3.0.dist-info/top_level.txt,sha256=5ZHbF8Oj1W24bifG65Fl3hCaivZpkO6EyOHHaRavZxE,12
33
- python_materialsdb-0.3.0.dist-info/RECORD,,
28
+ python_materialsdb-0.3.2.dist-info/licenses/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
29
+ python_materialsdb-0.3.2.dist-info/METADATA,sha256=EWVnNfW1XA8YsScGmrwWkLBqKVkvy8dGAvZgX0pk7cw,7673
30
+ python_materialsdb-0.3.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
31
+ python_materialsdb-0.3.2.dist-info/entry_points.txt,sha256=7bCySVv6oV9hiva7Fn5Dm9oboPRIC6DU6DRlsjCLO6k,66
32
+ python_materialsdb-0.3.2.dist-info/top_level.txt,sha256=5ZHbF8Oj1W24bifG65Fl3hCaivZpkO6EyOHHaRavZxE,12
33
+ python_materialsdb-0.3.2.dist-info/RECORD,,