bruce-models 0.3.2__tar.gz → 0.3.4__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.2 → bruce_models-0.3.4}/PKG-INFO +1 -1
  2. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/__init__.py +1 -1
  3. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/cmd_run.py +2 -0
  4. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/main.py +6 -0
  5. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_change_set.py +4 -9
  6. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_data_transform.py +3 -3
  7. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_datalab.py +26 -46
  8. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_gis.py +6 -10
  9. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_historic.py +27 -15
  10. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_import_brz.py +27 -13
  11. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_import_ifc.py +35 -19
  12. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_merge.py +4 -3
  13. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_relation.py +6 -11
  14. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/runner.py +73 -12
  15. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/client_file/client_file_value_map.py +68 -2
  16. {bruce_models-0.3.2 → bruce_models-0.3.4}/MANIFEST.in +0 -0
  17. {bruce_models-0.3.2 → bruce_models-0.3.4}/README.md +0 -0
  18. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/__main__.py +0 -0
  19. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/account/__init__.py +0 -0
  20. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/account/account.py +0 -0
  21. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/account/account_audit.py +0 -0
  22. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/api/__init__.py +0 -0
  23. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/api/api.py +0 -0
  24. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/api/bruce_api.py +0 -0
  25. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/api/errors.py +0 -0
  26. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/api/guardian_api.py +0 -0
  27. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/api/response.py +0 -0
  28. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/change/__init__.py +0 -0
  29. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/change/change_set.py +0 -0
  30. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/change/change_set_builder.py +0 -0
  31. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/change/edit_store.py +0 -0
  32. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/__init__.py +0 -0
  33. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/baseline.py +0 -0
  34. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/baselines/benchmark.json +0 -0
  35. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/bench.py +0 -0
  36. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/cmd_accounts.py +0 -0
  37. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/cmd_audit.py +0 -0
  38. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/cmd_auth.py +0 -0
  39. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/cmd_indexes.py +0 -0
  40. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/cmd_render.py +0 -0
  41. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/cmd_status.py +0 -0
  42. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/commands.py +0 -0
  43. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/config.py +0 -0
  44. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/exits.py +0 -0
  45. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/humanize.py +0 -0
  46. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/identity.py +0 -0
  47. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/live.py +0 -0
  48. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/report.py +0 -0
  49. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_bench.py +0 -0
  50. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_data_model.py +0 -0
  51. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_entity_lod.py +0 -0
  52. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_import_csv.py +0 -0
  53. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_import_geojson.py +0 -0
  54. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_import_json.py +0 -0
  55. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_import_kml.py +0 -0
  56. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_import_xlsx.py +0 -0
  57. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/run_scale.py +0 -0
  58. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed.py +0 -0
  59. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_bench.py +0 -0
  60. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_brz.py +0 -0
  61. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_change_set.py +0 -0
  62. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_data_model.py +0 -0
  63. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_data_transform.py +0 -0
  64. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_datalab.py +0 -0
  65. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_geojson.py +0 -0
  66. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_gis.py +0 -0
  67. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_historic.py +0 -0
  68. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_ifc.py +0 -0
  69. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_json.py +0 -0
  70. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_kml.py +0 -0
  71. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_lod.py +0 -0
  72. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_merge.py +0 -0
  73. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_relation.py +0 -0
  74. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_scale.py +0 -0
  75. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/seed_xlsx.py +0 -0
  76. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/style.py +0 -0
  77. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/cli/suites.py +0 -0
  78. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/client_file/__init__.py +0 -0
  79. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/client_file/client_file.py +0 -0
  80. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/client_file/client_file_query_builder.py +0 -0
  81. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/client_file/client_file_texture.py +0 -0
  82. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/common/__init__.py +0 -0
  83. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/common/bounds.py +0 -0
  84. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/common/geometry.py +0 -0
  85. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/common/geometry_builder.py +0 -0
  86. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/common/lru_cache.py +0 -0
  87. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/data_lab/__init__.py +0 -0
  88. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/data_lab/data_lab.py +0 -0
  89. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/data_lab/data_lab_builder.py +0 -0
  90. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/data_lab/data_lab_pairing.py +0 -0
  91. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/data_mapping/__init__.py +0 -0
  92. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/data_mapping/data_mapping.py +0 -0
  93. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/data_mapping/data_mapping_builder.py +0 -0
  94. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/data_transform/__init__.py +0 -0
  95. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/data_transform/data_transform.py +0 -0
  96. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/entity/__init__.py +0 -0
  97. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/entity/entity.py +0 -0
  98. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/entity/entity_historic_data.py +0 -0
  99. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/entity/entity_index.py +0 -0
  100. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/entity/entity_link.py +0 -0
  101. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/entity/entity_type.py +0 -0
  102. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/entity/entity_ucs.py +0 -0
  103. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_assembly/__init__.py +0 -0
  104. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_assembly/assembly_import.py +0 -0
  105. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_brz/__init__.py +0 -0
  106. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_brz/brz.py +0 -0
  107. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_csv/__init__.py +0 -0
  108. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_csv/csv_import.py +0 -0
  109. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_geojson/__init__.py +0 -0
  110. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_geojson/geojson.py +0 -0
  111. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_geojson/geojson_import.py +0 -0
  112. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_json/__init__.py +0 -0
  113. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_json/json_import.py +0 -0
  114. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_kml/__init__.py +0 -0
  115. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_kml/kml.py +0 -0
  116. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_kml/kml_import.py +0 -0
  117. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_xlsx/__init__.py +0 -0
  118. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_xlsx/xlsx.py +0 -0
  119. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/format_xlsx/xlsx_import.py +0 -0
  120. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/geometry/__init__.py +0 -0
  121. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/geometry/glb.py +0 -0
  122. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/hosting/__init__.py +0 -0
  123. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/hosting/hosting_location.py +0 -0
  124. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/internal/__init__.py +0 -0
  125. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/internal/uploader.py +0 -0
  126. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/lod/__init__.py +0 -0
  127. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/lod/entity_lod.py +0 -0
  128. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/lod/lod_category.py +0 -0
  129. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/relation/__init__.py +0 -0
  130. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/relation/entity_relation.py +0 -0
  131. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/relation/entity_relation_type.py +0 -0
  132. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/scenario/__init__.py +0 -0
  133. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/scenario/scenario.py +0 -0
  134. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/server/__init__.py +0 -0
  135. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/server/api_version.py +0 -0
  136. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/server/pending_action.py +0 -0
  137. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/tileset/__init__.py +0 -0
  138. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/tileset/tileset.py +0 -0
  139. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/user/__init__.py +0 -0
  140. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/user/session.py +0 -0
  141. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/util/__init__.py +0 -0
  142. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/util/import_utils.py +0 -0
  143. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/util/optional.py +0 -0
  144. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/util/path_utils.py +0 -0
  145. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/util/query_plan.py +0 -0
  146. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models/util/required.py +0 -0
  147. {bruce_models-0.3.2 → bruce_models-0.3.4}/bruce_models.egg-info/SOURCES.txt +0 -0
  148. {bruce_models-0.3.2 → bruce_models-0.3.4}/pyproject.toml +0 -0
  149. {bruce_models-0.3.2 → bruce_models-0.3.4}/requirements.txt +0 -0
  150. {bruce_models-0.3.2 → bruce_models-0.3.4}/setup.cfg +0 -0
  151. {bruce_models-0.3.2 → bruce_models-0.3.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bruce-models
3
- Version: 0.3.2
3
+ Version: 0.3.4
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.2"
65
+ __version__ = "0.3.4"
66
66
 
67
67
  __all__ = [
68
68
  "__version__",
@@ -18,6 +18,7 @@ from bruce_models.cli import config as cfg
18
18
  from bruce_models.cli import identity
19
19
  from bruce_models.cli import live
20
20
  from bruce_models.cli import report
21
+ from bruce_models.cli import runner
21
22
  from bruce_models.cli import seed as seed_default
22
23
  from bruce_models.cli import suites
23
24
  from bruce_models.cli.config import ASK
@@ -894,6 +895,7 @@ def _run_one(suite, api, args, style):
894
895
  default_page = getattr(suite, "default_page_rows", None)
895
896
  if default_page:
896
897
  extra["page_rows"] = getattr(args, "page_rows", None) or default_page
898
+ runner.set_concurrency(getattr(args, "concurrency", None))
897
899
  return suite.run(api, rows=rows, on_progress=progress,
898
900
  on_message=status.add_message, cleanup=cleanup, **extra)
899
901
  finally:
@@ -15,6 +15,7 @@ from bruce_models.api.errors import ApiError
15
15
  from bruce_models.cli import cmd_accounts, cmd_audit, cmd_auth, cmd_indexes, cmd_run, cmd_status
16
16
  from bruce_models.cli import exits
17
17
  from bruce_models.cli.config import ASK
18
+ from bruce_models.cli import runner
18
19
  from bruce_models.cli import seed
19
20
  from bruce_models.cli import seed_bench
20
21
  from bruce_models.cli import seed_scale
@@ -61,6 +62,11 @@ def _add_run_options(parser):
61
62
  help="Write the run as JSON, for trending it over time.")
62
63
  parser.add_argument("--fail-fast", dest="fail_fast", action="store_true",
63
64
  help="Stop at the first failure instead of running the rest.")
65
+ parser.add_argument("--concurrency", type=int, metavar="N",
66
+ help="How many independent operations a suite may run at once. "
67
+ f"Default {runner.DEFAULT_CONCURRENCY}, 1 or 0 runs one at a time. "
68
+ "Only steps whose work is genuinely independent fan out; a step that "
69
+ "depends on the one before it always waits.")
64
70
  parser.add_argument("--timeout", type=int, metavar="SECONDS",
65
71
  help="Give up after this long. Whatever the run created is left behind, "
66
72
  "so prefer a generous value over a tight one.")
@@ -14,7 +14,8 @@ from bruce_models.entity.entity import Entity
14
14
  from bruce_models.entity.entity_type import EntityType
15
15
  from bruce_models.cli import seed_change_set
16
16
  from bruce_models.cli import seed_datalab
17
- from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _own_type, _report_step)
17
+ from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _own_type,
18
+ _report_step, _save_entities)
18
19
 
19
20
  # A commit of this size is seconds of work, so anything approaching this is a stuck job rather than
20
21
  # a slow one.
@@ -78,14 +79,8 @@ def _create_entities(api: BruceApi, result: Result, type_id: str,
78
79
  with Timer(step):
79
80
  try:
80
81
  for mode in seed_change_set.MODES:
81
- ids = []
82
- for values in seed_change_set.rows(mode):
83
- saved = Entity.update(api, {"Bruce": {"EntityType.ID": type_id}, **values})
84
- entity_id = (saved.get("Bruce") or {}).get("ID")
85
- if not entity_id:
86
- raise ImportFailed(f"{values} was saved without an ID")
87
- ids.append(entity_id)
88
- created[mode] = ids
82
+ created[mode] = _save_entities(api, seed_change_set.rows(mode),
83
+ entity_type_id=type_id)
89
84
  step.ok = True
90
85
  step.detail = f"{len(seed_change_set.MODES)} batches of {seed_change_set.ENTITY_COUNT}"
91
86
  except (ApiError, ImportFailed, ValueError) as e:
@@ -13,7 +13,8 @@ from bruce_models.entity.entity import Entity
13
13
  from bruce_models.entity.entity_type import EntityType
14
14
  from bruce_models.cli import seed_data_transform
15
15
  from bruce_models.cli import seed_datalab
16
- from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _own_type, _report_step)
16
+ from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _own_type,
17
+ _report_step, _save_entities)
17
18
  from bruce_models.cli.run_datalab import _await_action, _datalab_for
18
19
 
19
20
  # High enough that no Transform this suite or its Account creates can carry it.
@@ -73,8 +74,7 @@ def _create_entities(api: BruceApi, result: Result, type_id: str, report) -> boo
73
74
  step.expected = seed_data_transform.ENTITY_COUNT
74
75
  with Timer(step):
75
76
  try:
76
- for values in seed_data_transform.rows():
77
- Entity.update(api, {"Bruce": {"EntityType.ID": type_id}, **values})
77
+ _save_entities(api, seed_data_transform.rows(), entity_type_id=type_id)
78
78
  step.ok = True
79
79
  step.detail = f"{step.expected} Entities, no {seed_data_transform.TARGET_ATTRIBUTE} yet"
80
80
  except (ApiError, ValueError) as e:
@@ -3,19 +3,19 @@ DataLab selections: reading them back, writing to them, and moving Entity Types.
3
3
  """
4
4
 
5
5
  import time
6
- from typing import Dict, Optional, Tuple, cast
6
+ from typing import Dict, Optional, Tuple
7
7
  from bruce_models.util.required import required
8
8
  from bruce_models.api.bruce_api import BruceApi
9
9
  from bruce_models.api.errors import ApiError
10
- from bruce_models.entity.entity import Entity
11
10
  from bruce_models.data_lab.data_lab_builder import DataLabBuilder
12
11
  from bruce_models.data_mapping.data_mapping_builder import DataMappingBuilder
13
12
  from bruce_models.entity.entity_type import EntityType
14
13
  from bruce_models.server.api_version import ApiVersion
15
14
  from bruce_models.server.pending_action import PendingAction, EStatus
16
15
  from bruce_models.cli import seed_datalab
17
- from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _message_relay,
18
- _own_type, _report_step)
16
+ from bruce_models.cli.runner import (ImportFailed, Result, Step, Timer, _finish,
17
+ _in_parallel, _message_relay, _own_type,
18
+ _report_step, _save_entities)
19
19
 
20
20
 
21
21
  # An action is queued and runs server side, so every one of them is waited on.
@@ -30,9 +30,6 @@ SORT_BY_ATTRIBUTE_MIN_VERSION = "1.0.6645"
30
30
  def _supported(api: BruceApi, minimum: str) -> Tuple[bool, str]:
31
31
  """
32
32
  Whether an installation is new enough for a feature, and the version it reported.
33
-
34
- A version that cannot be read answers yes, matching Suite.supported_by: refusing to run because
35
- the version was unavailable would turn a transient failure into a silently skipped check.
36
33
  """
37
34
  try:
38
35
  version = ApiVersion.of(ApiVersion.get(api))
@@ -46,9 +43,6 @@ def _supported(api: BruceApi, minimum: str) -> Tuple[bool, str]:
46
43
  def _datalab_start(api: BruceApi, result: Result, report) -> Optional[str]:
47
44
  """
48
45
  A parent Entity Type to hang the run off, and the source Type the suites select over.
49
-
50
- The parent is what makes teardown one delete and makes the Types these actions create by name
51
- safe: they land underneath it, so a name the server chooses for us (ITEM) is still this run's.
52
46
  """
53
47
  token = seed_datalab.new_token()
54
48
  result.seed = token
@@ -89,14 +83,8 @@ def _create_datalab_entities(api: BruceApi, result: Result, source_type_id: str,
89
83
  created = 0
90
84
  with Timer(step):
91
85
  try:
92
- for values in seed_datalab.rows(label):
93
- saved = Entity.update(api, cast("Entity.JEntity", {
94
- "Bruce": {"EntityType.ID": source_type_id},
95
- **values,
96
- }))
97
- if not (saved.get("Bruce") or {}).get("ID"):
98
- raise ImportFailed(f"{values['code']} was saved without an ID: {saved}")
99
- created += 1
86
+ created = len(_save_entities(api, seed_datalab.rows(label),
87
+ entity_type_id=source_type_id))
100
88
  step.ok = created == seed_datalab.ENTITY_COUNT
101
89
  step.detail = f"{created} Entities across {len(seed_datalab.CATEGORIES)} values"
102
90
  except (ApiError, ImportFailed, ValueError) as e:
@@ -138,9 +126,6 @@ def _await_action(api: BruceApi, response, on_message=None) -> dict:
138
126
  def _types_under(api: BruceApi, parent_type_id: str) -> Dict[str, str]:
139
127
  """
140
128
  The Entity Types beneath one parent, by name, for finding the ones an action created.
141
-
142
- Scoped to the parent because the names these actions choose are ordinary words, and an account
143
- is free to have its own Type called PIPE somewhere else.
144
129
  """
145
130
  listing = (EntityType.get_list(api) or {}).get("Items") or []
146
131
  found = {}
@@ -355,20 +340,28 @@ def run_datalab_transfer_types(api: BruceApi, rows: int = 0, on_progress=None, o
355
340
  if not _create_datalab_entities(api, result, source_type_id, label, report):
356
341
  return _finish(api, result, started, cleanup, report, "test")
357
342
 
358
- _check_transfer_to_type(api, result, source_type_id, report)
359
- _check_transfer_by_attribute(api, result, source_type_id, report)
360
- _check_map_by_attribute(api, result, source_type_id, report)
361
- _check_transfer_with_mapping(api, result, source_type_id, report)
362
- _check_transfer_without_parent(api, result, source_type_id, report)
343
+ checks = [
344
+ (result.step("Move to a fixed Type"), _check_transfer_to_type),
345
+ (result.step("Sort into Types by Attribute"), _check_transfer_by_attribute),
346
+ (result.step("Map by a value table"), _check_map_by_attribute),
347
+ (result.step("Move with a Data Mapping"), _check_transfer_with_mapping),
348
+ (result.step("Move to a parentless Type"), _check_transfer_without_parent),
349
+ ]
350
+ failures = _in_parallel(checks,
351
+ lambda pair, _index: pair[1](api, result, source_type_id, pair[0]))
352
+ for (step, _), failure in zip(checks, failures):
353
+ if isinstance(failure, Exception) and not step.detail:
354
+ step.detail = str(failure)
355
+ _report_step(report, step)
363
356
 
364
357
  return _finish(api, result, started, cleanup, report, "test")
365
358
 
366
359
 
367
- def _check_transfer_to_type(api: BruceApi, result: Result, source_type_id: str, report) -> None:
360
+ def _check_transfer_to_type(api: BruceApi, result: Result, source_type_id: str,
361
+ step: Step) -> None:
368
362
  """
369
363
  Variant one: every matching Entity moves to one named Entity Type.
370
364
  """
371
- step = result.step("Move to a fixed Type")
372
365
  step.expected = seed_datalab.ENTITY_COUNT
373
366
  prefix = seed_datalab.code_prefix("fixed")
374
367
  with Timer(step):
@@ -384,18 +377,16 @@ def _check_transfer_to_type(api: BruceApi, result: Result, source_type_id: str,
384
377
  step.detail = f"{moved} moved, {left} left behind"
385
378
  except (ApiError, ImportFailed, ValueError) as e:
386
379
  step.detail = str(e)
387
- _report_step(report, step)
388
380
 
389
381
 
390
382
  def _check_transfer_by_attribute(api: BruceApi, result: Result, source_type_id: str,
391
- report) -> None:
383
+ step: Step) -> None:
392
384
  """
393
385
  Variant two: Entity Types are created per Attribute value, named after it.
394
386
 
395
387
  The source Type has a parent, so the action looks a name up under it before creating one. That
396
388
  is a different path from a parentless source, which only ever creates.
397
389
  """
398
- step = result.step("Sort into Types by Attribute")
399
390
  expected = seed_datalab.expected_sorted_types()
400
391
  step.expected = seed_datalab.ENTITY_COUNT
401
392
  prefix = seed_datalab.code_prefix("sort")
@@ -405,7 +396,6 @@ def _check_transfer_by_attribute(api: BruceApi, result: Result, source_type_id:
405
396
  if not supported:
406
397
  step.warned = True
407
398
  step.detail = f"needs {SORT_BY_ATTRIBUTE_MIN_VERSION}, has {version}"
408
- _report_step(report, step)
409
399
  return
410
400
 
411
401
  with Timer(step):
@@ -434,14 +424,13 @@ def _check_transfer_by_attribute(api: BruceApi, result: Result, source_type_id:
434
424
  else f"{landed} into {len(expected)} Types under a parent")
435
425
  except (ApiError, ImportFailed, ValueError) as e:
436
426
  step.detail = str(e)
437
- _report_step(report, step)
438
427
 
439
428
 
440
- def _check_map_by_attribute(api: BruceApi, result: Result, source_type_id: str, report) -> None:
429
+ def _check_map_by_attribute(api: BruceApi, result: Result, source_type_id: str,
430
+ step: Step) -> None:
441
431
  """
442
432
  Variant three: an explicit table of Attribute value to Entity Type.
443
433
  """
444
- step = result.step("Map by a value table")
445
434
  counts = seed_datalab.counts_by_category()
446
435
  step.expected = seed_datalab.ENTITY_COUNT - seed_datalab.expected_unmapped_count()
447
436
  prefix = seed_datalab.code_prefix("table")
@@ -482,15 +471,13 @@ def _check_map_by_attribute(api: BruceApi, result: Result, source_type_id: str,
482
471
  else f"{step.expected} mapped, {left} left in place")
483
472
  except (ApiError, ImportFailed, ValueError) as e:
484
473
  step.detail = str(e)
485
- _report_step(report, step)
486
474
 
487
475
 
488
476
  def _check_transfer_with_mapping(api: BruceApi, result: Result, source_type_id: str,
489
- report) -> None:
477
+ step: Step) -> None:
490
478
  """
491
479
  The same action, told where values land rather than carrying them across as they are.
492
480
  """
493
- step = result.step("Move with a Data Mapping")
494
481
  counts = seed_datalab.counts_by_category()
495
482
  step.expected = seed_datalab.ENTITY_COUNT
496
483
  prefix = seed_datalab.code_prefix("mapped")
@@ -530,19 +517,13 @@ def _check_transfer_with_mapping(api: BruceApi, result: Result, source_type_id:
530
517
  else f"{moved} moved, {landed} values mapped")
531
518
  except (ApiError, ImportFailed, ValueError) as e:
532
519
  step.detail = str(e)
533
- _report_step(report, step)
534
520
 
535
521
 
536
522
  def _check_transfer_without_parent(api: BruceApi, result: Result, source_type_id: str,
537
- report) -> None:
523
+ step: Step) -> None:
538
524
  """
539
525
  That the action still works when the Type it moves to sits at the top level.
540
-
541
- Every other check here runs under a parent, which is what keeps teardown to one delete. This one
542
- holds the parentless case open, since nothing cascades to a top level Type and an account's own
543
- Types live there too.
544
526
  """
545
- step = result.step("Move to a parentless Type")
546
527
  step.expected = seed_datalab.ENTITY_COUNT
547
528
  prefix = seed_datalab.code_prefix("orphan")
548
529
  with Timer(step):
@@ -559,4 +540,3 @@ def _check_transfer_without_parent(api: BruceApi, result: Result, source_type_id
559
540
  step.detail = f"{moved} moved to a parentless Type"
560
541
  except (ApiError, ImportFailed, ValueError) as e:
561
542
  step.detail = str(e)
562
- _report_step(report, step)
@@ -15,7 +15,8 @@ from bruce_models.entity.entity import Entity
15
15
  from bruce_models.entity.entity_type import EntityType
16
16
  from bruce_models.cli import seed_datalab
17
17
  from bruce_models.cli import seed_gis
18
- from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _own_type, _report_step)
18
+ from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _own_type,
19
+ _report_step, _save_entities)
19
20
 
20
21
 
21
22
  def run_gis(api: BruceApi, rows: int = 0, on_progress=None, on_message=None,
@@ -85,15 +86,10 @@ def _create_entities(api: BruceApi, result: Result, type_id: str, report) -> Dic
85
86
  created: Dict[str, str] = {}
86
87
  with Timer(step):
87
88
  try:
88
- for kind in seed_gis.KINDS:
89
- saved = Entity.update(api, cast("Entity.JEntity", {
90
- "Bruce": seed_gis.bruce_block(kind, type_id),
91
- "code": seed_gis.code(kind),
92
- }))
93
- entity_id = (saved.get("Bruce") or {}).get("ID")
94
- if not entity_id:
95
- raise ImportFailed(f"{kind} was saved without an ID: {saved}")
96
- created[kind] = entity_id
89
+ saved = _save_entities(api, [{"Bruce": seed_gis.bruce_block(kind, type_id),
90
+ "code": seed_gis.code(kind)}
91
+ for kind in seed_gis.KINDS])
92
+ created = dict(zip(seed_gis.KINDS, saved))
97
93
  step.ok = len(created) == step.expected
98
94
  step.detail = ", ".join(seed_gis.KINDS)
99
95
  except (ApiError, ImportFailed, ValueError) as e:
@@ -3,7 +3,6 @@ Historic snapshots read back at points in time, then overlaid by a Scenario.
3
3
  """
4
4
 
5
5
  import time
6
- from typing import cast
7
6
  from bruce_models.util.required import required
8
7
  from bruce_models.api.bruce_api import BruceApi
9
8
  from bruce_models.api.errors import ApiError
@@ -13,7 +12,8 @@ from bruce_models.entity.entity_historic_data import EntityHistoricData
13
12
  from bruce_models.entity.entity_type import EntityType
14
13
  from bruce_models.scenario.scenario import Scenario
15
14
  from bruce_models.cli import seed_historic
16
- from bruce_models.cli.runner import ImportFailed, Result, Timer, _finish, _own_type, _report_step
15
+ from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _own_type,
16
+ _report_step, _save_entities)
17
17
 
18
18
 
19
19
  def run_historic_scenario(api: BruceApi, rows: int = 0, on_progress=None, on_message=None,
@@ -76,16 +76,11 @@ def _create_historic_entities(api: BruceApi, result: Result, report) -> list:
76
76
  entity_ids = []
77
77
  with Timer(step):
78
78
  try:
79
- for code in seed_historic.entity_codes():
80
- saved = Entity.update(api, cast("Entity.JEntity", {
81
- "Bruce": {"EntityType.ID": required(result.entity_type_id, "Entity Type ID")},
82
- **seed_historic.live_values(code),
83
- }))
84
- entity_id = (saved.get("Bruce") or {}).get("ID")
85
- if not entity_id:
86
- raise ImportFailed(f"{code} was saved without an ID: {saved}")
87
- entity_ids.append(entity_id)
88
- result.entity_ids.append(entity_id)
79
+ entity_ids = _save_entities(
80
+ api,
81
+ [seed_historic.live_values(code) for code in seed_historic.entity_codes()],
82
+ entity_type_id=required(result.entity_type_id, "Entity Type ID"))
83
+ result.entity_ids.extend(entity_ids)
89
84
  step.ok = True
90
85
  step.detail = f"{len(entity_ids)} Entities at their live values"
91
86
  except (ApiError, ImportFailed, ValueError) as e:
@@ -195,11 +190,27 @@ def _check_scenario(api: BruceApi, result: Result, entity_ids, stamp: int, repor
195
190
  raise ImportFailed(f"Scenario was registered without an ID: {scenario}")
196
191
  result.scenario_ids.append(scenario_id)
197
192
 
198
- EntityHistoricData.update(api, [EntityHistoricData.build_record(
193
+ written = EntityHistoricData.update(api, [EntityHistoricData.build_record(
199
194
  entity_id, seed_historic.DRIVER_PATH, seed_historic.SCENARIO_DATETIME,
200
195
  {"temperature": seed_historic.SCENARIO_TEMPERATURE,
201
196
  "status": seed_historic.SCENARIO_STATUS},
202
197
  scenario=scenario_id)])
198
+ # Per-record problems arrive as warnings inside a successful response, so an unchecked
199
+ # write reads back as live values later and looks like the Scenario was ignored.
200
+ write_warnings = warnings_in(written)
201
+ if write_warnings:
202
+ step.detail = f"writing the Scenario record: {write_warnings}"
203
+ _report_step(report, step)
204
+ return
205
+
206
+ # A Scenario read falls back to live values both when the Scenario is ignored and when
207
+ # it holds no record for the point, and those need telling apart to be actionable.
208
+ stored = EntityHistoricData.get_list(
209
+ api, entity_ids=[entity_id], attr_key=seed_historic.DRIVER_PATH,
210
+ scenario=scenario_id)
211
+ if not (stored.get("Items") or []):
212
+ raise ImportFailed("the Scenario record was accepted without warnings and did not "
213
+ "read back, so nothing was stored against the Scenario")
203
214
 
204
215
  point = seed_historic.SCENARIO_POINT
205
216
  in_scenario = Entity.get(api, entity_id, historic_point=point, scenario=scenario_id)
@@ -207,8 +218,9 @@ def _check_scenario(api: BruceApi, result: Result, entity_ids, stamp: int, repor
207
218
 
208
219
  problems = []
209
220
  if in_scenario.get("status") != seed_historic.SCENARIO_STATUS:
210
- problems.append(f"in the Scenario read {in_scenario.get('status')}, "
211
- f"expected {seed_historic.SCENARIO_STATUS}")
221
+ problems.append(f"in the Scenario at {point} read {in_scenario.get('status')}, "
222
+ f"expected {seed_historic.SCENARIO_STATUS} from the record at "
223
+ f"{seed_historic.SCENARIO_DATETIME}")
212
224
  # The point of a Scenario is that it changes nothing outside itself.
213
225
  if base.get("status") != seed_historic.SCENARIO_BASE_STATUS:
214
226
  problems.append(f"outside it read {base.get('status')}, "
@@ -16,8 +16,8 @@ from bruce_models.tileset.tileset import Tileset
16
16
  from bruce_models.cli import humanize
17
17
  from bruce_models.cli import seed_brz
18
18
  from bruce_models.cli import seed_bench
19
- from bruce_models.cli.runner import (ImportFailed, Result, Step, Timer, _cleanup, _job_outcome,
20
- _message_relay, _report_step)
19
+ from bruce_models.cli.runner import (ImportFailed, Result, Step, Timer, _cleanup,
20
+ _in_parallel, _job_outcome, _message_relay, _report_step)
21
21
 
22
22
 
23
23
  # Root offsets are pure subtraction so they stay tight; leaf coordinates pass through the importer unchanged.
@@ -237,14 +237,17 @@ def run_align_brz(
237
237
  step = result.step("Import 3 assemblies")
238
238
  with Timer(step):
239
239
  try:
240
- for index, seed in enumerate(assemblies):
241
- name = f"align-{'abc'[index]}"
242
- outcome = _import_assembly(api, result, seed.data, name, suffix,
243
- on_message)
240
+ outcomes = _in_parallel(
241
+ assemblies,
242
+ lambda seed, index: _import_assembly(api, result, seed.data,
243
+ f"align-{'abc'[index]}", suffix, on_message))
244
+ for index, outcome in enumerate(outcomes):
245
+ if isinstance(outcome, Exception):
246
+ raise outcome
244
247
  result.entity_ids.append(outcome["root_entity_id"])
245
248
  result.client_file_ids.append(outcome["client_file_id"])
246
249
  imported.append(outcome)
247
- report(f" {name} imported")
250
+ report(f" align-{'abc'[index]} imported")
248
251
  step.ok = True
249
252
  step.detail = (f"{len(imported)} roots, "
250
253
  + _job_outcome(EStatus.COMPLETE, imported[0]["dispatch"]))
@@ -320,6 +323,17 @@ def _check_alignment(result: Result, assemblies, offsets, report) -> None:
320
323
  _report_step(report, step)
321
324
 
322
325
 
326
+ def _build_tileset(api: BruceApi, result: Result, outcome, index: int):
327
+ """
328
+ Builds one assembly's tileset and reads back where its root box sits.
329
+ """
330
+ tileset_id, load_url = Tileset.build(api, f"align-{'abc'[index]}",
331
+ outcome["root_entity_id"],
332
+ on_message=_message_relay(None))
333
+ result.tileset_ids.append(tileset_id)
334
+ return Tileset.root_box_centre(Tileset.fetch_document(api, load_url))
335
+
336
+
323
337
  def _check_tilesets(api: BruceApi, result: Result, assemblies, imported, offsets, report) -> None:
324
338
  """
325
339
  Each generated tileset must be written in the shared frame rather than re-centred on its own pivot.
@@ -329,13 +343,13 @@ def _check_tilesets(api: BruceApi, result: Result, assemblies, imported, offsets
329
343
  try:
330
344
  discriminating = []
331
345
  problems = []
332
- for index, (seed, outcome, offset) in enumerate(zip(assemblies, imported, offsets)):
346
+ built = _in_parallel(imported,
347
+ lambda outcome, index: _build_tileset(api, result, outcome, index))
348
+ for index, (seed, offset) in enumerate(zip(assemblies, offsets)):
333
349
  label = "abc"[index]
334
- tileset_id, load_url = Tileset.build(api, f"align-{label}",
335
- outcome["root_entity_id"],
336
- on_message=_message_relay(None))
337
- result.tileset_ids.append(tileset_id)
338
- centre = Tileset.root_box_centre(Tileset.fetch_document(api, load_url))
350
+ if isinstance(built[index], Exception):
351
+ raise built[index]
352
+ centre = built[index]
339
353
 
340
354
  shifted = _axis_range(seed.leaves, offset, TILESET_BOX_MARGIN)
341
355
  local = _axis_range(seed.leaves, (0.0, 0.0, 0.0), TILESET_BOX_MARGIN)
@@ -14,8 +14,8 @@ from bruce_models.format_assembly.assembly_import import AssemblyImport
14
14
  from bruce_models.server.pending_action import EStatus
15
15
  from bruce_models.cli import seed_bench
16
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)
17
+ from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _in_parallel,
18
+ _job_outcome, _message_relay, _report_step)
19
19
 
20
20
 
21
21
  # How deep the walk over an imported tree is allowed to go before it gives up.
@@ -226,30 +226,46 @@ def run_ifc_variants(
226
226
  step.detail = f"{len(documents)} documents, {total / 1024:.1f} KB total"
227
227
  _report_step(report, step)
228
228
 
229
- for document in documents:
230
- _run_variant(api, result, document, suffix, on_message, report)
229
+ # Each shape is its own upload, conversion, import and Entity Type, so no two of them can see each other.
230
+ outcomes = _in_parallel(documents,
231
+ lambda document, _index: _import_variant(api, result, document,
232
+ suffix, on_message))
233
+ for document, outcome in zip(documents, outcomes):
234
+ _report_variant(result, document, outcome, report)
231
235
 
232
236
  return _finish(api, result, started, cleanup, report)
233
237
 
234
238
 
235
- def _run_variant(api: BruceApi, result: Result, document: seed_ifc.Document, suffix: str,
236
- on_message, report) -> None:
239
+ def _import_variant(api: BruceApi, result: Result, document: seed_ifc.Document, suffix: str,
240
+ on_message) -> tuple:
237
241
  """
238
- One representation shape: import it, then check every element reached the tree.
242
+ One representation shape, imported and counted, timed on its own wall clock.
243
+ """
244
+ started = time.monotonic()
245
+ imported = _import_document(api, result, document, suffix, on_message)
246
+ found = _node_count(api, imported["root_entity_id"])
247
+ return found, time.monotonic() - started
248
+
249
+
250
+ def _report_variant(result: Result, document: seed_ifc.Document, outcome, report) -> None:
251
+ """
252
+ Turns one shape's outcome into its step, in the order the documents were listed.
253
+
254
+ Reported after the work rather than during it, so the rows read the same whatever order they
255
+ finished in and one run can be diffed against another.
239
256
  """
240
257
  expected_nodes = seed_ifc.SPATIAL_LEVELS + document.element_count
241
258
  step = result.step(f"Import {document.key}")
242
259
  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}"
260
+ if isinstance(outcome, Exception):
261
+ step.detail = f"{document.description}: {outcome}"
262
+ else:
263
+ found, seconds = outcome
264
+ step.seconds = seconds
265
+ step.actual = found
266
+ if found != expected_nodes:
267
+ step.detail = f"expected {expected_nodes} Entities, found {found}"
268
+ else:
269
+ step.ok = True
270
+ step.detail = f"{found} Entities, {document.description}"
255
271
  _report_step(report, step)
@@ -13,7 +13,8 @@ from bruce_models.entity.entity import Entity
13
13
  from bruce_models.entity.entity_type import EntityType
14
14
  from bruce_models.cli import seed_datalab
15
15
  from bruce_models.cli import seed_merge
16
- from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _own_type, _report_step)
16
+ from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _own_type,
17
+ _report_step, _save_entities)
17
18
  from bruce_models.cli.run_datalab import _await_action, _datalab_for
18
19
 
19
20
 
@@ -80,8 +81,8 @@ def _create_entities(api: BruceApi, result: Result, type_id: str, report) -> boo
80
81
  step.expected = seed_merge.GROUP_COUNT * 2
81
82
  with Timer(step):
82
83
  try:
83
- for values in seed_merge.primaries() + seed_merge.secondaries():
84
- Entity.update(api, {"Bruce": {"EntityType.ID": type_id}, **values})
84
+ _save_entities(api, seed_merge.primaries() + seed_merge.secondaries(),
85
+ entity_type_id=type_id)
85
86
  step.ok = True
86
87
  step.detail = f"{seed_merge.GROUP_COUNT} primaries, {seed_merge.GROUP_COUNT} secondaries"
87
88
  except (ApiError, ValueError) as e:
@@ -8,14 +8,14 @@ from typing import Dict, Optional
8
8
  from bruce_models.util.required import required
9
9
  from bruce_models.api.bruce_api import BruceApi
10
10
  from bruce_models.api.errors import ApiError
11
- from bruce_models.entity.entity import Entity
12
11
  from bruce_models.entity.entity_link import EntityLink
13
12
  from bruce_models.entity.entity_type import EntityType
14
13
  from bruce_models.relation.entity_relation import EntityRelation
15
14
  from bruce_models.relation.entity_relation_type import EntityRelationType
16
15
  from bruce_models.cli import seed_datalab
17
16
  from bruce_models.cli import seed_relation
18
- from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _own_type, _report_step)
17
+ from bruce_models.cli.runner import (ImportFailed, Result, Timer, _finish, _own_type,
18
+ _report_step, _save_entities)
19
19
 
20
20
 
21
21
  def run_relation(api: BruceApi, rows: int = 0, on_progress=None, on_message=None,
@@ -103,15 +103,10 @@ def _create_entities(api: BruceApi, result: Result, type_id: str, report) -> Dic
103
103
  created: Dict[str, object] = {}
104
104
  with Timer(step):
105
105
  try:
106
- def save(values):
107
- saved = Entity.update(api, {"Bruce": {"EntityType.ID": type_id}, **values})
108
- entity_id = (saved.get("Bruce") or {}).get("ID")
109
- if not entity_id:
110
- raise ImportFailed(f"{values} was saved without an ID")
111
- return entity_id
112
-
113
- created["principal"] = save(seed_relation.principal())
114
- created["related"] = [save(values) for values in seed_relation.related()]
106
+ saved = _save_entities(api, [seed_relation.principal()] + list(seed_relation.related()),
107
+ entity_type_id=type_id)
108
+ created["principal"] = saved[0]
109
+ created["related"] = saved[1:]
115
110
  step.ok = True
116
111
  step.detail = f"one principal, {seed_relation.RELATED_COUNT} related"
117
112
  except (ApiError, ImportFailed, ValueError) as e: