python-materialsdb 0.3.0__tar.gz → 0.3.1__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.
Files changed (54) hide show
  1. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/PKG-INFO +1 -1
  2. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/pyproject.toml +1 -1
  3. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/cache.py +69 -16
  4. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/serialiser.py +27 -12
  5. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/python_materialsdb.egg-info/PKG-INFO +1 -1
  6. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_build_bonsai_addon.py +16 -2
  7. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_cache.py +11 -8
  8. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/LICENSE.md +0 -0
  9. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/README.md +0 -0
  10. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/setup.cfg +0 -0
  11. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/__init__.py +0 -0
  12. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/classes.py +0 -0
  13. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/config.py +0 -0
  14. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/construction.py +0 -0
  15. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/gui/__init__.py +0 -0
  16. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/gui/__main__.py +0 -0
  17. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/gui/discovery.py +0 -0
  18. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/gui/listener.py +0 -0
  19. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/gui/server.py +0 -0
  20. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/gui/static/app-constructions.js +0 -0
  21. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/gui/static/app.js +0 -0
  22. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/gui/static/constructions.html +0 -0
  23. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/gui/static/index.html +0 -0
  24. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/gui/static/picker-core.js +0 -0
  25. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/ifc/__init__.py +0 -0
  26. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/ifc/material_builder.py +0 -0
  27. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/ifc/material_psets.json +0 -0
  28. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/ifc/project_library.py +0 -0
  29. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/query.py +0 -0
  30. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/schema/MaterialsDBIndex100.xsd +0 -0
  31. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/schema/materialsdb102.xsd +0 -0
  32. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/schema/materialsdb103.xsd +0 -0
  33. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/store.py +0 -0
  34. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/summary.py +0 -0
  35. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/materialsdb/utils.py +0 -0
  36. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/python_materialsdb.egg-info/SOURCES.txt +0 -0
  37. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/python_materialsdb.egg-info/dependency_links.txt +0 -0
  38. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/python_materialsdb.egg-info/entry_points.txt +0 -0
  39. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/python_materialsdb.egg-info/requires.txt +0 -0
  40. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/src/python_materialsdb.egg-info/top_level.txt +0 -0
  41. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_bonsai_discovery.py +0 -0
  42. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_bonsai_insert.py +0 -0
  43. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_construction.py +0 -0
  44. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_construction_frontend.py +0 -0
  45. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_construction_ifc.py +0 -0
  46. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_gui_server.py +0 -0
  47. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_listener_payload.py +0 -0
  48. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_material_builder.py +0 -0
  49. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_picker_frontend.py +0 -0
  50. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_project_library.py +0 -0
  51. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_query.py +0 -0
  52. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_serialiser.py +0 -0
  53. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_store.py +0 -0
  54. {python_materialsdb-0.3.0 → python_materialsdb-0.3.1}/tests/test_summary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-materialsdb
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: A library to work with materialsdb.org open standard for building materials.
5
5
  Author-email: Cyril Waechter <cyrwae@hotmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-materialsdb"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "A library to work with materialsdb.org open standard for building materials."
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
@@ -2,6 +2,7 @@ import os
2
2
  import pathlib
3
3
  import urllib.request
4
4
  from collections import namedtuple
5
+ from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
5
6
 
6
7
  from lxml import etree
7
8
 
@@ -85,25 +86,77 @@ def _plan_index(index):
85
86
  return _IndexPlan(index, new_index, unchanged, todos)
86
87
 
87
88
 
88
- def update_producers_data(url_list=MATERIALSDBINDEXURLLIST, on_progress=None):
89
- """on_progress(done, total, name) is called after each producer download
90
- with a global running total; returning a truthy value stops the update
91
- between two downloads (the partial Report is returned as-is)."""
89
+ def update_producers_data(url_list=MATERIALSDBINDEXURLLIST, on_progress=None, max_workers: int = 8):
90
+ """Update the cached producer files from the index URLs.
91
+
92
+ Downloads run on a bounded thread pool (max_workers). on_progress(done,
93
+ total, name) is called once per completed download from the calling
94
+ thread with a monotonic running total; returning a truthy value stops
95
+ NEW submissions (queued jobs are abandoned, in-flight ones finish but
96
+ no longer count toward the partial Report). A download error stops the
97
+ run and the first exception is re-raised after the pool drains.
98
+ """
92
99
  plans = [_plan_index(index) for index in url_list]
93
- total = sum(len(plan.todos) for plan in plans)
94
- existing, updated, deleted = [], [], []
100
+ jobs = [
101
+ (plan, company, cached_producer, producer_path)
102
+ for plan in plans
103
+ for (company, cached_producer, producer_path) in plan.todos
104
+ ]
105
+ total = len(jobs)
106
+ workers = max(1, min(int(max_workers), total or 1))
107
+
108
+ existing: list = []
109
+ updated: list = []
110
+ deleted: list = []
95
111
  done = 0
112
+ cancel_requested = False
113
+ failure = None
114
+
115
+ def _download(job):
116
+ _, company, cached_producer, producer_path = job
117
+ deleted_path = None
118
+ if cached_producer is not None:
119
+ deleted_path = get_producers_dir() / pathlib.Path(cached_producer.get("href")).name
120
+ deleted_path.unlink(True)
121
+ urllib.request.urlretrieve(company.get("href"), producer_path)
122
+ return deleted_path
123
+
124
+ pool = ThreadPoolExecutor(max_workers=workers)
125
+ pending = list(jobs) # pops from the FRONT, submission order preserved
126
+ futures: dict = {}
127
+ try:
128
+ while futures or pending:
129
+ while pending and len(futures) < workers:
130
+ job = pending.pop(0)
131
+ futures[pool.submit(_download, job)] = job
132
+ if not futures:
133
+ break
134
+ ready, _ = wait(set(futures), return_when=FIRST_COMPLETED)
135
+ for future in ready:
136
+ job = futures.pop(future)
137
+ try:
138
+ deleted_path = future.result()
139
+ except Exception as err: # noqa: BLE001 - re-raised below, caller decides
140
+ if failure is None:
141
+ failure = err
142
+ break
143
+ if deleted_path is not None:
144
+ deleted.append(deleted_path)
145
+ updated.append(job[3])
146
+ done += 1
147
+ if on_progress is not None and on_progress(done, total, pathlib.Path(job[3]).name):
148
+ cancel_requested = True
149
+ break
150
+ if cancel_requested or failure is not None:
151
+ break
152
+ finally:
153
+ pool.shutdown(wait=True, cancel_futures=True)
154
+
155
+ if failure is not None:
156
+ raise failure
157
+ if cancel_requested:
158
+ return Report(existing, updated, deleted)
96
159
  for plan in plans:
97
- for company, cached_producer, producer_path in plan.todos:
98
- if cached_producer is not None:
99
- cached_path = get_producers_dir() / pathlib.Path(cached_producer.get("href")).name
100
- deleted.append(cached_path)
101
- cached_path.unlink(True)
102
- urllib.request.urlretrieve(company.get("href"), producer_path)
103
- updated.append(producer_path)
104
- done += 1
105
- if on_progress is not None and on_progress(done, total, pathlib.Path(company.get("href")).name):
106
- return Report(existing, updated, deleted)
107
160
  if plan.todos:
108
161
  plan.new_index.write(str(get_cached_index_path(plan.index)))
109
162
  existing.extend(plan.unchanged)
@@ -25,9 +25,31 @@ def get_xml_schema() -> str:
25
25
  return str(Path(__file__).parent / "schema/materialsdb103.xsd")
26
26
 
27
27
 
28
+ @cache
29
+ def cached_type_hints(cls) -> dict:
30
+ return typing.get_type_hints(cls)
31
+
32
+
33
+ @cache
34
+ def _tag_local_name(tag: str) -> str:
35
+ m = re.search("{.*}(.*)", tag)
36
+ return m.group(1) if m else tag
37
+
38
+
28
39
  def get_element_name(element: objectify.ObjectifiedElement) -> str:
29
- m = re.search("{.*}(.*)", element.tag)
30
- return m.group(1) if m else element.tag
40
+ return _tag_local_name(element.tag)
41
+
42
+
43
+ @cache
44
+ def _is_optional(type_hint):
45
+ return typing.get_origin(type_hint) is typing.Union and typing.get_args(type_hint)[1] is type(None)
46
+
47
+
48
+ @cache
49
+ def _strip_optional(type_hint):
50
+ if _is_optional(type_hint):
51
+ return typing.get_args(type_hint)[0]
52
+ return type_hint
31
53
 
32
54
 
33
55
  def create_element_maker():
@@ -49,11 +71,6 @@ def get_valid_root(tree: objectify.ObjectifiedElement) -> objectify.ObjectifiedE
49
71
  return root
50
72
 
51
73
 
52
- @cache
53
- def cached_type_hints(cls) -> dict:
54
- return typing.get_type_hints(cls)
55
-
56
-
57
74
  class XmlDeserialiser:
58
75
  def __init__(self):
59
76
  self.schema = etree.XMLSchema(file=get_xml_schema())
@@ -93,7 +110,7 @@ class XmlDeserialiser:
93
110
  value = element.get(attrib)
94
111
  if value is None:
95
112
  continue
96
- base_class = self.strip_optional(type_hints[attrib])
113
+ base_class = _strip_optional(type_hints[attrib])
97
114
 
98
115
  kwargs[attrib] = base_class(value)
99
116
  for child_name in getattr(element_class, "xml_elements", ()):
@@ -127,12 +144,10 @@ class XmlDeserialiser:
127
144
  return instance
128
145
 
129
146
  def strip_optional(self, type_hint):
130
- if self.is_optional(type_hint):
131
- return typing.get_args(type_hint)[0]
132
- return type_hint
147
+ return _strip_optional(type_hint)
133
148
 
134
149
  def is_optional(self, type_hint):
135
- return typing.get_origin(type_hint) is typing.Union and typing.get_args(type_hint)[1] is type(None)
150
+ return _is_optional(type_hint)
136
151
 
137
152
  def cls_name(self, name: str) -> str:
138
153
  return name if name[0].isupper() else name.title()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-materialsdb
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: A library to work with materialsdb.org open standard for building materials.
5
5
  Author-email: Cyril Waechter <cyrwae@hotmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -1,3 +1,4 @@
1
+ import re
1
2
  import sys
2
3
  import zipfile
3
4
  from pathlib import Path
@@ -8,13 +9,22 @@ sys.path.insert(0, str(ROOT / "dev_utils"))
8
9
  import build_bonsai_addon # ty: ignore[unresolved-import] - dev_util on a side path
9
10
 
10
11
 
12
+ def _manifest_version():
13
+ text = (ROOT / "bonsai_addon" / "blender_manifest.toml").read_text(encoding="utf-8")
14
+ match = re.search(r'(?m)^version = "([^"]+)"$', text)
15
+ assert match is not None
16
+ return match.group(1)
17
+
18
+
11
19
  def test_default_build_unchanged(tmp_path):
20
+ manifest_version = _manifest_version()
21
+
12
22
  out = build_bonsai_addon.build(out_dir=tmp_path)
13
23
 
14
24
  assert out.name == "materialsdb_listener.zip"
15
25
  with zipfile.ZipFile(out) as zf:
16
26
  assert "blender_manifest.toml" in zf.namelist()
17
- assert 'version = "0.2.0"' in zf.read("blender_manifest.toml").decode("utf-8")
27
+ assert f'version = "{manifest_version}"' in zf.read("blender_manifest.toml").decode("utf-8")
18
28
  assert "blender_manifest.toml".replace(".", "/") not in zf.namelist() # manifest at zip ROOT
19
29
 
20
30
 
@@ -30,8 +40,12 @@ def test_version_injected_into_manifest_and_name(tmp_path):
30
40
  def test_version_injection_is_literal_independent(tmp_path):
31
41
  """The injection must not depend on the manifest's current version
32
42
  literal (it changes on every release bump)."""
43
+
44
+ current = _manifest_version()
45
+ stale = "0.0.1" if current != "0.0.1" else "0.0.2"
46
+
33
47
  out = build_bonsai_addon.build(version="3.2.1", out_dir=tmp_path)
34
48
 
35
49
  with zipfile.ZipFile(out) as zf:
36
50
  assert 'version = "3.2.1"' in zf.read("blender_manifest.toml").decode("utf-8")
37
- assert 'version = "0.2.0"' not in zf.read("blender_manifest.toml").decode("utf-8")
51
+ assert f'version = "{stale}"' not in zf.read("blender_manifest.toml").decode("utf-8")
@@ -33,7 +33,7 @@ def cache_env(tmp_path, monkeypatch):
33
33
  # defaults are bound at def time, so patch __defaults__ too
34
34
  single = [cache.MATERIALSDBINDEXURLLIST[0]]
35
35
  monkeypatch.setattr(cache, "MATERIALSDBINDEXURLLIST", single)
36
- monkeypatch.setattr(cache.update_producers_data, "__defaults__", (single, None))
36
+ monkeypatch.setattr(cache.update_producers_data, "__defaults__", (single, None, 8))
37
37
  monkeypatch.setattr(cache.updates_available, "__defaults__", (single,))
38
38
  recorded: _Recorded = {"urls": [], "dests": [], "index": INDEX_XML}
39
39
 
@@ -57,9 +57,11 @@ def test_first_refresh_downloads_all_with_global_total(cache_env):
57
57
  on_progress=lambda done, total, name: progress.append((done, total, name)) or False
58
58
  )
59
59
 
60
- assert recorded["urls"] == ["https://x.org/a.xml", "https://x.org/b.xml"]
61
- assert progress == [(1, 2, "a.xml"), (2, 2, "b.xml")]
62
- assert [p.name for p in report.updated] == ["a.xml", "b.xml"]
60
+ assert set(recorded["urls"]) == {"https://x.org/a.xml", "https://x.org/b.xml"}
61
+ assert [p[0] for p in progress] == [1, 2] # monotonic done
62
+ assert {p[1] for p in progress} == {2} # global total reached before any download finished
63
+ assert {p[2] for p in progress} == {"a.xml", "b.xml"}
64
+ assert {p.name for p in report.updated} == {"a.xml", "b.xml"}
63
65
  assert report.existing == []
64
66
  assert (cache_dir / "Producers" / "a.xml").exists() # downloads land in Producers dir
65
67
  assert (cache_dir / "ProducerIndex.xml").exists() # new index persisted
@@ -68,7 +70,7 @@ def test_first_refresh_downloads_all_with_global_total(cache_env):
68
70
  def test_second_refresh_uses_existing_files(cache_env):
69
71
  cache, _, recorded = cache_env
70
72
  report = cache.update_producers_data()
71
- assert [p.name for p in report.updated] == ["a.xml", "b.xml"]
73
+ assert {p.name for p in report.updated} == {"a.xml", "b.xml"}
72
74
  recorded["urls"].clear()
73
75
 
74
76
  progress = []
@@ -100,9 +102,9 @@ def test_outdated_single_producer_updates_with_total_one(cache_env):
100
102
  on_progress=lambda done, total, name: progress.append((done, total, name)) or False
101
103
  )
102
104
 
103
- assert recorded["urls"] == ["https://x.org/a.xml"]
105
+ assert recorded["urls"] == ["https://x.org/a.xml"] # single job — deterministic
104
106
  assert progress == [(1, 1, "a.xml")]
105
- assert [p.name for p in record.updated] == ["a.xml"]
107
+ assert {p.name for p in record.updated} == {"a.xml"}
106
108
  assert [p.name for p in record.deleted] == ["a.xml"] # stale file removed before re-download
107
109
  assert [p.name for p in record.existing] == ["b.xml"]
108
110
 
@@ -128,7 +130,8 @@ def test_cancel_stops_between_downloads(cache_env):
128
130
  def cancel_after_first(done, total, name):
129
131
  return done >= 1 # truthy → abort
130
132
 
131
- report = cache.update_producers_data(on_progress=cancel_after_first)
133
+ # single worker: b.html cannot start before the callback runs
134
+ report = cache.update_producers_data(on_progress=cancel_after_first, max_workers=1)
132
135
 
133
136
  assert recorded["urls"] == ["https://x.org/a.xml"] # second download never starts
134
137
  assert [p.name for p in report.updated] == ["a.xml"]