python-materialsdb 0.0.2__tar.gz → 0.1.0__tar.gz
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.
- {python-materialsdb-0.0.2/src/python_materialsdb.egg-info → python_materialsdb-0.1.0}/PKG-INFO +23 -6
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/README.md +15 -3
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/pyproject.toml +5 -1
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/setup.cfg +2 -1
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/materialsdb/cache.py +2 -1
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/materialsdb/ifc/project_library.py +4 -4
- python_materialsdb-0.1.0/src/materialsdb/query.py +24 -0
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/materialsdb/serialiser.py +24 -1
- python_materialsdb-0.1.0/src/materialsdb/store.py +290 -0
- python_materialsdb-0.1.0/src/materialsdb/summary.py +75 -0
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0/src/python_materialsdb.egg-info}/PKG-INFO +23 -6
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/python_materialsdb.egg-info/SOURCES.txt +7 -1
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/python_materialsdb.egg-info/requires.txt +3 -0
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/tests/test_project_library.py +8 -0
- python_materialsdb-0.1.0/tests/test_query.py +34 -0
- python_materialsdb-0.1.0/tests/test_serialiser.py +65 -0
- python_materialsdb-0.1.0/tests/test_store.py +109 -0
- python_materialsdb-0.1.0/tests/test_summary.py +41 -0
- python-materialsdb-0.0.2/tests/test_serialiser.py +0 -13
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/LICENSE.md +0 -0
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/materialsdb/__init__.py +0 -0
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/materialsdb/classes.py +0 -0
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/materialsdb/config.py +0 -0
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/materialsdb/ifc/__init__.py +0 -0
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/materialsdb/ifc/material_psets.json +0 -0
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/materialsdb/utils.py +0 -0
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/python_materialsdb.egg-info/dependency_links.txt +0 -0
- {python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/python_materialsdb.egg-info/top_level.txt +0 -0
{python-materialsdb-0.0.2/src/python_materialsdb.egg-info → python_materialsdb-0.1.0}/PKG-INFO
RENAMED
|
@@ -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
|
|
@@ -15,8 +15,13 @@ Classifier: Operating System :: OS Independent
|
|
|
15
15
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
16
|
Classifier: Topic :: Scientific/Engineering
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
-
Provides-Extra: ifc
|
|
19
18
|
License-File: LICENSE.md
|
|
19
|
+
Requires-Dist: lxml
|
|
20
|
+
Provides-Extra: ifc
|
|
21
|
+
Requires-Dist: ifcopenshell; extra == "ifc"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest-benchmark; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
20
25
|
|
|
21
26
|
python-materialsdb is an unofficial python library for [materialsdb.org][1] an open format and database for building materials.
|
|
22
27
|
|
|
@@ -45,9 +50,21 @@ Note: in materialsdb standard languages are [ISO 639-1](https://en.wikipedia.org
|
|
|
45
50
|
|
|
46
51
|
# Usage examples :
|
|
47
52
|
Check out some [examples](examples):
|
|
48
|
-
* [Convert
|
|
53
|
+
* [Convert latest materials data to ifc](examples/generate_ifc_project_libraries.py)
|
|
49
54
|
* [Create your own materialsdb.org compliant XML](examples/create_layers.py)
|
|
50
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
|
+
|
|
51
68
|
# How to install
|
|
52
69
|
## Using pip
|
|
53
70
|
```bash
|
|
@@ -55,8 +72,8 @@ pip install python-materialsdb
|
|
|
55
72
|
```
|
|
56
73
|
|
|
57
74
|
# Dependencies
|
|
58
|
-
* [lxml][2] (BSD) : xml parser
|
|
59
|
-
* [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)
|
|
60
77
|
|
|
61
78
|
# Third parties :
|
|
62
79
|
* [materialsdb.org][1] (GPL) : materials schema
|
|
@@ -25,9 +25,21 @@ Note: in materialsdb standard languages are [ISO 639-1](https://en.wikipedia.org
|
|
|
25
25
|
|
|
26
26
|
# Usage examples :
|
|
27
27
|
Check out some [examples](examples):
|
|
28
|
-
* [Convert
|
|
28
|
+
* [Convert latest materials data to ifc](examples/generate_ifc_project_libraries.py)
|
|
29
29
|
* [Create your own materialsdb.org compliant XML](examples/create_layers.py)
|
|
30
30
|
|
|
31
|
+
# Querying materials :
|
|
32
|
+
The library keeps an sqlite index of the cached materials data for fast
|
|
33
|
+
filtering and single-material access:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from materialsdb import query
|
|
37
|
+
|
|
38
|
+
query.refresh() # incremental update from cached xml
|
|
39
|
+
rows = query.search("isolant", sort="lambda") # filtered, sorted summaries
|
|
40
|
+
material = query.get_material(rows[0].id) # full material dataclass
|
|
41
|
+
```
|
|
42
|
+
|
|
31
43
|
# How to install
|
|
32
44
|
## Using pip
|
|
33
45
|
```bash
|
|
@@ -35,8 +47,8 @@ pip install python-materialsdb
|
|
|
35
47
|
```
|
|
36
48
|
|
|
37
49
|
# Dependencies
|
|
38
|
-
* [lxml][2] (BSD) : xml parser
|
|
39
|
-
* [ifcopenshell][3] (LGPL) : ifc read/write
|
|
50
|
+
* [lxml][2] (BSD) : xml parser (tested with version 6.1.1)
|
|
51
|
+
* [ifcopenshell][3] (LGPL) : ifc read/write (tested with version 0.8.5)
|
|
40
52
|
|
|
41
53
|
# Third parties :
|
|
42
54
|
* [materialsdb.org][1] (GPL) : materials schema
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = python-materialsdb
|
|
3
|
-
version = 0.0
|
|
3
|
+
version = 0.1.0
|
|
4
4
|
author = Cyril Waechter
|
|
5
5
|
author_email = cyrwae@hotmail.com
|
|
6
6
|
description = A library to work with materialsdb.org open standard for building materials.
|
|
@@ -35,6 +35,7 @@ where = src
|
|
|
35
35
|
|
|
36
36
|
[options.extras_require]
|
|
37
37
|
ifc = ifcopenshell
|
|
38
|
+
dev = pytest-benchmark
|
|
38
39
|
|
|
39
40
|
[egg_info]
|
|
40
41
|
tag_build =
|
|
@@ -75,7 +75,8 @@ def update_producers_data(url_list=MATERIALSDBINDEXURLLIST):
|
|
|
75
75
|
def update_producers_from_index(index):
|
|
76
76
|
cached_index = parse_cached_index(index)
|
|
77
77
|
cached_root = cached_index.getroot()
|
|
78
|
-
|
|
78
|
+
with urllib.request.urlopen(index) as response:
|
|
79
|
+
new_index = etree.parse(response)
|
|
79
80
|
new_root = new_index.getroot()
|
|
80
81
|
producers_dir = get_producers_dir()
|
|
81
82
|
has_index_update = False
|
{python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/materialsdb/ifc/project_library.py
RENAMED
|
@@ -209,7 +209,7 @@ class ProjectLibrary:
|
|
|
209
209
|
ifcopenshell.api.run(
|
|
210
210
|
"material.assign_material",
|
|
211
211
|
file,
|
|
212
|
-
|
|
212
|
+
products=[wall],
|
|
213
213
|
material=assigned_material,
|
|
214
214
|
)
|
|
215
215
|
if material.information.roof:
|
|
@@ -221,7 +221,7 @@ class ProjectLibrary:
|
|
|
221
221
|
ifcopenshell.api.run(
|
|
222
222
|
"material.assign_material",
|
|
223
223
|
file,
|
|
224
|
-
|
|
224
|
+
products=[roof],
|
|
225
225
|
material=assigned_material,
|
|
226
226
|
)
|
|
227
227
|
if material.information.floor:
|
|
@@ -233,7 +233,7 @@ class ProjectLibrary:
|
|
|
233
233
|
ifcopenshell.api.run(
|
|
234
234
|
"material.assign_material",
|
|
235
235
|
file,
|
|
236
|
-
|
|
236
|
+
products=[slab],
|
|
237
237
|
material=assigned_material,
|
|
238
238
|
)
|
|
239
239
|
if material.information.door:
|
|
@@ -245,7 +245,7 @@ class ProjectLibrary:
|
|
|
245
245
|
ifcopenshell.api.run(
|
|
246
246
|
"material.assign_material",
|
|
247
247
|
file,
|
|
248
|
-
|
|
248
|
+
products=[door],
|
|
249
249
|
material=assigned_material,
|
|
250
250
|
)
|
|
251
251
|
|
|
@@ -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)
|
|
@@ -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,6 +52,11 @@ 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())
|
|
@@ -62,13 +69,29 @@ class XmlDeserialiser:
|
|
|
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:
|
|
@@ -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)
|
|
@@ -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
|
+
)
|
{python-materialsdb-0.0.2 → python_materialsdb-0.1.0/src/python_materialsdb.egg-info}/PKG-INFO
RENAMED
|
@@ -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
|
|
@@ -15,8 +15,13 @@ Classifier: Operating System :: OS Independent
|
|
|
15
15
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
16
|
Classifier: Topic :: Scientific/Engineering
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
-
Provides-Extra: ifc
|
|
19
18
|
License-File: LICENSE.md
|
|
19
|
+
Requires-Dist: lxml
|
|
20
|
+
Provides-Extra: ifc
|
|
21
|
+
Requires-Dist: ifcopenshell; extra == "ifc"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest-benchmark; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
20
25
|
|
|
21
26
|
python-materialsdb is an unofficial python library for [materialsdb.org][1] an open format and database for building materials.
|
|
22
27
|
|
|
@@ -45,9 +50,21 @@ Note: in materialsdb standard languages are [ISO 639-1](https://en.wikipedia.org
|
|
|
45
50
|
|
|
46
51
|
# Usage examples :
|
|
47
52
|
Check out some [examples](examples):
|
|
48
|
-
* [Convert
|
|
53
|
+
* [Convert latest materials data to ifc](examples/generate_ifc_project_libraries.py)
|
|
49
54
|
* [Create your own materialsdb.org compliant XML](examples/create_layers.py)
|
|
50
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
|
+
|
|
51
68
|
# How to install
|
|
52
69
|
## Using pip
|
|
53
70
|
```bash
|
|
@@ -55,8 +72,8 @@ pip install python-materialsdb
|
|
|
55
72
|
```
|
|
56
73
|
|
|
57
74
|
# Dependencies
|
|
58
|
-
* [lxml][2] (BSD) : xml parser
|
|
59
|
-
* [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)
|
|
60
77
|
|
|
61
78
|
# Third parties :
|
|
62
79
|
* [materialsdb.org][1] (GPL) : materials schema
|
{python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/python_materialsdb.egg-info/SOURCES.txt
RENAMED
|
@@ -6,7 +6,10 @@ src/materialsdb/__init__.py
|
|
|
6
6
|
src/materialsdb/cache.py
|
|
7
7
|
src/materialsdb/classes.py
|
|
8
8
|
src/materialsdb/config.py
|
|
9
|
+
src/materialsdb/query.py
|
|
9
10
|
src/materialsdb/serialiser.py
|
|
11
|
+
src/materialsdb/store.py
|
|
12
|
+
src/materialsdb/summary.py
|
|
10
13
|
src/materialsdb/utils.py
|
|
11
14
|
src/materialsdb/ifc/__init__.py
|
|
12
15
|
src/materialsdb/ifc/material_psets.json
|
|
@@ -17,4 +20,7 @@ src/python_materialsdb.egg-info/dependency_links.txt
|
|
|
17
20
|
src/python_materialsdb.egg-info/requires.txt
|
|
18
21
|
src/python_materialsdb.egg-info/top_level.txt
|
|
19
22
|
tests/test_project_library.py
|
|
20
|
-
tests/
|
|
23
|
+
tests/test_query.py
|
|
24
|
+
tests/test_serialiser.py
|
|
25
|
+
tests/test_store.py
|
|
26
|
+
tests/test_summary.py
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
1
5
|
from materialsdb.ifc import project_library
|
|
2
6
|
|
|
3
7
|
|
|
8
|
+
@pytest.mark.skipif(
|
|
9
|
+
not Path("example_v103.xml").exists(),
|
|
10
|
+
reason="example_v103.xml not present locally",
|
|
11
|
+
)
|
|
4
12
|
def test_create_project_library():
|
|
5
13
|
file = project_library.create_project_library_from_xml("example_v103.xml")
|
|
6
14
|
file.write("example_v103.ifc")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from materialsdb import query
|
|
4
|
+
from materialsdb.store import MaterialStore
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture(autouse=True)
|
|
8
|
+
def pinned_fr_ch_config(monkeypatch):
|
|
9
|
+
monkeypatch.setattr("materialsdb.config.get_lang", lambda: "fr")
|
|
10
|
+
monkeypatch.setattr("materialsdb.config.get_country", lambda: "CH")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def isolated_store(tmp_path, monkeypatch, mini_xml):
|
|
15
|
+
query.get_store.cache_clear()
|
|
16
|
+
monkeypatch.setattr(
|
|
17
|
+
"materialsdb.query.MaterialStore",
|
|
18
|
+
lambda: MaterialStore(db_path=tmp_path / "q.db"),
|
|
19
|
+
)
|
|
20
|
+
store = query.get_store()
|
|
21
|
+
store.refresh(paths=[mini_xml])
|
|
22
|
+
yield store
|
|
23
|
+
store.close()
|
|
24
|
+
query.get_store.cache_clear()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_get_material(isolated_store):
|
|
28
|
+
material = query.get_material("00000000-0000-0000-0000-000000000002")
|
|
29
|
+
assert material.information.group == "Concrete"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_search(isolated_store):
|
|
33
|
+
rows = query.search("beton")
|
|
34
|
+
assert len(rows) == 1
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
sys.path.append(str(Path(__file__).parent.parent / "src"))
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from materialsdb.serialiser import XmlDeserialiser, XmlSerialiser
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.skipif(
|
|
11
|
+
not Path("example_v103.xml").exists(),
|
|
12
|
+
reason="example_v103.xml not present locally",
|
|
13
|
+
)
|
|
14
|
+
def test_deserialise_and_serialise():
|
|
15
|
+
xml_path = "example_v103.xml"
|
|
16
|
+
deserialiser = XmlDeserialiser()
|
|
17
|
+
source = deserialiser.from_xml(xml_path)
|
|
18
|
+
serialiser = XmlSerialiser()
|
|
19
|
+
serialiser.to_xml(source, xml_path="test.xml")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_mini_fixture_roundtrip(tmp_path, mini_source):
|
|
23
|
+
assert len(mini_source.material) == 3
|
|
24
|
+
assert mini_source.company == "Mini SA"
|
|
25
|
+
out = tmp_path / "roundtrip.xml"
|
|
26
|
+
XmlSerialiser().to_xml(mini_source, xml_path=str(out))
|
|
27
|
+
reparsed = XmlDeserialiser().from_xml(str(out))
|
|
28
|
+
assert len(reparsed.material) == 3
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.skipif(
|
|
32
|
+
not Path("example_v103.xml").exists(),
|
|
33
|
+
reason="example_v103.xml not present locally",
|
|
34
|
+
)
|
|
35
|
+
def test_type_hints_are_cached():
|
|
36
|
+
from materialsdb import serialiser
|
|
37
|
+
|
|
38
|
+
serialiser.cached_type_hints.cache_clear()
|
|
39
|
+
deserialiser = XmlDeserialiser()
|
|
40
|
+
deserialiser.from_xml("example_v103.xml")
|
|
41
|
+
hits_after_first = serialiser.cached_type_hints.cache_info().hits
|
|
42
|
+
deserialiser.from_xml("example_v103.xml")
|
|
43
|
+
assert serialiser.cached_type_hints.cache_info().hits > hits_after_first
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
import shutil
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_from_xml_files_reports_corrupt_and_parses_rest(tmp_path, mini_xml):
|
|
50
|
+
good_a = tmp_path / "a.xml"
|
|
51
|
+
good_b = tmp_path / "b.xml"
|
|
52
|
+
bad = tmp_path / "bad.xml"
|
|
53
|
+
shutil.copy(mini_xml, good_a)
|
|
54
|
+
shutil.copy(mini_xml, good_b)
|
|
55
|
+
bad.write_text("<materials><unclosed>", encoding="utf-8")
|
|
56
|
+
|
|
57
|
+
results = {
|
|
58
|
+
path.name: source
|
|
59
|
+
for path, source in XmlDeserialiser().from_xml_files([good_a, good_b, bad])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
assert results["a.xml"] is not None
|
|
63
|
+
assert len(results["a.xml"].material) == 3
|
|
64
|
+
assert results["b.xml"] is not None
|
|
65
|
+
assert results["bad.xml"] is None
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from materialsdb.store import MaterialStore
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture(autouse=True)
|
|
7
|
+
def pinned_fr_ch_config(monkeypatch):
|
|
8
|
+
monkeypatch.setattr("materialsdb.config.get_lang", lambda: "fr")
|
|
9
|
+
monkeypatch.setattr("materialsdb.config.get_country", lambda: "CH")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def store(tmp_path, mini_xml):
|
|
14
|
+
s = MaterialStore(db_path=tmp_path / "test.db")
|
|
15
|
+
s.refresh(paths=[mini_xml])
|
|
16
|
+
yield s
|
|
17
|
+
s.close()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_refresh_populates_all_materials(store):
|
|
21
|
+
assert len(store.summaries()) == 3
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_get_returns_full_material_equivalent_to_direct_parse(store, mini_source):
|
|
25
|
+
material = store.get("00000000-0000-0000-0000-000000000001")
|
|
26
|
+
original = mini_source.material[0]
|
|
27
|
+
assert str(material.information.names.name[0]) == str(
|
|
28
|
+
original.information.names.name[0]
|
|
29
|
+
)
|
|
30
|
+
assert material.information.group == "Insulation"
|
|
31
|
+
assert material.layers.layer[0].thermal[0].lambda_value == 0.036
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_get_unknown_id_returns_none(store):
|
|
35
|
+
assert store.get("nope") is None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_filters(store):
|
|
39
|
+
insulation = store.summaries(category="Insulation")
|
|
40
|
+
assert len(insulation) == 1
|
|
41
|
+
assert insulation[0].id.endswith("001")
|
|
42
|
+
|
|
43
|
+
assert len(store.summaries(company="Mini SA")) == 3
|
|
44
|
+
|
|
45
|
+
low_lambda = store.summaries(max_lambda=0.1)
|
|
46
|
+
assert {s.id[-3:] for s in low_lambda} == {"001"}
|
|
47
|
+
|
|
48
|
+
walls = store.summaries(usage="wall")
|
|
49
|
+
assert len(walls) == 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_sort_order_and_nulls_last(store):
|
|
53
|
+
rows = store.summaries(sort="lambda")
|
|
54
|
+
assert [r.lambda_min for r in rows] == [0.036, 0.21, None]
|
|
55
|
+
|
|
56
|
+
rows = store.summaries(sort="lambda", ascending=False)
|
|
57
|
+
assert [r.lambda_min for r in rows] == [0.21, 0.036, None]
|
|
58
|
+
|
|
59
|
+
rows = store.summaries(sort="thick", ascending=False)
|
|
60
|
+
assert [r.thick_min for r in rows] == [150, 100, None]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_text_filter_uses_configured_lang(monkeypatch, store):
|
|
64
|
+
monkeypatch.setattr("materialsdb.config.get_lang", lambda: "fr")
|
|
65
|
+
rows = store.summaries(text="isol")
|
|
66
|
+
assert len(rows) == 1
|
|
67
|
+
assert rows[0].id.endswith("001")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_refresh_is_incremental(store, mini_xml):
|
|
71
|
+
report = store.refresh(paths=[mini_xml])
|
|
72
|
+
assert report.updated == []
|
|
73
|
+
assert len(report.existing) == 1
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_refresh_rebuilds_changed_file(store, mini_xml, tmp_path):
|
|
77
|
+
copy = tmp_path / "changed.xml"
|
|
78
|
+
copy.write_text(
|
|
79
|
+
mini_xml.read_text(encoding="utf-8").replace("0.036", "0.03"),
|
|
80
|
+
encoding="utf-8",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
report = store.refresh(paths=[copy])
|
|
84
|
+
assert copy in report.updated
|
|
85
|
+
rows = store.summaries(min_lambda=0.03, max_lambda=0.1)
|
|
86
|
+
assert len(rows) == 1
|
|
87
|
+
assert rows[0].lambda_min == 0.03
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_deleted_producer_rows_are_removed(store):
|
|
91
|
+
store.refresh(paths=[]) # declare current file set empty
|
|
92
|
+
assert store.summaries() == []
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_corrupt_producer_is_skipped(store, tmp_path, mini_xml):
|
|
96
|
+
bad = tmp_path / "bad.xml"
|
|
97
|
+
bad.write_text("<materials>", encoding="utf-8")
|
|
98
|
+
report = store.refresh(force=True, paths=[mini_xml, bad])
|
|
99
|
+
assert len(report.existing) + len(report.updated) == 1
|
|
100
|
+
assert report.skipped == [bad]
|
|
101
|
+
assert len(store.summaries()) == 3
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_nonexistent_producer_is_skipped_without_raising(store, tmp_path, mini_xml):
|
|
105
|
+
ghost = tmp_path / "ghost.xml"
|
|
106
|
+
report = store.refresh(paths=[mini_xml, ghost])
|
|
107
|
+
assert report.existing == [mini_xml]
|
|
108
|
+
assert report.updated == []
|
|
109
|
+
assert len(store.summaries()) == 3
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from materialsdb.summary import summarize_material
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_insulation_material_summary_ch(mini_source):
|
|
5
|
+
material = mini_source.material[0]
|
|
6
|
+
s = summarize_material(material, country="CH")
|
|
7
|
+
|
|
8
|
+
assert s.id == "00000000-0000-0000-0000-000000000001"
|
|
9
|
+
assert s.category == "Insulation"
|
|
10
|
+
assert s.names == {"fr": "Isolant A", "de": "Daemmstoff A", "": "Material A"}
|
|
11
|
+
assert s.descriptions == {"fr": "Panneau isolant", "de": "Daemmplatte"}
|
|
12
|
+
assert s.usage["wall"] is True
|
|
13
|
+
assert s.usage["roof"] is False
|
|
14
|
+
assert s.thick_min == 100
|
|
15
|
+
assert s.thick_max == 200
|
|
16
|
+
assert s.lambda_min == 0.036
|
|
17
|
+
assert s.lambda_max == 0.05
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_french_country_picks_fr_values(mini_source):
|
|
21
|
+
s = summarize_material(mini_source.material[0], country="FR")
|
|
22
|
+
assert s.lambda_min == 0.04
|
|
23
|
+
assert s.lambda_max == 0.05
|
|
24
|
+
assert s.thick_min == 100
|
|
25
|
+
assert s.thick_max == 240
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_concrete_single_layer(mini_source):
|
|
29
|
+
s = summarize_material(mini_source.material[1], country="CH")
|
|
30
|
+
assert s.category == "Concrete"
|
|
31
|
+
assert s.names == {"fr": "Beton B"}
|
|
32
|
+
assert s.descriptions == {}
|
|
33
|
+
assert s.lambda_min == s.lambda_max == 0.21
|
|
34
|
+
assert s.thick_min == s.thick_max == 150
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_material_without_layers_has_none_metrics(mini_source):
|
|
38
|
+
s = summarize_material(mini_source.material[2])
|
|
39
|
+
assert s.lambda_min is None
|
|
40
|
+
assert s.thick_max is None
|
|
41
|
+
assert s.usage == {"wall": False, "roof": False, "floor": False, "door": False}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
sys.path.append(str(Path(__file__).parent.parent / "src"))
|
|
5
|
-
from materialsdb.serialiser import XmlDeserialiser, XmlSerialiser
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_deserialise_and_serialise():
|
|
9
|
-
xml_path = "example_v103.xml"
|
|
10
|
-
deserialiser = XmlDeserialiser()
|
|
11
|
-
source = deserialiser.from_xml(xml_path)
|
|
12
|
-
serialiser = XmlSerialiser()
|
|
13
|
-
serialiser.to_xml(source, xml_path="test.xml")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/materialsdb/ifc/material_psets.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python-materialsdb-0.0.2 → python_materialsdb-0.1.0}/src/python_materialsdb.egg-info/top_level.txt
RENAMED
|
File without changes
|