bruce-models 0.3.0__tar.gz → 0.3.2__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 (151) hide show
  1. {bruce_models-0.3.0 → bruce_models-0.3.2}/PKG-INFO +1 -1
  2. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/__init__.py +1 -1
  3. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/change/change_set.py +8 -0
  4. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/change/change_set_builder.py +13 -2
  5. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_change_set.py +6 -3
  6. bruce_models-0.3.2/bruce_models/cli/run_import_ifc.py +255 -0
  7. bruce_models-0.3.2/bruce_models/cli/seed_ifc.py +403 -0
  8. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/suites.py +18 -1
  9. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/client_file/client_file.py +70 -0
  10. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models.egg-info/SOURCES.txt +2 -0
  11. {bruce_models-0.3.0 → bruce_models-0.3.2}/MANIFEST.in +0 -0
  12. {bruce_models-0.3.0 → bruce_models-0.3.2}/README.md +0 -0
  13. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/__main__.py +0 -0
  14. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/account/__init__.py +0 -0
  15. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/account/account.py +0 -0
  16. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/account/account_audit.py +0 -0
  17. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/api/__init__.py +0 -0
  18. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/api/api.py +0 -0
  19. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/api/bruce_api.py +0 -0
  20. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/api/errors.py +0 -0
  21. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/api/guardian_api.py +0 -0
  22. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/api/response.py +0 -0
  23. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/change/__init__.py +0 -0
  24. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/change/edit_store.py +0 -0
  25. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/__init__.py +0 -0
  26. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/baseline.py +0 -0
  27. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/baselines/benchmark.json +0 -0
  28. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/bench.py +0 -0
  29. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/cmd_accounts.py +0 -0
  30. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/cmd_audit.py +0 -0
  31. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/cmd_auth.py +0 -0
  32. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/cmd_indexes.py +0 -0
  33. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/cmd_render.py +0 -0
  34. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/cmd_run.py +0 -0
  35. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/cmd_status.py +0 -0
  36. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/commands.py +0 -0
  37. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/config.py +0 -0
  38. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/exits.py +0 -0
  39. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/humanize.py +0 -0
  40. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/identity.py +0 -0
  41. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/live.py +0 -0
  42. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/main.py +0 -0
  43. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/report.py +0 -0
  44. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_bench.py +0 -0
  45. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_data_model.py +0 -0
  46. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_data_transform.py +0 -0
  47. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_datalab.py +0 -0
  48. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_entity_lod.py +0 -0
  49. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_gis.py +0 -0
  50. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_historic.py +0 -0
  51. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_import_brz.py +0 -0
  52. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_import_csv.py +0 -0
  53. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_import_geojson.py +0 -0
  54. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_import_json.py +0 -0
  55. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_import_kml.py +0 -0
  56. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_import_xlsx.py +0 -0
  57. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_merge.py +0 -0
  58. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_relation.py +0 -0
  59. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/run_scale.py +0 -0
  60. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/runner.py +0 -0
  61. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed.py +0 -0
  62. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_bench.py +0 -0
  63. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_brz.py +0 -0
  64. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_change_set.py +0 -0
  65. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_data_model.py +0 -0
  66. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_data_transform.py +0 -0
  67. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_datalab.py +0 -0
  68. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_geojson.py +0 -0
  69. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_gis.py +0 -0
  70. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_historic.py +0 -0
  71. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_json.py +0 -0
  72. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_kml.py +0 -0
  73. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_lod.py +0 -0
  74. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_merge.py +0 -0
  75. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_relation.py +0 -0
  76. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_scale.py +0 -0
  77. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/seed_xlsx.py +0 -0
  78. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/cli/style.py +0 -0
  79. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/client_file/__init__.py +0 -0
  80. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/client_file/client_file_query_builder.py +0 -0
  81. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/client_file/client_file_texture.py +0 -0
  82. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/client_file/client_file_value_map.py +0 -0
  83. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/common/__init__.py +0 -0
  84. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/common/bounds.py +0 -0
  85. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/common/geometry.py +0 -0
  86. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/common/geometry_builder.py +0 -0
  87. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/common/lru_cache.py +0 -0
  88. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/data_lab/__init__.py +0 -0
  89. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/data_lab/data_lab.py +0 -0
  90. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/data_lab/data_lab_builder.py +0 -0
  91. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/data_lab/data_lab_pairing.py +0 -0
  92. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/data_mapping/__init__.py +0 -0
  93. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/data_mapping/data_mapping.py +0 -0
  94. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/data_mapping/data_mapping_builder.py +0 -0
  95. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/data_transform/__init__.py +0 -0
  96. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/data_transform/data_transform.py +0 -0
  97. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/entity/__init__.py +0 -0
  98. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/entity/entity.py +0 -0
  99. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/entity/entity_historic_data.py +0 -0
  100. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/entity/entity_index.py +0 -0
  101. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/entity/entity_link.py +0 -0
  102. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/entity/entity_type.py +0 -0
  103. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/entity/entity_ucs.py +0 -0
  104. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_assembly/__init__.py +0 -0
  105. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_assembly/assembly_import.py +0 -0
  106. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_brz/__init__.py +0 -0
  107. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_brz/brz.py +0 -0
  108. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_csv/__init__.py +0 -0
  109. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_csv/csv_import.py +0 -0
  110. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_geojson/__init__.py +0 -0
  111. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_geojson/geojson.py +0 -0
  112. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_geojson/geojson_import.py +0 -0
  113. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_json/__init__.py +0 -0
  114. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_json/json_import.py +0 -0
  115. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_kml/__init__.py +0 -0
  116. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_kml/kml.py +0 -0
  117. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_kml/kml_import.py +0 -0
  118. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_xlsx/__init__.py +0 -0
  119. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_xlsx/xlsx.py +0 -0
  120. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/format_xlsx/xlsx_import.py +0 -0
  121. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/geometry/__init__.py +0 -0
  122. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/geometry/glb.py +0 -0
  123. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/hosting/__init__.py +0 -0
  124. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/hosting/hosting_location.py +0 -0
  125. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/internal/__init__.py +0 -0
  126. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/internal/uploader.py +0 -0
  127. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/lod/__init__.py +0 -0
  128. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/lod/entity_lod.py +0 -0
  129. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/lod/lod_category.py +0 -0
  130. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/relation/__init__.py +0 -0
  131. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/relation/entity_relation.py +0 -0
  132. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/relation/entity_relation_type.py +0 -0
  133. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/scenario/__init__.py +0 -0
  134. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/scenario/scenario.py +0 -0
  135. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/server/__init__.py +0 -0
  136. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/server/api_version.py +0 -0
  137. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/server/pending_action.py +0 -0
  138. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/tileset/__init__.py +0 -0
  139. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/tileset/tileset.py +0 -0
  140. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/user/__init__.py +0 -0
  141. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/user/session.py +0 -0
  142. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/util/__init__.py +0 -0
  143. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/util/import_utils.py +0 -0
  144. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/util/optional.py +0 -0
  145. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/util/path_utils.py +0 -0
  146. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/util/query_plan.py +0 -0
  147. {bruce_models-0.3.0 → bruce_models-0.3.2}/bruce_models/util/required.py +0 -0
  148. {bruce_models-0.3.0 → bruce_models-0.3.2}/pyproject.toml +0 -0
  149. {bruce_models-0.3.0 → bruce_models-0.3.2}/requirements.txt +0 -0
  150. {bruce_models-0.3.0 → bruce_models-0.3.2}/setup.cfg +0 -0
  151. {bruce_models-0.3.0 → bruce_models-0.3.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bruce-models
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: This is a library which defines data-models for the Nextspace API and provides functions for communicating with the endpoints.
5
5
  Author: Matvey Lavrinovich
6
6
  Author-email: matveylavrinovich@gmail.com
@@ -62,7 +62,7 @@ from .util.path_utils import PathUtils
62
62
 
63
63
  # The one place the version is written. setup.py reads it from here without importing the package,
64
64
  # and pypi-release.sh reads this same file, so a source checkout and a wheel can never disagree.
65
- __version__ = "0.3.0"
65
+ __version__ = "0.3.2"
66
66
 
67
67
  __all__ = [
68
68
  "__version__",
@@ -188,6 +188,14 @@ class ChangeSet:
188
188
  def status_of(response: Any) -> Optional[str]:
189
189
  return (response or {}).get("Status")
190
190
 
191
+ @staticmethod
192
+ def action_id_of(response: Any) -> Optional[int]:
193
+ """
194
+ The Pending Action a commit started, from the record the route answers with.
195
+ """
196
+ response = response or {}
197
+ return response.get("PendingActionID") or response.get("PendingAction.ID")
198
+
191
199
  @staticmethod
192
200
  def is_uncommitted(response: Any) -> bool:
193
201
  """
@@ -5,6 +5,7 @@ from bruce_models.util.required import required
5
5
  from bruce_models.change.change_set import ChangeSet
6
6
  from bruce_models.change.edit_store import EditStore
7
7
  from bruce_models.entity.entity import Entity
8
+ from bruce_models.server.pending_action import PendingAction
8
9
 
9
10
  MAX_REPORTED = 1000
10
11
 
@@ -381,14 +382,24 @@ class ChangeSetBuilder:
381
382
  self._record = ChangeSet.get(self.api, self.change_id)
382
383
  return self._record
383
384
 
384
- def commit(self) -> Any:
385
+ # A commit is a job, so the record still reads Uncommitted for a moment after the route answers.
386
+ COMMIT_POLL_SECONDS = 1.0
387
+
388
+ def commit(self, wait: bool = False, timeout: Optional[float] = None) -> Any:
385
389
  """
386
390
  Applies the set. Anything queued locally is saved first, so nothing is left behind.
387
391
  """
388
392
  self._refuse_repeats("be committed")
389
393
  if len(self) or not self.change_id:
390
394
  self.save()
391
- return ChangeSet.commit(self.api, required(self.change_id, "change set ID"))
395
+ response = ChangeSet.commit(self.api, required(self.change_id, "change set ID"))
396
+ if wait:
397
+ action_id = ChangeSet.action_id_of(response)
398
+ if action_id:
399
+ PendingAction.on_completion(self.api, action_id,
400
+ interval=self.COMMIT_POLL_SECONDS, timeout=timeout)
401
+ self.refresh()
402
+ return response
392
403
 
393
404
  def reject(self) -> Any:
394
405
  """
@@ -16,6 +16,10 @@ from bruce_models.cli import seed_change_set
16
16
  from bruce_models.cli import seed_datalab
17
17
  from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _own_type, _report_step)
18
18
 
19
+ # A commit of this size is seconds of work, so anything approaching this is a stuck job rather than
20
+ # a slow one.
21
+ COMMIT_TIMEOUT = 120
22
+
19
23
 
20
24
  def run_change_set(api: BruceApi, rows: int = 0, on_progress=None, on_message=None,
21
25
  cleanup: bool = True) -> Result:
@@ -174,8 +178,7 @@ def _check_commit(api: BruceApi, result: Result, type_id: str, entity_ids: List[
174
178
  try:
175
179
  if builder is None:
176
180
  raise ImportFailed("nothing was staged to commit")
177
- builder.commit()
178
- time.sleep(2)
181
+ builder.commit(wait=True, timeout=COMMIT_TIMEOUT)
179
182
  problems = []
180
183
  changed = _count_at(api, type_id, entity_ids, seed_change_set.AFTER)
181
184
  if changed != step.expected:
@@ -186,7 +189,7 @@ def _check_commit(api: BruceApi, result: Result, type_id: str, entity_ids: List[
186
189
  step.ok = not problems
187
190
  step.detail = (", ".join(problems) if problems
188
191
  else f"{changed} applied, {ChangeSet.COMMITTED}")
189
- except (ApiError, ImportFailed, ValueError) as e:
192
+ except (ApiError, ImportFailed, TimeoutError, ValueError) as e:
190
193
  step.detail = str(e)
191
194
  _report_step(report, step)
192
195
 
@@ -0,0 +1,255 @@
1
+ """
2
+ IFC imports: whether a generated document converts, and whether each representation shape survives.
3
+ """
4
+
5
+ import time
6
+ from typing import List, Optional
7
+
8
+ from bruce_models.api.bruce_api import BruceApi
9
+ from bruce_models.api.errors import ApiError
10
+ from bruce_models.client_file.client_file import ClientFile
11
+ from bruce_models.entity.entity import Entity
12
+ from bruce_models.entity.entity_type import EntityType
13
+ from bruce_models.format_assembly.assembly_import import AssemblyImport
14
+ from bruce_models.server.pending_action import EStatus
15
+ from bruce_models.cli import seed_bench
16
+ from bruce_models.cli import seed_ifc
17
+ from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _job_outcome,
18
+ _message_relay, _report_step)
19
+
20
+
21
+ # How deep the walk over an imported tree is allowed to go before it gives up.
22
+ # The documents here are a handful of nodes, so anything approaching this is a cycle rather than a deep model.
23
+ MAX_DEPTH = 12
24
+
25
+
26
+ def _import_document(api: BruceApi, result: Result, document: seed_ifc.Document,
27
+ suffix: str, on_message=None) -> dict:
28
+ """
29
+ Uploads one generated IFC and runs both phases of the import against it.
30
+ """
31
+ name = f"ifc-{document.key}-{suffix}"
32
+ file_name = f"{name}.ifc"
33
+ try:
34
+ uploaded = ClientFile.upload_temp_bytes(api, document.data, file_name)
35
+ except ValueError as e:
36
+ raise ImportFailed(f"{document.key}: {e}") from None
37
+
38
+ # The upload registers a Client File alongside the temp file, and teardown has to know about it.
39
+ client_file_id = uploaded.get("ClientFile.ID")
40
+ if client_file_id:
41
+ result.client_file_ids.append(client_file_id)
42
+
43
+ parent_type_id = EntityType.create(api, f"bruce test {name}")
44
+ result.assembly_type_ids.append(parent_type_id)
45
+
46
+ outcome = AssemblyImport.run(api, assembly_name=file_name,
47
+ parent_type_name=f"bruce test {name}",
48
+ temp_file_id=uploaded["TempFile.ID"],
49
+ temp_file_name=file_name,
50
+ parent_type_id=parent_type_id,
51
+ on_message=_message_relay(on_message))
52
+ assembly_id = (outcome.get("result") or {}).get("Assembly.ID")
53
+ if assembly_id:
54
+ result.assembly_ids.append(assembly_id)
55
+ result.entity_ids.append(outcome["root_entity_id"])
56
+ return outcome
57
+
58
+
59
+ def _node_count(api: BruceApi, root_entity_id: str) -> int:
60
+ """
61
+ How many Entities the imported tree holds, root included.
62
+ """
63
+ seen = set()
64
+ pending = [(root_entity_id, 0)]
65
+ while pending:
66
+ entity_id, depth = pending.pop()
67
+ if not entity_id or entity_id in seen or depth > MAX_DEPTH:
68
+ continue
69
+ seen.add(entity_id)
70
+ for child in Entity.get_children(api, entity_id):
71
+ pending.append((Entity.child_id(child), depth + 1))
72
+ return len(seen)
73
+
74
+
75
+ def _leaf_ids(api: BruceApi, root_entity_id: str) -> List[str]:
76
+ """
77
+ The Entities in the tree with nothing under them, which is where element properties land.
78
+ """
79
+ leaves = []
80
+ pending = [(root_entity_id, 0)]
81
+ seen = set()
82
+ while pending:
83
+ entity_id, depth = pending.pop()
84
+ if not entity_id or entity_id in seen or depth > MAX_DEPTH:
85
+ continue
86
+ seen.add(entity_id)
87
+ children = Entity.get_children(api, entity_id)
88
+ if not children:
89
+ leaves.append(entity_id)
90
+ for child in children:
91
+ pending.append((Entity.child_id(child), depth + 1))
92
+ return leaves
93
+
94
+
95
+ def _find_property(api: BruceApi, leaf_ids: List[str]) -> Optional[str]:
96
+ """
97
+ The first value the generated property set put on any leaf, or None when none carried one.
98
+
99
+ Which leaf carries it is not asserted: an importer is free to arrange the tree how it likes, and
100
+ the question here is whether the properties crossed the conversion at all.
101
+ """
102
+ for entity_id in leaf_ids:
103
+ try:
104
+ entity = Entity.get(api, entity_id)
105
+ except (ApiError, ValueError):
106
+ continue
107
+ # Segments rather than a dotted string: a Nextspace path quotes its parts, so
108
+ # "Pset_BruceTest.Label" parses as one segment naming an attribute nothing answers to.
109
+ for path in ([seed_ifc.PSET_NAME, seed_ifc.PSET_LABEL], [seed_ifc.PSET_LABEL]):
110
+ value = Entity.get_value(entity, path)
111
+ if value:
112
+ return str(value)
113
+ return None
114
+
115
+
116
+ def run_import_ifc(
117
+ api: BruceApi,
118
+ rows: int = 0,
119
+ on_progress=None,
120
+ on_message=None,
121
+ cleanup: bool = True
122
+ ) -> Result:
123
+ """
124
+ Imports one generated IFC and checks the tree and the properties it produced.
125
+ """
126
+ def report(message):
127
+ if on_progress:
128
+ on_progress(message)
129
+
130
+ result = Result()
131
+ started = time.monotonic()
132
+ document = seed_ifc.simple()
133
+ expected_nodes = seed_ifc.SPATIAL_LEVELS + document.element_count
134
+
135
+ step = result.step("Generate IFC")
136
+ with Timer(step):
137
+ step.ok = True
138
+ step.detail = (f"{document.element_count} elements, {document.size_bytes / 1024:.1f} KB "
139
+ f"(no file fixture)")
140
+ _report_step(report, step)
141
+
142
+ step = result.step("Import IFC")
143
+ imported = None
144
+ with Timer(step):
145
+ try:
146
+ imported = _import_document(api, result, document, seed_bench.new_suffix(),
147
+ on_message)
148
+ step.ok = True
149
+ step.detail = _job_outcome(EStatus.COMPLETE, imported["dispatch"])
150
+ except (ApiError, ImportFailed, ValueError, TimeoutError) as e:
151
+ step.detail = str(e)
152
+ _report_step(report, step)
153
+
154
+ if imported:
155
+ _check_tree(api, result, document, imported["root_entity_id"], expected_nodes, report)
156
+ _check_properties(api, result, imported["root_entity_id"], report)
157
+
158
+ return _finish(api, result, started, cleanup, report)
159
+
160
+
161
+ def _check_tree(api: BruceApi, result: Result, document: seed_ifc.Document,
162
+ root_entity_id: str, expected_nodes: int, report) -> None:
163
+ """
164
+ The converted tree must carry a node for every element plus the spatial levels above them.
165
+ """
166
+ step = result.step("Check hierarchy")
167
+ step.expected = expected_nodes
168
+ with Timer(step):
169
+ try:
170
+ found = _node_count(api, root_entity_id)
171
+ step.actual = found
172
+ if found != expected_nodes:
173
+ step.detail = (f"expected {expected_nodes} Entities for "
174
+ f"{document.element_count} elements, found {found}")
175
+ else:
176
+ step.ok = True
177
+ step.detail = f"{found} Entities, {document.element_count} elements"
178
+ except (ApiError, ValueError) as e:
179
+ step.detail = str(e)
180
+ _report_step(report, step)
181
+
182
+
183
+ def _check_properties(api: BruceApi, result: Result, root_entity_id: str, report) -> None:
184
+ """
185
+ An IFC property set has to survive the conversion, or the import carries geometry and nothing else.
186
+ """
187
+ step = result.step("Check properties")
188
+ with Timer(step):
189
+ try:
190
+ leaves = _leaf_ids(api, root_entity_id)
191
+ found = _find_property(api, leaves)
192
+ if not found:
193
+ step.detail = (f"no '{seed_ifc.PSET_NAME}.{seed_ifc.PSET_LABEL}' on any of "
194
+ f"{len(leaves)} leaf Entities")
195
+ else:
196
+ step.ok = True
197
+ step.detail = f"{seed_ifc.PSET_NAME} read back on a leaf"
198
+ except (ApiError, ValueError) as e:
199
+ step.detail = str(e)
200
+ _report_step(report, step)
201
+
202
+
203
+ def run_ifc_variants(
204
+ api: BruceApi,
205
+ rows: int = 0,
206
+ on_progress=None,
207
+ on_message=None,
208
+ cleanup: bool = True
209
+ ) -> Result:
210
+ """
211
+ Imports one IFC per representation shape, so a converter regression names the shape that broke.
212
+ """
213
+ def report(message):
214
+ if on_progress:
215
+ on_progress(message)
216
+
217
+ result = Result()
218
+ started = time.monotonic()
219
+ documents = seed_ifc.variants()
220
+ suffix = seed_bench.new_suffix()
221
+
222
+ step = result.step("Generate variants")
223
+ with Timer(step):
224
+ total = sum(document.size_bytes for document in documents)
225
+ step.ok = True
226
+ step.detail = f"{len(documents)} documents, {total / 1024:.1f} KB total"
227
+ _report_step(report, step)
228
+
229
+ for document in documents:
230
+ _run_variant(api, result, document, suffix, on_message, report)
231
+
232
+ return _finish(api, result, started, cleanup, report)
233
+
234
+
235
+ def _run_variant(api: BruceApi, result: Result, document: seed_ifc.Document, suffix: str,
236
+ on_message, report) -> None:
237
+ """
238
+ One representation shape: import it, then check every element reached the tree.
239
+ """
240
+ expected_nodes = seed_ifc.SPATIAL_LEVELS + document.element_count
241
+ step = result.step(f"Import {document.key}")
242
+ step.expected = expected_nodes
243
+ with Timer(step):
244
+ try:
245
+ imported = _import_document(api, result, document, suffix, on_message)
246
+ found = _node_count(api, imported["root_entity_id"])
247
+ step.actual = found
248
+ if found != expected_nodes:
249
+ step.detail = f"expected {expected_nodes} Entities, found {found}"
250
+ else:
251
+ step.ok = True
252
+ step.detail = f"{found} Entities, {document.description}"
253
+ except (ApiError, ImportFailed, ValueError, TimeoutError) as e:
254
+ step.detail = f"{document.description}: {e}"
255
+ _report_step(report, step)