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 +42 -17
- materialsdb/ifc/project_library.py +13 -64
- materialsdb/query.py +24 -0
- materialsdb/serialiser.py +25 -2
- materialsdb/store.py +290 -0
- materialsdb/summary.py +75 -0
- materialsdb/utils.py +76 -1
- {python_materialsdb-0.0.1.dist-info → python_materialsdb-0.1.0.dist-info}/METADATA +21 -8
- python_materialsdb-0.1.0.dist-info/RECORD +17 -0
- {python_materialsdb-0.0.1.dist-info → python_materialsdb-0.1.0.dist-info}/WHEEL +1 -1
- python_materialsdb-0.0.1.dist-info/RECORD +0 -14
- {python_materialsdb-0.0.1.dist-info → python_materialsdb-0.1.0.dist-info/licenses}/LICENSE.md +0 -0
- {python_materialsdb-0.0.1.dist-info → python_materialsdb-0.1.0.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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() /
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
71
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-materialsdb
|
|
3
|
-
Version: 0.0
|
|
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
|
|
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
|
|
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,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,,
|
{python_materialsdb-0.0.1.dist-info → python_materialsdb-0.1.0.dist-info/licenses}/LICENSE.md
RENAMED
|
File without changes
|
|
File without changes
|