python-materialsdb 0.0.1__py3-none-any.whl → 0.1.0__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
@@ -1,10 +1,14 @@
1
1
  import os
2
2
  import pathlib
3
3
  import urllib.request
4
+ from collections import namedtuple
4
5
  from typing import Optional
5
6
  from lxml import etree
6
7
 
7
- MATERIALSDBINDEXURL = "http://www.materialsdb.org/download/ProducerIndex.xml"
8
+ MATERIALSDBINDEXURLLIST = [
9
+ "http://www.materialsdb.org/download/ProducerIndex.xml",
10
+ "http://www.materialsdb.org/download/generic/GenericIndex.xml",
11
+ ]
8
12
 
9
13
 
10
14
  def get_cache_folder():
@@ -19,12 +23,12 @@ def get_cache_folder():
19
23
  return cache_dir
20
24
 
21
25
 
22
- def get_cached_index_path() -> pathlib.Path:
23
- return get_cache_folder() / "ProducerIndex.xml"
26
+ def get_cached_index_path(index) -> pathlib.Path:
27
+ return get_cache_folder() / pathlib.Path(index).name
24
28
 
25
29
 
26
- def parse_cached_index() -> etree._ElementTree:
27
- path = get_cached_index_path()
30
+ def parse_cached_index(index) -> etree._ElementTree:
31
+ path = get_cached_index_path(index)
28
32
  if path.exists():
29
33
  return etree.parse(str(path))
30
34
  root = etree.Element("root")
@@ -53,27 +57,48 @@ def get_producers_dir() -> pathlib.Path:
53
57
  return producer_path
54
58
 
55
59
 
56
- def update_producers_data():
57
- cached_index = parse_cached_index()
60
+ Report = namedtuple("Report", ["existing", "updated", "deleted"])
61
+
62
+
63
+ def update_producers_data(url_list=MATERIALSDBINDEXURLLIST):
64
+ existing = []
65
+ updated = []
66
+ deleted = []
67
+ for index in url_list:
68
+ report = update_producers_from_index(index)
69
+ existing.extend(report.existing)
70
+ updated.extend(report.updated)
71
+ deleted.extend(report.deleted)
72
+ return Report(existing, updated, deleted)
73
+
74
+
75
+ def update_producers_from_index(index):
76
+ cached_index = parse_cached_index(index)
58
77
  cached_root = cached_index.getroot()
59
- new_index = etree.parse(MATERIALSDBINDEXURL)
78
+ with urllib.request.urlopen(index) as response:
79
+ new_index = etree.parse(response)
60
80
  new_root = new_index.getroot()
61
81
  producers_dir = get_producers_dir()
62
82
  has_index_update = False
83
+ existing = []
84
+ updated = []
85
+ deleted = []
63
86
  for company in new_root:
64
- cached_company = get_by_id(cached_root, company.get("id"))
65
- if (
66
- not require_update(cached_company, company)
67
- and (producers_dir / pathlib.Path(company.get("href")).name).exists()
68
- ):
87
+ cached_producer = get_by_id(cached_root, company.get("id"))
88
+ producer_path = producers_dir / pathlib.Path(company.get("href")).name
89
+ if not require_update(cached_producer, company) and producer_path.exists():
90
+ existing.append(producer_path)
69
91
  continue
70
- if cached_company:
71
- (producers_dir / pathlib.Path(cached_company.get("href")).name).unlink(True)
92
+ if cached_producer:
93
+ cached_path = producers_dir / pathlib.Path(cached_producer.get("href")).name
94
+ deleted.append(cached_path)
95
+ cached_path.unlink(True)
72
96
  has_index_update = True
73
- producer_path = producers_dir / pathlib.Path(company.get("href")).name
74
97
  urllib.request.urlretrieve(company.get("href"), producer_path)
98
+ updated.append(producer_path)
75
99
  if has_index_update:
76
- new_index.write(str(get_cached_index_path()))
100
+ new_index.write(str(get_cached_index_path(index)))
101
+ return Report(existing, updated, deleted)
77
102
 
78
103
 
79
104
  def producers():
@@ -6,14 +6,10 @@ import ifcopenshell
6
6
  import ifcopenshell.api
7
7
 
8
8
  from materialsdb.serialiser import XmlDeserialiser
9
- from materialsdb import config
9
+ from materialsdb import config, utils
10
10
  from materialsdb.classes import (
11
11
  Materials,
12
12
  Material,
13
- TLocalizedString,
14
- Webinfo,
15
- ISO639_1,
16
- Mimetype,
17
13
  )
18
14
 
19
15
  CATEGORIES = {
@@ -53,33 +49,12 @@ PSETS = json.loads(Path(__file__).with_name("material_psets.json").read_text("ut
53
49
  PSETS = clean_psets(PSETS)
54
50
 
55
51
 
56
- def date_from_xml(days: float) -> datetime.datetime:
57
- """Convert days since 30.12.1899 which is materialsdb xml convention to datetime"""
58
- return datetime.datetime(1899, 12, 30) + datetime.timedelta(days=days)
59
-
60
-
61
- def date_to_xml(date: datetime.datetime) -> float:
62
- """Convert datetime to days since 30.12.1899 which is materialsdb xml convention
63
- >>> datetime.timedelta(days=1).total_seconds()
64
- 86400.0"""
65
- return (date - datetime.datetime(1899, 12, 30)).total_seconds() / 86400
66
-
67
-
68
- def get_by_country(values, country):
69
- for value in values:
70
- if value.country == country:
71
- return value
72
- for value in values:
73
- if value.country is None:
74
- return value
75
-
76
-
77
52
  def get_value(layer, definition, country=None):
78
53
  value = layer
79
54
  for attrib in definition["path"]:
80
55
  value = getattr(value, attrib)
81
56
  if isinstance(value, list):
82
- value = get_by_country(value, country)
57
+ value = utils.get_by_country(value, country)
83
58
  if not value:
84
59
  return None
85
60
  return value
@@ -147,7 +122,7 @@ class ProjectLibrary:
147
122
  owner_history = file.createIfcOwnerHistory(
148
123
  OwningUser=person_and_organisation,
149
124
  OwningApplication=self.application,
150
- CreationDate=int(date_from_xml(source.crd).timestamp()),
125
+ CreationDate=max(int(utils.date_from_xml(source.crd).timestamp()),-2147483648),
151
126
  )
152
127
  self.owner_history = owner_history
153
128
 
@@ -164,10 +139,10 @@ class ProjectLibrary:
164
139
  def create_materials(self, source: Materials):
165
140
  file = self.file
166
141
  context = file.createIfcRepresentationContext()
167
- for material in source.material:
168
- name = get_material_name(material, self.lang)
169
- description = get_material_description(material, self.lang)
170
- webinfo = get_material_webinfo(material, self.lang)
142
+ for material in utils.get_materials(source, self.country):
143
+ name = utils.get_material_name(material, self.lang)
144
+ description = utils.get_material_description(material, self.lang)
145
+ webinfo = utils.get_material_webinfo(material, self.lang)
171
146
  category = material.information.group
172
147
  labels = material.information.labels
173
148
  color = material.information.color
@@ -208,9 +183,9 @@ class ProjectLibrary:
208
183
  Properties=properties,
209
184
  Material=ifc_material,
210
185
  )
211
- geometry = get_by_country(layer.geometry, self.country)
186
+ geometry = utils.get_by_country(layer.geometry, self.country)
212
187
  if getattr(geometry, "thick", None):
213
- element_name = name + f" | {geometry.thick}mm"
188
+ element_name = f"{name} | {geometry.thick}mm"
214
189
  ifc_layer = file.create_entity(
215
190
  "IfcMaterialLayer",
216
191
  Material=ifc_material,
@@ -234,7 +209,7 @@ class ProjectLibrary:
234
209
  ifcopenshell.api.run(
235
210
  "material.assign_material",
236
211
  file,
237
- product=wall,
212
+ products=[wall],
238
213
  material=assigned_material,
239
214
  )
240
215
  if material.information.roof:
@@ -246,7 +221,7 @@ class ProjectLibrary:
246
221
  ifcopenshell.api.run(
247
222
  "material.assign_material",
248
223
  file,
249
- product=roof,
224
+ products=[roof],
250
225
  material=assigned_material,
251
226
  )
252
227
  if material.information.floor:
@@ -258,7 +233,7 @@ class ProjectLibrary:
258
233
  ifcopenshell.api.run(
259
234
  "material.assign_material",
260
235
  file,
261
- product=slab,
236
+ products=[slab],
262
237
  material=assigned_material,
263
238
  )
264
239
  if material.information.door:
@@ -270,7 +245,7 @@ class ProjectLibrary:
270
245
  ifcopenshell.api.run(
271
246
  "material.assign_material",
272
247
  file,
273
- product=door,
248
+ products=[door],
274
249
  material=assigned_material,
275
250
  )
276
251
 
@@ -306,32 +281,6 @@ class ProjectLibrary:
306
281
  )
307
282
 
308
283
 
309
- def get_material_name(material: Material, lang: str) -> TLocalizedString:
310
- name = TLocalizedString("")
311
- for name in material.information.names.name:
312
- if name.lang == lang:
313
- return name
314
- return name
315
-
316
-
317
- def get_material_description(material: Material, lang: str) -> TLocalizedString:
318
- description = TLocalizedString("")
319
- explanations = getattr(material.information, "explanations", None)
320
- for description in getattr(explanations, "explanation", ()):
321
- if description.lang == lang:
322
- return description
323
- return description
324
-
325
-
326
- def get_material_webinfo(material: Material, lang: str) -> Webinfo:
327
- webinfo = Webinfo(Mimetype(""), "", "", lang=ISO639_1(lang))
328
- webinfos = getattr(material.information, "webinfos", None)
329
- for webinfo in getattr(webinfos, "webinfo", ()):
330
- if webinfo.lang == lang:
331
- return webinfo
332
- return webinfo
333
-
334
-
335
284
  def create_project_library_from_xml(xml_path):
336
285
  library = ProjectLibrary()
337
286
  deserialiser = XmlDeserialiser()
materialsdb/query.py ADDED
@@ -0,0 +1,24 @@
1
+ """Convenience facade over the SQLite material store."""
2
+ from functools import lru_cache
3
+ from typing import List, Optional
4
+
5
+ from materialsdb.classes import Material
6
+ from materialsdb.store import MaterialStore, Report
7
+ from materialsdb.summary import MaterialSummary
8
+
9
+
10
+ @lru_cache(maxsize=1)
11
+ def get_store() -> MaterialStore:
12
+ return MaterialStore()
13
+
14
+
15
+ def get_material(material_id: str) -> Optional[Material]:
16
+ return get_store().get(material_id)
17
+
18
+
19
+ def search(text: str, **filters) -> List[MaterialSummary]:
20
+ return get_store().summaries(text=text, **filters)
21
+
22
+
23
+ def refresh(force: bool = False) -> Report:
24
+ return get_store().refresh(force=force)
materialsdb/serialiser.py CHANGED
@@ -9,6 +9,8 @@ See the LICENSE.md file for more details.
9
9
  Author : Cyril Waechter
10
10
  """
11
11
  import re
12
+ from concurrent.futures import ThreadPoolExecutor
13
+ from functools import lru_cache
12
14
  from pathlib import Path
13
15
  import typing
14
16
  from typing import Protocol, Tuple, Dict, Type, Optional, Any, Union
@@ -50,25 +52,46 @@ def get_valid_root(tree: objectify.ObjectifiedElement) -> str:
50
52
  return root
51
53
 
52
54
 
55
+ @lru_cache(maxsize=None)
56
+ def cached_type_hints(cls) -> dict:
57
+ return typing.get_type_hints(cls)
58
+
59
+
53
60
  class XmlDeserialiser:
54
61
  def __init__(self):
55
62
  self.schema = etree.XMLSchema(file=get_xml_schema())
56
63
  self.parser = objectify.makeparser(schema=self.schema)
57
64
 
58
- def from_xml(self, xml_path: str, assert_schema: bool = False):
65
+ def from_xml(self, xml_path: str, assert_schema: bool = False) -> classes.Materials:
59
66
  if assert_schema:
60
67
  tree = objectify.parse(xml_path, self.parser)
61
68
  else:
62
69
  tree = objectify.parse(xml_path)
63
70
  return self.from_element(get_valid_root(tree))
64
71
 
72
+ def from_xml_files(self, paths, max_workers=None):
73
+ """Parse multiple producer XML files concurrently.
74
+
75
+ Yields (path, Materials) per successfully parsed file and
76
+ (path, None) for files that could not be parsed."""
77
+
78
+ def load(path):
79
+ try:
80
+ return Path(path), self.from_xml(str(path))
81
+ except Exception as err:
82
+ print(f"{Path(path).name}: could not parse file:\n\t{err}")
83
+ return Path(path), None
84
+
85
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
86
+ yield from executor.map(load, paths)
87
+
65
88
  def from_element(self, element=None, base_class=None):
66
89
  element_name = get_element_name(element)
67
90
  element_class = base_class or getattr(classes, self.cls_name(element_name))
68
91
  kwargs: Dict[str, Any] = {}
69
92
  if element_class.xs_type != "element":
70
93
  kwargs["object"] = element.text or ""
71
- type_hints = typing.get_type_hints(element_class)
94
+ type_hints = cached_type_hints(element_class)
72
95
  for attrib in getattr(element_class, "xml_attributes", ()):
73
96
  value = element.get(attrib)
74
97
  if value is None:
materialsdb/store.py ADDED
@@ -0,0 +1,290 @@
1
+ import datetime
2
+ import hashlib
3
+ import json
4
+ import sqlite3
5
+ from collections import namedtuple
6
+ from pathlib import Path
7
+ from typing import List, Optional
8
+
9
+ from lxml import etree, objectify
10
+
11
+ from materialsdb import cache, config
12
+ from materialsdb.classes import Material
13
+ from materialsdb.serialiser import XmlDeserialiser, get_valid_root
14
+ from materialsdb.summary import MaterialSummary, summarize_material
15
+
16
+ Report = namedtuple("Report", ["existing", "updated", "deleted", "skipped"])
17
+
18
+ SCHEMA_VERSION = "1"
19
+
20
+ _SCHEMA = """
21
+ CREATE TABLE IF NOT EXISTS materials (
22
+ id TEXT PRIMARY KEY, company_id TEXT, company TEXT, category TEXT,
23
+ names TEXT, descriptions TEXT,
24
+ lambda_min REAL, lambda_max REAL, thick_min REAL, thick_max REAL,
25
+ usage TEXT, source_file TEXT, xml BLOB);
26
+ CREATE INDEX IF NOT EXISTS idx_company ON materials(company);
27
+ CREATE INDEX IF NOT EXISTS idx_category ON materials(category);
28
+ CREATE INDEX IF NOT EXISTS idx_lambda ON materials(lambda_min);
29
+ CREATE TABLE IF NOT EXISTS producer_files (
30
+ path TEXT PRIMARY KEY, sha256 TEXT, built_at REAL);
31
+ CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
32
+ """
33
+
34
+ _MATERIAL_COLUMNS = (
35
+ "id",
36
+ "company_id",
37
+ "company",
38
+ "category",
39
+ "names",
40
+ "descriptions",
41
+ "lambda_min",
42
+ "lambda_max",
43
+ "thick_min",
44
+ "thick_max",
45
+ "usage",
46
+ "source_file",
47
+ "xml",
48
+ )
49
+ _COLUMN_LIST = ", ".join(_MATERIAL_COLUMNS)
50
+
51
+ _NUMERIC_SORTS = {"lambda": "lambda_min", "thick": "thick_min"}
52
+ _STRING_SORTS = {"company": "company", "category": "category"}
53
+
54
+
55
+ def _sha256(path: Path) -> str:
56
+ return hashlib.sha256(path.read_bytes()).hexdigest()
57
+
58
+
59
+ class MaterialStore:
60
+ SCHEMA_VERSION = SCHEMA_VERSION
61
+
62
+ def __init__(self, db_path: Optional[Path] = None):
63
+ self.db_path = (
64
+ Path(db_path) if db_path else cache.get_cache_folder() / "materials.db"
65
+ )
66
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
67
+ self.connection = sqlite3.connect(str(self.db_path))
68
+ self.connection.execute("PRAGMA journal_mode=WAL")
69
+ self.connection.executescript(_SCHEMA)
70
+ self._ensure_schema_version()
71
+
72
+ # ---------- meta / lifecycle ----------
73
+
74
+ def _ensure_schema_version(self):
75
+ row = self.connection.execute(
76
+ "SELECT value FROM meta WHERE key='schema_version'"
77
+ ).fetchone()
78
+ stored = row[0] if row else None
79
+ if stored != SCHEMA_VERSION:
80
+ self.connection.execute("DROP TABLE IF EXISTS materials")
81
+ self.connection.execute("DROP TABLE IF EXISTS producer_files")
82
+ self.connection.executescript(_SCHEMA)
83
+ self.connection.execute(
84
+ "INSERT INTO meta(key, value) VALUES ('schema_version', ?) "
85
+ "ON CONFLICT(key) DO UPDATE SET value=excluded.value",
86
+ (SCHEMA_VERSION,),
87
+ )
88
+ self.connection.commit()
89
+
90
+ def close(self):
91
+ self.connection.close()
92
+
93
+ # ---------- build / refresh ----------
94
+
95
+ def refresh(self, force=False, paths=None) -> Report:
96
+ if paths is None:
97
+ paths = list(cache.producers())
98
+ else:
99
+ paths = [Path(p) for p in paths]
100
+
101
+ kept = {str(p) for p in paths}
102
+ deleted = []
103
+ for (stored_path,) in self.connection.execute(
104
+ "SELECT path FROM producer_files"
105
+ ).fetchall():
106
+ if stored_path not in kept:
107
+ self.connection.execute(
108
+ "DELETE FROM materials WHERE source_file=?", (stored_path,)
109
+ )
110
+ self.connection.execute(
111
+ "DELETE FROM producer_files WHERE path=?", (stored_path,)
112
+ )
113
+ deleted.append(Path(stored_path))
114
+
115
+ existing, updated, skipped = [], [], []
116
+ deserialiser = XmlDeserialiser()
117
+ for path in paths:
118
+ row = self.connection.execute(
119
+ "SELECT sha256 FROM producer_files WHERE path=?", (str(path),)
120
+ ).fetchone()
121
+ try:
122
+ digest = _sha256(path)
123
+ if not force and row and row[0] == digest:
124
+ existing.append(path)
125
+ continue
126
+ self._upsert_file(deserialiser, path)
127
+ except Exception as err:
128
+ print(f"{path.name}: skipped during store refresh:\n\t{err}")
129
+ skipped.append(path)
130
+ continue
131
+ self.connection.execute(
132
+ "INSERT INTO producer_files(path, sha256, built_at) VALUES (?, ?, ?) "
133
+ "ON CONFLICT(path) DO UPDATE SET sha256=excluded.sha256, "
134
+ "built_at=excluded.built_at",
135
+ (str(path), digest, datetime.datetime.now().timestamp()),
136
+ )
137
+ updated.append(path)
138
+
139
+ self.connection.commit()
140
+ return Report(existing, updated, deleted, skipped)
141
+
142
+ def _upsert_file(self, deserialiser: XmlDeserialiser, path: Path):
143
+ tree = objectify.parse(str(path))
144
+ root = get_valid_root(tree)
145
+ source = deserialiser.from_element(root)
146
+ self.connection.execute(
147
+ "DELETE FROM materials WHERE source_file=?", (str(path),)
148
+ )
149
+ for element in root.material:
150
+ material = deserialiser.from_element(element)
151
+ summary = summarize_material(
152
+ material, company_id=str(source.companyid), company=source.company
153
+ )
154
+ self.connection.execute(
155
+ f"INSERT INTO materials ({_COLUMN_LIST}) "
156
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
157
+ (
158
+ summary.id,
159
+ summary.company_id,
160
+ summary.company,
161
+ summary.category,
162
+ json.dumps(summary.names),
163
+ json.dumps(summary.descriptions),
164
+ summary.lambda_min,
165
+ summary.lambda_max,
166
+ summary.thick_min,
167
+ summary.thick_max,
168
+ json.dumps(summary.usage),
169
+ str(path),
170
+ sqlite3.Binary(etree.tostring(element)),
171
+ ),
172
+ )
173
+
174
+ # ---------- queries ----------
175
+
176
+ def summaries(
177
+ self,
178
+ company=None,
179
+ category=None,
180
+ min_lambda=None,
181
+ max_lambda=None,
182
+ min_thick=None,
183
+ max_thick=None,
184
+ usage=None,
185
+ text=None,
186
+ sort="company",
187
+ ascending=True,
188
+ lang=None,
189
+ ) -> List[MaterialSummary]:
190
+ where, params = [], []
191
+
192
+ def add(condition, value):
193
+ where.append(condition)
194
+ params.append(value)
195
+
196
+ if company:
197
+ add("company=?", company)
198
+ if category:
199
+ add("category=?", category)
200
+ if min_lambda is not None:
201
+ add("lambda_max>=?", min_lambda)
202
+ if max_lambda is not None:
203
+ add("lambda_min<=?", max_lambda)
204
+ if min_thick is not None:
205
+ add("thick_max>=?", min_thick)
206
+ if max_thick is not None:
207
+ add("thick_min<=?", max_thick)
208
+
209
+ query = f"SELECT {_COLUMN_LIST} FROM materials"
210
+ if where:
211
+ query += " WHERE " + " AND ".join(where)
212
+ rows = self.connection.execute(query, params).fetchall()
213
+
214
+ lang = lang or config.get_lang()
215
+ results = [self._row_to_summary(row) for row in rows]
216
+
217
+ if usage:
218
+ results = [r for r in results if r.usage.get(usage)]
219
+ if text:
220
+ needle = text.lower()
221
+ results = [
222
+ r
223
+ for r in results
224
+ if needle in (r.names.get(lang) or r.names.get("") or "").lower()
225
+ ]
226
+ return self._sorted(results, sort, ascending)
227
+
228
+ @staticmethod
229
+ def _sorted(
230
+ results: List[MaterialSummary], sort: str, ascending: bool
231
+ ) -> List[MaterialSummary]:
232
+ reverse = not ascending
233
+ if sort == "name":
234
+ return sorted(
235
+ results,
236
+ key=lambda r: r.names.get("") or "",
237
+ reverse=reverse,
238
+ )
239
+ if sort in _NUMERIC_SORTS:
240
+ attr = _NUMERIC_SORTS[sort]
241
+ return sorted(
242
+ results,
243
+ key=lambda r: (
244
+ (getattr(r, attr) is None) != reverse,
245
+ getattr(r, attr) if getattr(r, attr) is not None else 0,
246
+ ),
247
+ reverse=reverse,
248
+ )
249
+ attr = _STRING_SORTS.get(sort, "company")
250
+ return sorted(results, key=lambda r: str(getattr(r, attr)), reverse=reverse)
251
+
252
+ @staticmethod
253
+ def _row_to_summary(row) -> MaterialSummary:
254
+ (
255
+ id_,
256
+ company_id,
257
+ company,
258
+ category,
259
+ names,
260
+ descriptions,
261
+ lambda_min,
262
+ lambda_max,
263
+ thick_min,
264
+ thick_max,
265
+ usage,
266
+ _source_file,
267
+ _xml,
268
+ ) = row
269
+ return MaterialSummary(
270
+ id=id_,
271
+ company_id=company_id,
272
+ company=company,
273
+ category=category,
274
+ names=json.loads(names),
275
+ descriptions=json.loads(descriptions),
276
+ lambda_min=lambda_min,
277
+ lambda_max=lambda_max,
278
+ thick_min=thick_min,
279
+ thick_max=thick_max,
280
+ usage=json.loads(usage),
281
+ )
282
+
283
+ def get(self, material_id: str) -> Optional[Material]:
284
+ row = self.connection.execute(
285
+ "SELECT xml FROM materials WHERE id=?", (material_id,)
286
+ ).fetchone()
287
+ if row is None:
288
+ return None
289
+ element = objectify.fromstring(bytes(row[0]))
290
+ return XmlDeserialiser().from_element(element)
materialsdb/summary.py ADDED
@@ -0,0 +1,75 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, Optional, Tuple
3
+
4
+ from materialsdb import config, utils
5
+ from materialsdb.classes import Material
6
+
7
+ USAGE_FLAGS = ("wall", "roof", "floor", "door")
8
+
9
+
10
+ @dataclass
11
+ class MaterialSummary:
12
+ id: str
13
+ company_id: str
14
+ company: str
15
+ category: str
16
+ names: Dict[str, str]
17
+ descriptions: Dict[str, str]
18
+ lambda_min: Optional[float]
19
+ lambda_max: Optional[float]
20
+ thick_min: Optional[float]
21
+ thick_max: Optional[float]
22
+ usage: Dict[str, bool]
23
+
24
+
25
+ def _localized_dict(items) -> Dict[str, str]:
26
+ result: Dict[str, str] = {}
27
+ for item in items or ():
28
+ result[str(item.lang or "")] = str(item)
29
+ return result
30
+
31
+
32
+ def _min_max(values) -> Tuple[Optional[float], Optional[float]]:
33
+ values = [v for v in values if v is not None]
34
+ if not values:
35
+ return None, None
36
+ return min(values), max(values)
37
+
38
+
39
+ def summarize_material(
40
+ material: Material,
41
+ company_id: str = "",
42
+ company: str = "",
43
+ country: Optional[str] = None,
44
+ ) -> MaterialSummary:
45
+ country = country or config.get_country()
46
+ information = material.information
47
+
48
+ lambdas = []
49
+ thicks = []
50
+ for layer in utils.get_material_layers(material):
51
+ thermal = utils.get_by_country(layer.thermal or (), country)
52
+ geometry = utils.get_by_country(layer.geometry or (), country)
53
+ if thermal is not None:
54
+ lambdas.append(thermal.lambda_value)
55
+ if geometry is not None:
56
+ thicks.append(geometry.thick)
57
+
58
+ lambda_min, lambda_max = _min_max(lambdas)
59
+ thick_min, thick_max = _min_max(thicks)
60
+
61
+ return MaterialSummary(
62
+ id=str(material.id),
63
+ company_id=str(company_id),
64
+ company=str(company),
65
+ category=str(information.group or ""),
66
+ names=_localized_dict(getattr(information.names, "name", ())),
67
+ descriptions=_localized_dict(
68
+ getattr(getattr(information, "explanations", None), "explanation", ())
69
+ ),
70
+ lambda_min=lambda_min,
71
+ lambda_max=lambda_max,
72
+ thick_min=thick_min,
73
+ thick_max=thick_max,
74
+ usage={flag: str(getattr(information, flag)) == "1" for flag in USAGE_FLAGS},
75
+ )
materialsdb/utils.py CHANGED
@@ -1,5 +1,15 @@
1
1
  import datetime
2
- from materialsdb.classes import TDateTime
2
+ from typing import Generator
3
+ from materialsdb.classes import (
4
+ TDateTime,
5
+ TLocalizedString,
6
+ Mimetype,
7
+ Webinfo,
8
+ ISO639_1,
9
+ Materials,
10
+ Material,
11
+ Layer,
12
+ )
3
13
 
4
14
 
5
15
  def date_from_xml(days: float) -> datetime.datetime:
@@ -18,3 +28,68 @@ def date_to_xml(date: datetime.datetime) -> float:
18
28
 
19
29
  def new_tdatetime():
20
30
  return TDateTime(date_to_xml(datetime.datetime.now(datetime.timezone.utc)))
31
+
32
+
33
+ def get_materials(
34
+ materials: Materials, country: str, include_outdated=False
35
+ ) -> Generator[Material, None, None]:
36
+ for material in materials.material:
37
+ if include_outdated:
38
+ yield material
39
+ continue
40
+ material_countries = getattr(
41
+ getattr(material.information, "countries", ()), "country", ()
42
+ )
43
+ if not material_countries:
44
+ yield material
45
+ continue
46
+ for material_country in material_countries:
47
+ if not material_country == country:
48
+ break
49
+ else:
50
+ continue
51
+ current_datetime = date_to_xml(datetime.datetime.now(tz=datetime.timezone.utc))
52
+ selling_from = material_country.sellingfrom
53
+ selling_until = getattr(material_country, "sellinguntil", None) or 10 ** 66
54
+ if selling_from < current_datetime and selling_until > current_datetime:
55
+ yield material
56
+
57
+
58
+ def get_material_name(material: Material, lang: str) -> TLocalizedString:
59
+ name = TLocalizedString("")
60
+ for name in material.information.names.name:
61
+ if name.lang == lang or not name.lang:
62
+ return name
63
+ return name
64
+
65
+
66
+ def get_material_description(material: Material, lang: str) -> TLocalizedString:
67
+ description = TLocalizedString("")
68
+ explanations = getattr(material.information, "explanations", None)
69
+ for description in getattr(explanations, "explanation", ()):
70
+ if description.lang == lang:
71
+ return description
72
+ return description
73
+
74
+
75
+ def get_material_webinfo(material: Material, lang: str) -> Webinfo:
76
+ webinfo = Webinfo(Mimetype(""), "", "", lang=ISO639_1(lang))
77
+ webinfos = getattr(material.information, "webinfos", None)
78
+ for webinfo in getattr(webinfos, "webinfo", ()):
79
+ if webinfo.lang == lang:
80
+ return webinfo
81
+ return webinfo
82
+
83
+
84
+ def get_material_layers(material: Material) -> Generator[Layer, None, None]:
85
+ for layer in getattr(getattr(material, "layers", ()), "layer", ()):
86
+ yield layer
87
+
88
+
89
+ def get_by_country(values, country):
90
+ for value in values:
91
+ if value.country == country:
92
+ return value
93
+ for value in values:
94
+ if value.country is None:
95
+ return value
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-materialsdb
3
- Version: 0.0.1
3
+ Version: 0.1.0
4
4
  Summary: A library to work with materialsdb.org open standard for building materials.
5
5
  Home-page: https://github.com/CyrilWaechter/python-materialsdb
6
6
  Author: Cyril Waechter
@@ -9,7 +9,6 @@ License: GPLv3+
9
9
  Project-URL: Bug Tracker, https://github.com/CyrilWaechter/python-materialsdb/issues
10
10
  Project-URL: Community, https://community.osarch.org/
11
11
  Keywords: materials,BIM,ifcopenshell,IFC
12
- Platform: UNKNOWN
13
12
  Classifier: Programming Language :: Python :: 3
14
13
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
15
14
  Classifier: Operating System :: OS Independent
@@ -19,7 +18,10 @@ Description-Content-Type: text/markdown
19
18
  License-File: LICENSE.md
20
19
  Requires-Dist: lxml
21
20
  Provides-Extra: ifc
22
- Requires-Dist: ifcopenshell ; extra == 'ifc'
21
+ Requires-Dist: ifcopenshell; extra == "ifc"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest-benchmark; extra == "dev"
24
+ Dynamic: license-file
23
25
 
24
26
  python-materialsdb is an unofficial python library for [materialsdb.org][1] an open format and database for building materials.
25
27
 
@@ -48,9 +50,21 @@ Note: in materialsdb standard languages are [ISO 639-1](https://en.wikipedia.org
48
50
 
49
51
  # Usage examples :
50
52
  Check out some [examples](examples):
51
- * [Convert lastest materials data to ifc](examples/generate_ifc_project_libraries.py)
53
+ * [Convert latest materials data to ifc](examples/generate_ifc_project_libraries.py)
52
54
  * [Create your own materialsdb.org compliant XML](examples/create_layers.py)
53
55
 
56
+ # Querying materials :
57
+ The library keeps an sqlite index of the cached materials data for fast
58
+ filtering and single-material access:
59
+
60
+ ```python
61
+ from materialsdb import query
62
+
63
+ query.refresh() # incremental update from cached xml
64
+ rows = query.search("isolant", sort="lambda") # filtered, sorted summaries
65
+ material = query.get_material(rows[0].id) # full material dataclass
66
+ ```
67
+
54
68
  # How to install
55
69
  ## Using pip
56
70
  ```bash
@@ -58,8 +72,8 @@ pip install python-materialsdb
58
72
  ```
59
73
 
60
74
  # Dependencies
61
- * [lxml][2] (BSD) : xml parser
62
- * [ifcopenshell][3] (LGPL) : ifc read/write
75
+ * [lxml][2] (BSD) : xml parser (tested with version 6.1.1)
76
+ * [ifcopenshell][3] (LGPL) : ifc read/write (tested with version 0.8.5)
63
77
 
64
78
  # Third parties :
65
79
  * [materialsdb.org][1] (GPL) : materials schema
@@ -67,4 +81,3 @@ pip install python-materialsdb
67
81
  [1]: http://www.materialsdb.org
68
82
  [2]: https://lxml.de
69
83
  [3]: ifcopenshell.org
70
-
@@ -0,0 +1,17 @@
1
+ materialsdb/__init__.py,sha256=2UuaCCmONhxIvpxB1eXHh88eb05i2ilR0-F0o6rKlCQ,63
2
+ materialsdb/cache.py,sha256=ags7_jiAwq5q2nKZZxoA7ybwF5sMVLSKpNe8Ps-iU0o,3386
3
+ materialsdb/classes.py,sha256=obuS_JRfdvqKJ8RMOx65X7AzI2J5W0xNgIXJ2y7evbM,21194
4
+ materialsdb/config.py,sha256=Bz16QTIl8u81g07www_zGdA-Ilsii_kbi3NYuSXlrlQ,1331
5
+ materialsdb/query.py,sha256=xoClMj0Xl7nQ_6DjETTc59gUYYaNH75lDje0BDwrxQQ,656
6
+ materialsdb/serialiser.py,sha256=_QENTdjoX1yeDFMfpUWtrC1-sN-Vusi5QpJL5l-Evck,6677
7
+ materialsdb/store.py,sha256=B5j99dqr5H1_dnTGceowAoLpFRq6CGG5w_fL_o-XGig,9734
8
+ materialsdb/summary.py,sha256=hBL8l27vKF7qgZXtYgA9O3WQRH3Ai-VZ43_PTTe-mjQ,2196
9
+ materialsdb/utils.py,sha256=jwJETjvBuHb6_K_7STsNaM-bE2Oc1X-th1xTrllW-Go,3081
10
+ materialsdb/ifc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ materialsdb/ifc/material_psets.json,sha256=g4Z-wOsnOP-TZR2R_9ikDpa9GalfyA8o3gDHvQtMAgg,15472
12
+ materialsdb/ifc/project_library.py,sha256=yZtI3qRHHDYib61CEVQb0G_X63_Ot_THqqljqUJY3DY,12202
13
+ python_materialsdb-0.1.0.dist-info/licenses/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
14
+ python_materialsdb-0.1.0.dist-info/METADATA,sha256=t-77t35f4VValTki4NbypXfZzcl-Ccli4aSACONPOO8,3112
15
+ python_materialsdb-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
16
+ python_materialsdb-0.1.0.dist-info/top_level.txt,sha256=5ZHbF8Oj1W24bifG65Fl3hCaivZpkO6EyOHHaRavZxE,12
17
+ python_materialsdb-0.1.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.36.2)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,14 +0,0 @@
1
- materialsdb/__init__.py,sha256=2UuaCCmONhxIvpxB1eXHh88eb05i2ilR0-F0o6rKlCQ,63
2
- materialsdb/cache.py,sha256=vS4Sb5zR0XHyzaTSLW7NEpl5XezF6yCBGgjekAbcOo0,2577
3
- materialsdb/classes.py,sha256=obuS_JRfdvqKJ8RMOx65X7AzI2J5W0xNgIXJ2y7evbM,21194
4
- materialsdb/config.py,sha256=Bz16QTIl8u81g07www_zGdA-Ilsii_kbi3NYuSXlrlQ,1331
5
- materialsdb/serialiser.py,sha256=57haSVTFbRC_srvd-ZBQXLMbPE1RIQuYlF6J6pFdd80,5860
6
- materialsdb/utils.py,sha256=w3RXikkM0TUFlgHuZyADbckIyTNNsu3JWoYTflvPyr0,703
7
- materialsdb/ifc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- materialsdb/ifc/material_psets.json,sha256=g4Z-wOsnOP-TZR2R_9ikDpa9GalfyA8o3gDHvQtMAgg,15472
9
- materialsdb/ifc/project_library.py,sha256=N5kHW4UJ7LNJeULDAeH_W35CUsAuBhOJNtN293pLHb0,13782
10
- python_materialsdb-0.0.1.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
11
- python_materialsdb-0.0.1.dist-info/METADATA,sha256=Iyzs9-sXPmOeOkaOH6bDtanjJitpkNAOlU136d4vbro,2565
12
- python_materialsdb-0.0.1.dist-info/WHEEL,sha256=OqRkF0eY5GHssMorFjlbTIq072vpHpF60fIQA6lS9xA,92
13
- python_materialsdb-0.0.1.dist-info/top_level.txt,sha256=5ZHbF8Oj1W24bifG65Fl3hCaivZpkO6EyOHHaRavZxE,12
14
- python_materialsdb-0.0.1.dist-info/RECORD,,