python-materialsdb 0.0.2__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
@@ -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
- new_index = etree.parse(index)
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
@@ -209,7 +209,7 @@ class ProjectLibrary:
209
209
  ifcopenshell.api.run(
210
210
  "material.assign_material",
211
211
  file,
212
- product=wall,
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
- product=roof,
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
- product=slab,
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
- product=door,
248
+ products=[door],
249
249
  material=assigned_material,
250
250
  )
251
251
 
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,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 = 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
+ )
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-materialsdb
3
- Version: 0.0.2
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
@@ -18,7 +18,10 @@ Description-Content-Type: text/markdown
18
18
  License-File: LICENSE.md
19
19
  Requires-Dist: lxml
20
20
  Provides-Extra: ifc
21
- 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
22
25
 
23
26
  python-materialsdb is an unofficial python library for [materialsdb.org][1] an open format and database for building materials.
24
27
 
@@ -47,9 +50,21 @@ Note: in materialsdb standard languages are [ISO 639-1](https://en.wikipedia.org
47
50
 
48
51
  # Usage examples :
49
52
  Check out some [examples](examples):
50
- * [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)
51
54
  * [Create your own materialsdb.org compliant XML](examples/create_layers.py)
52
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
+
53
68
  # How to install
54
69
  ## Using pip
55
70
  ```bash
@@ -57,8 +72,8 @@ pip install python-materialsdb
57
72
  ```
58
73
 
59
74
  # Dependencies
60
- * [lxml][2] (BSD) : xml parser
61
- * [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)
62
77
 
63
78
  # Third parties :
64
79
  * [materialsdb.org][1] (GPL) : materials schema
@@ -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.40.0)
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=4YYaX4cyp-zahhdd5ejppk8_GZ3exwEb6GUeww2wmpQ,3327
3
- materialsdb/classes.py,sha256=obuS_JRfdvqKJ8RMOx65X7AzI2J5W0xNgIXJ2y7evbM,21194
4
- materialsdb/config.py,sha256=Bz16QTIl8u81g07www_zGdA-Ilsii_kbi3NYuSXlrlQ,1331
5
- materialsdb/serialiser.py,sha256=27W9W2ux_00F5QPCUWFVwNcQJnrGy3loCRMXjm2x1Qk,5881
6
- materialsdb/utils.py,sha256=jwJETjvBuHb6_K_7STsNaM-bE2Oc1X-th1xTrllW-Go,3081
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=3H9LzIIpJEiygtx5ar8BFA-MkJy4n8F-TQ_uXX6PlK4,12190
10
- python_materialsdb-0.0.2.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
11
- python_materialsdb-0.0.2.dist-info/METADATA,sha256=MneyvAMo67vbqky_7fwLrFvc4KU91vGArYg7SjxNRo0,2546
12
- python_materialsdb-0.0.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
13
- python_materialsdb-0.0.2.dist-info/top_level.txt,sha256=5ZHbF8Oj1W24bifG65Fl3hCaivZpkO6EyOHHaRavZxE,12
14
- python_materialsdb-0.0.2.dist-info/RECORD,,