nuspace 0.6.2__tar.gz → 0.6.3__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 (134) hide show
  1. {nuspace-0.6.2 → nuspace-0.6.3}/CHANGELOG.md +10 -2
  2. {nuspace-0.6.2 → nuspace-0.6.3}/PKG-INFO +6 -6
  3. {nuspace-0.6.2 → nuspace-0.6.3}/pyproject.toml +6 -6
  4. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/content/kind.py +2 -1
  5. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/content/search.py +12 -2
  6. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/host/space.py +4 -2
  7. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ops/__init__.py +2 -0
  8. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ops/cell.py +1 -0
  9. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ops/kernel.py +18 -7
  10. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ops/read.py +7 -5
  11. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ops/state.py +2 -1
  12. nuspace-0.6.3/src/nuspace/ops/stores.py +51 -0
  13. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ops/tree.py +2 -2
  14. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ops/utils.py +18 -8
  15. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/backends/base.py +4 -5
  16. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/utils.py +1 -1
  17. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/kernel/body.py +1 -1
  18. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/kernel/envs.py +1 -1
  19. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/kernel/out.py +4 -5
  20. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/kernel/reconcile.py +8 -18
  21. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/kernel/runs.py +4 -2
  22. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/services/bootstrap.py +2 -2
  23. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/services/init.py +14 -14
  24. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/services/nav.py +1 -1
  25. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/services/reactions.py +4 -4
  26. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/services/supervisor.py +3 -15
  27. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/ui/home.py +1 -1
  28. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/ui/search.py +3 -3
  29. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/ui/seed.py +1 -1
  30. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/utils.py +2 -2
  31. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ui.py +1 -1
  32. {nuspace-0.6.2 → nuspace-0.6.3}/tests/conftest.py +6 -2
  33. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_content.py +1 -1
  34. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_host.py +12 -3
  35. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_kernel.py +1 -1
  36. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_ops_state.py +1 -1
  37. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_reactions.py +3 -2
  38. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_search.py +1 -1
  39. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_services.py +3 -2
  40. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_shapes.py +2 -2
  41. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_web.py +2 -1
  42. {nuspace-0.6.2 → nuspace-0.6.3}/.gitignore +0 -0
  43. {nuspace-0.6.2 → nuspace-0.6.3}/.pre-commit-config.yaml +0 -0
  44. {nuspace-0.6.2 → nuspace-0.6.3}/LICENSE.md +0 -0
  45. {nuspace-0.6.2 → nuspace-0.6.3}/Makefile +0 -0
  46. {nuspace-0.6.2 → nuspace-0.6.3}/README.md +0 -0
  47. {nuspace-0.6.2 → nuspace-0.6.3}/examples/space.py +0 -0
  48. {nuspace-0.6.2 → nuspace-0.6.3}/pkgs/ts/nuspace-ui/python/nuspace_ui/build +0 -0
  49. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/__init__.py +0 -0
  50. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_branding/__init__.py +0 -0
  51. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_branding/header.py +0 -0
  52. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_branding/logo.py +0 -0
  53. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_branding/palette.py +0 -0
  54. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_cli/__init__.py +0 -0
  55. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_cli/__main__.py +0 -0
  56. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_cli/_installed.py +0 -0
  57. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_cli/_meta.py +0 -0
  58. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_cli/commands/__init__.py +0 -0
  59. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_cli/commands/init.py +0 -0
  60. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_cli/commands/run.py +0 -0
  61. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_cli/commands/telemetry.py +0 -0
  62. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_cli/commands/verse.py +0 -0
  63. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_cli/main.py +0 -0
  64. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_telemetry/__init__.py +0 -0
  65. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_telemetry/config.py +0 -0
  66. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/_telemetry/send.py +0 -0
  67. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/content/__init__.py +0 -0
  68. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ext/__init__.py +0 -0
  69. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ext/registry.py +0 -0
  70. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ext/specs.py +0 -0
  71. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/host/__init__.py +0 -0
  72. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/host/presets.py +0 -0
  73. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ops/extend.py +0 -0
  74. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ops/pin.py +0 -0
  75. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ops/plane.py +0 -0
  76. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/ops/usage.py +0 -0
  77. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/shapes/__init__.py +0 -0
  78. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/shapes/cell.py +0 -0
  79. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/shapes/connection.py +0 -0
  80. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/shapes/contents.py +0 -0
  81. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/shapes/kernel.py +0 -0
  82. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/shapes/plane.py +0 -0
  83. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/shapes/reroot.py +0 -0
  84. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/shapes/space.py +0 -0
  85. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/shapes/state.py +0 -0
  86. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/shapes/states.py +0 -0
  87. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/__init__.py +0 -0
  88. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/backends/__init__.py +0 -0
  89. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/backends/async_.py +0 -0
  90. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/backends/mp.py +0 -0
  91. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/backends/pool.py +0 -0
  92. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/__init__.py +0 -0
  93. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/__init__.py +0 -0
  94. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/device.py +0 -0
  95. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/env.py +0 -0
  96. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/session.py +0 -0
  97. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/shell.py +0 -0
  98. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/sidebar/__init__.py +0 -0
  99. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/sidebar/feed.py +0 -0
  100. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/sidebar/interactions.py +0 -0
  101. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/sidebar/ref.py +0 -0
  102. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/viewer/__init__.py +0 -0
  103. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/viewer/feed.py +0 -0
  104. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/viewer/interactions.py +0 -0
  105. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/devices/web/viewer/ref.py +0 -0
  106. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/info.py +0 -0
  107. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/kernel/__init__.py +0 -0
  108. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/kernel/dispatch.py +0 -0
  109. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/kernel/kernel.py +0 -0
  110. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/kernel/space.py +0 -0
  111. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/kernel/utils.py +0 -0
  112. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/services/__init__.py +0 -0
  113. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/services/reload.py +0 -0
  114. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/ui/__init__.py +0 -0
  115. {nuspace-0.6.2 → nuspace-0.6.3}/src/nuspace/system/ui/settings.py +0 -0
  116. {nuspace-0.6.2 → nuspace-0.6.3}/tests/_ext/nuspace_testing/__init__.py +0 -0
  117. {nuspace-0.6.2 → nuspace-0.6.3}/tests/_ext/pyproject.toml +0 -0
  118. {nuspace-0.6.2 → nuspace-0.6.3}/tests/_lib/_support/__init__.py +0 -0
  119. {nuspace-0.6.2 → nuspace-0.6.3}/tests/_lib/_support/actions.py +0 -0
  120. {nuspace-0.6.2 → nuspace-0.6.3}/tests/_lib/_support/contents.py +0 -0
  121. {nuspace-0.6.2 → nuspace-0.6.3}/tests/_lib/_support/kernel.py +0 -0
  122. {nuspace-0.6.2 → nuspace-0.6.3}/tests/_lib/_support/kernel_envs.py +0 -0
  123. {nuspace-0.6.2 → nuspace-0.6.3}/tests/_lib/_support/made.py +0 -0
  124. {nuspace-0.6.2 → nuspace-0.6.3}/tests/_lib/_support/probe.py +0 -0
  125. {nuspace-0.6.2 → nuspace-0.6.3}/tests/_lib/_support/specs.py +0 -0
  126. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_cli.py +0 -0
  127. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_home.py +0 -0
  128. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_ops_kernel.py +0 -0
  129. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_ops_structure.py +0 -0
  130. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_packaging.py +0 -0
  131. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_session_pipe.py +0 -0
  132. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_settings.py +0 -0
  133. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_telemetry.py +0 -0
  134. {nuspace-0.6.2 → nuspace-0.6.3}/tests/nuspace/test_ui.py +0 -0
@@ -2,13 +2,21 @@
2
2
 
3
3
  ## Packages shipped from this repo
4
4
 
5
- - **nuspace** (the runtime, the `nuspace` command) — 0.6.2
6
- - **nuspace-ui** (the compiled web bundle) — 0.5.8
5
+ - **nuspace** (the runtime, the `nuspace` command) — 0.6.3
6
+ - **nuspace-ui** (the compiled web bundle) — 0.6.3
7
7
 
8
8
  Below is the changelog for **nuspace** - the full commit stream. Newest first.
9
9
  The `nuspace-ui` wheel is the same source tree's vite output and is not tracked
10
10
  separately here.
11
11
 
12
+ ## 0.6.3 - 2026-11-05
13
+
14
+ - Release nuspace and nuspace-ui 0.6.3 on nu 0.7.0 and virtuals 0.2.0
15
+ - Reads of containers that may never exist give empty instead of failing
16
+ - A space creates every fixed store container at open, so reads find them
17
+ - Kernel, supervisor and boot list create their containers up front at open
18
+ - Move onto nu's NULL model: absent reads checked by existence, safe defaults
19
+
12
20
  ## 0.6.2 — 2026-10-30
13
21
 
14
22
  - Release nuspace 0.6.2 on nu 0.6.8 and nuverse 0.3.5
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nuspace
3
- Version: 0.6.2
3
+ Version: 0.6.3
4
4
  Summary: Computational space for your data and tools.
5
5
  Project-URL: Repository, https://github.com/nustackdev/nuspace
6
6
  Author-email: Gor Arakelyan <gorarkln@gmail.com>
@@ -17,16 +17,16 @@ Classifier: Programming Language :: Python :: 3.13
17
17
  Requires-Python: >=3.10
18
18
  Requires-Dist: certifi>=2023.7.22
19
19
  Requires-Dist: click>=8.0
20
- Requires-Dist: nucore>=0.6.8
21
- Requires-Dist: nuspace-ui>=0.5.8
22
- Requires-Dist: nustd[kv,mp-pool,proxy,ui,valkey]>=0.6.8
20
+ Requires-Dist: nucore>=0.7.0
21
+ Requires-Dist: nuspace-ui>=0.6.3
22
+ Requires-Dist: nustd[kv,mp-pool,proxy,ui,valkey]>=0.7.0
23
23
  Requires-Dist: nuverse>=0.3.5
24
24
  Requires-Dist: rich-click>=1.6
25
25
  Requires-Dist: rich>=10.7
26
26
  Requires-Dist: typing-extensions>=4.7.0
27
- Requires-Dist: virtuals-py[redis]>=0.1.10
27
+ Requires-Dist: virtuals-py[redis]>=0.2.0
28
28
  Provides-Extra: all
29
- Requires-Dist: nustd[all]>=0.6.5; extra == 'all'
29
+ Requires-Dist: nustd[all]>=0.7.0; extra == 'all'
30
30
  Description-Content-Type: text/markdown
31
31
 
32
32
  <div align="center">
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nuspace"
7
- version = "0.6.2"
7
+ version = "0.6.3"
8
8
  description = "Computational space for your data and tools."
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
@@ -14,14 +14,14 @@ dependencies = [
14
14
  # The fabrics a space runs on, nothing more: kv for the store, valkey for
15
15
  # its change notifications, ui for the browser, mp_pool for the workers,
16
16
  # proxy (invisibles) for the worker's session.
17
- "nucore>=0.6.8",
18
- "nustd[kv,ui,valkey,mp_pool,proxy]>=0.6.8",
17
+ "nucore>=0.7.0",
18
+ "nustd[kv,ui,valkey,mp_pool,proxy]>=0.7.0",
19
19
  # The store's change notifications run over virtuals' redis backend.
20
- "virtuals-py[redis]>=0.1.10",
20
+ "virtuals-py[redis]>=0.2.0",
21
21
  # The compiled web bundle. A separate distribution only because PyPI caps
22
22
  # project size and a SPA rebuilt on every UI tweak would eat the quota --
23
23
  # not optional. `pip install nuspace` gives you the browser.
24
- "nuspace-ui>=0.5.8",
24
+ "nuspace-ui>=0.6.3",
25
25
  # The everyday Planes and Cells. A plugin like any other, found through
26
26
  # its entry point, but every space ships with it.
27
27
  "nuverse>=0.3.5",
@@ -55,7 +55,7 @@ classifiers = [
55
55
  [project.optional-dependencies]
56
56
  # Every nustd fabric and kv backend, for cells that reach past the defaults.
57
57
  all = [
58
- "nustd[all]>=0.6.5",
58
+ "nustd[all]>=0.7.0",
59
59
  ]
60
60
 
61
61
  [project.urls]
@@ -304,7 +304,8 @@ class Content(Generic[S]):
304
304
  ``items(scope)[id]`` is :meth:`at`. The mapping is the store's own, so
305
305
  ``keys``, ``len``, ``contains`` and ``on_children_change`` read and
306
306
  watch storage, a write lands an item and ``items(scope)[id].erase()``
307
- removes one. Not relative: name the running cell as
307
+ removes one. A scope no item ever landed in has no mapping yet:
308
+ ``exists()`` is False and ``len`` or ``keys`` raise. Not relative: name the running cell as
308
309
  ``(CELL, ops.Here.cell)``.
309
310
 
310
311
  Args:
@@ -31,6 +31,15 @@ if TYPE_CHECKING:
31
31
  __all__ = ["asked", "matched", "scopes", "walk"]
32
32
 
33
33
 
34
+ def _ids(held: nu.Nu) -> nu.List:
35
+ """A mapping's keys, ``[]`` when it was never written.
36
+
37
+ A kind's level, or one scope of it, exists only once an item landed
38
+ there: a kind never written in a plane has no ``planes`` to list.
39
+ """
40
+ return nu.list(nu.If(held.exists(), nu.list(held.keys()), []))
41
+
42
+
34
43
  def scopes(kind: type[Content[Any]], level: str) -> nu.List:
35
44
  """The ids of the scopes at ``level`` holding items of ``kind``: cells or planes. A bare read.
36
45
 
@@ -41,7 +50,8 @@ def scopes(kind: type[Content[Any]], level: str) -> nu.List:
41
50
  if level not in SLOTS:
42
51
  msg = f"scope level {level!r} is none of {tuple(SLOTS)}"
43
52
  raise ValueError(msg)
44
- return nu.list(getattr(States.contents[kind_name(kind)], SLOTS[level]).keys())
53
+ held = getattr(States.contents[kind_name(kind)], SLOTS[level])
54
+ return _ids(held)
45
55
 
46
56
 
47
57
  def matched(
@@ -59,7 +69,7 @@ def matched(
59
69
  """
60
70
  held = kind.items(scope)
61
71
  return (
62
- nu.list(held.keys())
72
+ _ids(held)
63
73
  .iter()
64
74
  .filter(lambda found: kind.match(kind.at(scope, nu.Str(found)), **question))
65
75
  )
@@ -26,7 +26,8 @@ planes are sure to exist on a first open, so the start is sequenced here
26
26
  instead (the same term, same ``by``).
27
27
 
28
28
  :func:`init_space` is steps 1 and 2 alone, in the store's brackets only: the
29
- space directory made and seeded, and closed again, nothing run.
29
+ space directory made, its containers created (as reconcile does at open)
30
+ and seeded, and closed again, nothing run.
30
31
 
31
32
  Workers import :mod:`nuspace`, and so this module: the web device, which
32
33
  pulls in the web server, is imported only when a space serves.
@@ -39,6 +40,7 @@ from typing import TYPE_CHECKING
39
40
 
40
41
  import nu
41
42
  from nuspace.ext import RegistryWarning, space_registry
43
+ from nuspace.ops import init_stores
42
44
  from nuspace.ops.usage import opened
43
45
  from nuspace.system.info import write_info
44
46
  from nuspace.system.kernel import DEFAULT_SPARES, init_start, open_kernel, store
@@ -92,7 +94,7 @@ def init_space(path: str) -> nu.With:
92
94
  Raises:
93
95
  NotASpace: ``path`` holds something other than a space.
94
96
  """
95
- return nu.With(store(path), body=seed_space())
97
+ return nu.With(store(path), body=init_stores() >> seed_space())
96
98
 
97
99
 
98
100
  def open_space(
@@ -84,6 +84,7 @@ from .state import (
84
84
  plane_state,
85
85
  sibling,
86
86
  )
87
+ from .stores import init_stores
87
88
  from .tree import move_plane
88
89
  from .usage import track
89
90
  from .utils import atomic, atomic_state, mint_ordered_id, snapshot
@@ -114,6 +115,7 @@ __all__ = [
114
115
  "create_cell",
115
116
  "create_plane",
116
117
  "env",
118
+ "init_stores",
117
119
  "latest",
118
120
  "mint_ordered_id",
119
121
  "move_pin",
@@ -229,6 +229,7 @@ def cell_writes(
229
229
  >> row.name.set(name)
230
230
  >> _write_prog(row, prog, has_ui)
231
231
  >> row.props.made_by.set(made_by)
232
+ >> row.meta.init(nu.Dict.create())
232
233
  )
233
234
  if meta is not None:
234
235
  writes = writes >> row.meta.update(meta)
@@ -24,7 +24,7 @@ import nustd.kv
24
24
  from nuspace.shapes import Space
25
25
 
26
26
  from .read import cell_plane, cells, plane_exists
27
- from .utils import MintId, atomic
27
+ from .utils import MintId, atomic, listed
28
28
 
29
29
 
30
30
  if TYPE_CHECKING:
@@ -48,6 +48,7 @@ __all__ = [
48
48
  "off_running",
49
49
  "plane_interrupt",
50
50
  "plane_kill",
51
+ "plane_live",
51
52
  "plane_run",
52
53
  "plane_stop",
53
54
  "run",
@@ -76,6 +77,15 @@ STOP_GRACE = 10.0
76
77
  _kernel = Space.kernel
77
78
 
78
79
 
80
+ def plane_live(plane_id: nu.StrArg) -> nu.List:
81
+ """A plane's live plane run ids, ``[]`` when it has none. Bare read.
82
+
83
+ Its ``planes_running`` entry is there only while it has live runs (the
84
+ last run out takes it away), so a plane with none has no set to read.
85
+ """
86
+ return listed(_kernel.planes_running[plane_id].runs)
87
+
88
+
79
89
  def _snap(term: nu.Nu) -> nu.Nu:
80
90
  return nustd.kv.Snapshot(term, scope=Space)
81
91
 
@@ -134,10 +144,11 @@ def add_plane_run(
134
144
  >> row.by.set(by)
135
145
  >> row.envs.set(_envs(envs))
136
146
  >> row.termination_requested.set(False)
137
- >> row.cells.init({})
138
- >> row.cells_running.init(set())
139
- >> row.latest.init({})
140
- >> row.workers.init(set())
147
+ >> row.cells.init(nu.Dict.create())
148
+ >> row.cells_running.init(nu.Set.create())
149
+ >> row.latest.init(nu.Dict.create())
150
+ >> row.workers.init(nu.Set.create())
151
+ >> row.workers_running.init(nu.Set.create())
141
152
  >> nu.ForEachDo(
142
153
  cells(plane_id),
143
154
  lambda cell: nu.let(
@@ -291,7 +302,7 @@ def off_running(run_id: nu.StrArg) -> nu.Nu:
291
302
 
292
303
  def live_runs_of(plane_id: nu.StrArg, body: Callable[[nu.Str], nu.Nu]) -> nu.Nu:
293
304
  """``body(run_id)`` for every live plane run of the plane. No bracket."""
294
- live = nu.list(_kernel.planes_running[plane_id].runs)
305
+ live = plane_live(plane_id)
295
306
  return nu.ForEachDo(live, lambda rid: body(nu.Str(rid)))
296
307
 
297
308
 
@@ -378,7 +389,7 @@ def runs(plane: nu.StrArg | None = None) -> nu.List:
378
389
  A run's cell runs are :func:`cell_runs`; an ended run is read by id with
379
390
  :func:`run`.
380
391
  """
381
- live = _kernel.running if plane is None else _kernel.planes_running[plane].runs
392
+ live = _kernel.running if plane is None else plane_live(plane)
382
393
  return nu.sorted(live).iter().map(lambda rid: _run_row(nu.Str(rid))).to_list()
383
394
 
384
395
 
@@ -14,6 +14,8 @@ from __future__ import annotations
14
14
  import nu
15
15
  from nuspace.shapes import Space
16
16
 
17
+ from .utils import listed
18
+
17
19
 
18
20
  __all__ = [
19
21
  "cell_exists",
@@ -53,7 +55,7 @@ def plane_title(plane_id: nu.StrArg) -> nu.Str:
53
55
 
54
56
  def cells(plane_id: nu.StrArg) -> nu.List:
55
57
  """A plane's cell ids, in order. ``[]`` for a plane never written."""
56
- return nu.list(Space.planes[plane_id].cells)
58
+ return listed(Space.planes[plane_id].cells)
57
59
 
58
60
 
59
61
  def cell_exists(cell_id: nu.StrArg) -> nu.Bool:
@@ -66,7 +68,7 @@ def cell_named(plane_id: nu.StrArg, name: nu.StrArg) -> nu.Str:
66
68
  named = (
67
69
  cells(plane_id).iter().filter(lambda c: Space.cells[nu.Str(c)].name.fallback("") == name)
68
70
  )
69
- return nu.str(nu.First(named)).fallback("")
71
+ return nu.str(nu.First(named, ""))
70
72
 
71
73
 
72
74
  def cell_plane(cell_id: nu.StrArg) -> nu.Str:
@@ -79,7 +81,7 @@ def children(plane_id: nu.StrArg = "") -> nu.List:
79
81
 
80
82
  ``[]`` for a plane never written.
81
83
  """
82
- under = nu.list(Space.planes[plane_id].children)
84
+ under = listed(Space.planes[plane_id].children)
83
85
  return nu.List(nu.If(nu.Str(plane_id) == "", nu.list(Space.top), under))
84
86
 
85
87
 
@@ -90,7 +92,7 @@ def parent(plane_id: nu.StrArg) -> nu.Str:
90
92
 
91
93
  def prog(cell_id: nu.StrArg) -> nu.Str:
92
94
  """A cell's source, ``""`` where there is none."""
93
- return nu.str(Space.cells[cell_id].prog).fallback("")
95
+ return nu.str(Space.cells[cell_id].prog.fallback(""))
94
96
 
95
97
 
96
98
  def plane_rows() -> nu.List:
@@ -132,7 +134,7 @@ def cell_rows(plane_id: nu.StrArg) -> nu.List:
132
134
  return nu.Dict.of(
133
135
  id=at,
134
136
  name=cell.name.fallback(""),
135
- prog=nu.str(cell.prog).fallback(""),
137
+ prog=nu.str(cell.prog.fallback("")),
136
138
  props=nu.Dict.of(
137
139
  made_by=cell.props.made_by.fallback(""), has_ui=cell.props.has_ui.fallback(True)
138
140
  ),
@@ -174,7 +174,8 @@ def drop_items(level: str, scope_id: nu.StrArg) -> nu.Nu:
174
174
 
175
175
  def drop(kind: nu.Attr) -> nu.Nu:
176
176
  held = getattr(States.contents[nu.Str(kind)], level)
177
- return nu.IfDo(held.contains(scope_id), held.del_item(scope_id))
177
+ there = nu.IfDo(held.contains(scope_id), held.del_item(scope_id))
178
+ return nu.IfDo(held.exists(), there)
178
179
 
179
180
  return nu.ForEachDo(nu.list(States.contents.keys()), drop)
180
181
 
@@ -0,0 +1,51 @@
1
+ """The stores' fixed containers, made real once at open.
2
+
3
+ A container never written is absent, and reading one (its length, its
4
+ keys, whether it holds an id) raises. The top level of both stores is fixed
5
+ by their root shapes, so each container there is created up front, before
6
+ anything reads it. ``init`` writes only where one is missing, so this is
7
+ safe on every open of an existing space. A plane's or a cell's own
8
+ containers are made with the row, by the op that adds it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import nu
14
+ from nuspace.shapes import Space, States
15
+
16
+ from .utils import atomic, atomic_state
17
+
18
+
19
+ __all__ = ["init_stores"]
20
+
21
+
22
+ def _space() -> nu.Nu:
23
+ """Space's top level: structure, devices, the space's own state, the kernel's indexes."""
24
+ return (
25
+ Space.planes.init(nu.Dict.create())
26
+ >> Space.cells.init(nu.Dict.create())
27
+ >> Space.top.init(nu.List.create())
28
+ >> Space.pinned.init(nu.List.create())
29
+ >> Space.connections.init(nu.Dict.create())
30
+ >> Space.state.recents.init(nu.List.create())
31
+ >> Space.state.made.init(nu.Dict.create())
32
+ >> Space.kernel.runs.init(nu.Dict.create())
33
+ >> Space.kernel.running.init(nu.Set.create())
34
+ >> Space.kernel.workers.init(nu.Dict.create())
35
+ >> Space.kernel.workers_running.init(nu.Set.create())
36
+ >> Space.kernel.planes_running.init(nu.Dict.create())
37
+ )
38
+
39
+
40
+ def _states() -> nu.Nu:
41
+ """States' top level: every plane's state, every cell's, every kind's items."""
42
+ return (
43
+ States.planes.init(nu.Dict.create())
44
+ >> States.cells.init(nu.Dict.create())
45
+ >> States.contents.init(nu.Dict.create())
46
+ )
47
+
48
+
49
+ def init_stores() -> nu.Nu:
50
+ """Every fixed top level container of both stores, made where missing. Two commits, Space first."""
51
+ return atomic(_space()) >> atomic_state(_states())
@@ -19,7 +19,7 @@ import nu
19
19
  from nuspace.shapes import Space
20
20
 
21
21
  from .read import plane_exists
22
- from .utils import atomic
22
+ from .utils import atomic, listed
23
23
 
24
24
 
25
25
  if TYPE_CHECKING:
@@ -49,7 +49,7 @@ def subtree(plane_id: nu.StrArg, body: Callable[[nu.Nu], nu.Nu]) -> nu.Nu:
49
49
  cycle written by hand ends rather than going round forever.
50
50
  """
51
51
  going, edge = nu.List(_Walk.going), nu.List(_Walk.edge)
52
- below = nu.Flatten(nu.Map(edge, lambda p: nu.list(Space.planes[nu.Str(p)].children)))
52
+ below = nu.Flatten(nu.Map(edge, lambda p: listed(Space.planes[nu.Str(p)].children)))
53
53
  step = nu.List(nu.Collect(nu.Filter(nu.Unique(below), lambda p: going.contains(p).not_())))
54
54
  walk = nu.WhileDo(edge.len() > 0, _Walk.going.set(going + edge) >> _Walk.edge.set(step))
55
55
  return nu.Frame(_Walk, walk >> body(_Walk.going), going=[], edge=nu.List.of(plane_id))
@@ -37,8 +37,7 @@ from typing import TYPE_CHECKING
37
37
 
38
38
  import nu
39
39
  import nustd.kv
40
- from nu.lang import ScalarQuery
41
- from nu.lang.sentinels import EMPTY
40
+ from nu.lang import ScalarQuery, is_null
42
41
  from nuspace.shapes import Space, States
43
42
 
44
43
 
@@ -54,6 +53,7 @@ __all__ = [
54
53
  "atomic_state",
55
54
  "field_str",
56
55
  "keep_order",
56
+ "listed",
57
57
  "mint_ordered_id",
58
58
  "snapshot",
59
59
  ]
@@ -87,7 +87,7 @@ class MintId(ScalarQuery):
87
87
  prefix: The id's prefix.
88
88
 
89
89
  Yields:
90
- The id, a str. EMPTY when the prefix is EMPTY.
90
+ The id, a str. NULL when the prefix is NULL.
91
91
  """
92
92
 
93
93
  def _compile(self, nid: int, children: tuple[Callable, ...]) -> Callable:
@@ -95,8 +95,8 @@ class MintId(ScalarQuery):
95
95
 
96
96
  def thunk(rt: Runtime) -> object:
97
97
  p = prefix(rt)
98
- if p is EMPTY:
99
- return EMPTY
98
+ if is_null(p):
99
+ return p
100
100
  return mint_ordered_id(p)
101
101
 
102
102
  return thunk
@@ -106,8 +106,8 @@ class MintId(ScalarQuery):
106
106
 
107
107
  async def athunk(rt: Runtime) -> object:
108
108
  p = await prefix(rt)
109
- if p is EMPTY:
110
- return EMPTY
109
+ if is_null(p):
110
+ return p
111
111
  return mint_ordered_id(p)
112
112
 
113
113
  return athunk
@@ -159,10 +159,20 @@ def snapshot(term: nu.Nu) -> nu.Nu:
159
159
  return nustd.kv.Snapshot(nustd.kv.Snapshot(term, scope=States), scope=Space)
160
160
 
161
161
 
162
+ def listed(ref: nu.Nu) -> nu.List:
163
+ """A list or set container's members as a list, ``[]`` when it was never written. Bare read.
164
+
165
+ For containers that may legitimately be absent, eg a plane's ``cells``
166
+ asked about a plane that is not there. A fixed container is made at open
167
+ instead (:func:`~nuspace.ops.init_stores`) and read as is.
168
+ """
169
+ return nu.list(nu.If(ref.exists(), nu.list(ref), []))
170
+
171
+
162
172
  def field_str(row: nu.Nu, field: str) -> nu.Str:
163
173
  """One string field off a dict, a row or a browser event: ``""`` when absent.
164
174
 
165
- Total on purpose, so a field left out hands an op a string, not EMPTY.
175
+ Total on purpose, so a field left out hands an op a string, not NULL.
166
176
  """
167
177
  return nu.str(nu.Dict(row).get_item(field, ""))
168
178
 
@@ -33,8 +33,7 @@ import nu
33
33
  import nustd.time
34
34
  from nu.context import FabricRef
35
35
  from nu.engine.structure import Declared
36
- from nu.lang import ScalarAction
37
- from nu.lang.sentinels import EMPTY
36
+ from nu.lang import ScalarAction, is_null
38
37
  from nuspace.ops.utils import atomic
39
38
  from nuspace.shapes import EXIT_FAILED, Space
40
39
 
@@ -165,7 +164,7 @@ def require_backend(value: object) -> Backend:
165
164
  if isinstance(value, Backend):
166
165
  return value
167
166
  name = value.name if isinstance(value, _Named) else value
168
- if name is EMPTY or name == "":
167
+ if is_null(name) or name == "":
169
168
  msg = "The plane names no backend: every plane must name one, eg mp or async"
170
169
  raise UnknownBackendError(msg)
171
170
  msg = f"No backend registered as {name!r}"
@@ -268,7 +267,7 @@ def _made(backend: nu.StrArg, run_id: nu.StrArg, made: nu.Nu) -> nu.Nu:
268
267
  >> row.handle.set(nu.str(pair[1]))
269
268
  >> row.started_at.set(nustd.time.time())
270
269
  >> run.workers.add(wid)
271
- >> nu.IfDo(row.terminated_at.missing(), live)
270
+ >> nu.IfDo(row.terminated_at.exists().not_(), live)
272
271
  )
273
272
 
274
273
  return nu.ForEachDo(nu.List(made), record)
@@ -283,7 +282,7 @@ def ended(worker_id: nu.StrArg, exit_: nu.StrArg, error: nu.StrArg = "") -> nu.N
283
282
  run = _kernel.runs[row.run.fallback("")]
284
283
  return (
285
284
  nu.IfDo(
286
- row.terminated_at.missing(),
285
+ row.terminated_at.exists().not_(),
287
286
  row.terminated_at.set(nustd.time.time()) >> row.exit.set(exit_) >> row.error.set(error),
288
287
  )
289
288
  >> run.workers_running.remove(worker_id, missing_ok=True)
@@ -246,7 +246,7 @@ class Arms:
246
246
  # --- Reading one event's fields ---------------------------------------------
247
247
  #
248
248
  # Total on purpose: a field the browser left out reads as its default, not as
249
- # EMPTY, so an op is handed what it wants. A string field is
249
+ # NULL, so an op is handed what it wants. A string field is
250
250
  # :func:`~nuspace.ops.utils.field_str`, shared with the rows the ops read.
251
251
 
252
252
 
@@ -106,7 +106,7 @@ def end_cell_run(
106
106
  if out is not None:
107
107
  writes = cr.out.set(out) >> writes
108
108
  writes = writes >> cr.terminated_at.set(nustd.time.time())
109
- return nu.IfDo(cr.terminated_at.missing(), writes) >> row.cells_running.remove(
109
+ return nu.IfDo(cr.terminated_at.exists().not_(), writes) >> row.cells_running.remove(
110
110
  cell_run_id, missing_ok=True
111
111
  )
112
112
 
@@ -97,6 +97,6 @@ class KernelConfig:
97
97
 
98
98
 
99
99
  class KernelConfigRef(FabricRef):
100
- """The :class:`KernelConfig` bound on the context. EMPTY when none is."""
100
+ """The :class:`KernelConfig` bound on the context. NULL when none is."""
101
101
 
102
102
  fabric = KernelConfig
@@ -30,8 +30,7 @@ from typing import TYPE_CHECKING
30
30
  from nu.context import FabricRef
31
31
  from nu.core.spans.bracket import _LifecycleBracket
32
32
  from nu.engine.structure import Declared
33
- from nu.lang import ScalarAction, ScalarQuery
34
- from nu.lang.sentinels import EMPTY
33
+ from nu.lang import ScalarAction, ScalarQuery, is_null
35
34
 
36
35
 
37
36
  if TYPE_CHECKING:
@@ -117,7 +116,7 @@ class RunOut:
117
116
 
118
117
 
119
118
  class RunOutRef(FabricRef):
120
- """The :class:`RunOut` bound on the context. EMPTY outside a run."""
119
+ """The :class:`RunOut` bound on the context. NULL outside a run."""
121
120
 
122
121
  fabric = RunOut
123
122
 
@@ -177,7 +176,7 @@ class Captured(_LifecycleBracket):
177
176
 
178
177
 
179
178
  def _value(v: object, default: object) -> object:
180
- return default if v is EMPTY or v is None else v
179
+ return default if is_null(v) or v is None else v
181
180
 
182
181
 
183
182
  class _Pure(ScalarQuery):
@@ -214,7 +213,7 @@ class TakeOut(ScalarAction):
214
213
 
215
214
  Args:
216
215
  extra: Text appended first as one stderr entry, eg a traceback.
217
- EMPTY or "" appends nothing.
216
+ NULL or "" appends nothing.
218
217
  final: Take unfinished lines too.
219
218
 
220
219
  Yields:
@@ -4,14 +4,16 @@ A plane run in ``running`` or a worker in ``workers_running`` belongs to a
4
4
  process that is gone, since workers die with the host. Ending each
5
5
  ``killed``, its live cell runs with it, makes the records true again before
6
6
  anything else reads them, the narrower indexes (by plane, by run) emptied
7
- with them. Only the indexes are walked, never history. It also makes the
8
- kernel's containers real, since a fold subscribing to a container that is
9
- not there never hears anything.
7
+ with them. Only the indexes are walked, never history. It runs first, so it
8
+ also makes both stores' fixed containers real up front, the kernel's
9
+ included: every read after it (the walks here, ``running.contains``, a
10
+ run's lookups, the planes listed) finds a container, never an absent one.
10
11
  """
11
12
 
12
13
  from __future__ import annotations
13
14
 
14
15
  import nu
16
+ from nuspace.ops import init_stores
15
17
  from nuspace.ops.utils import atomic
16
18
  from nuspace.shapes import EXIT_KILLED, Space
17
19
  from nuspace.system import backends
@@ -25,22 +27,10 @@ __all__ = ["reconcile"]
25
27
  _kernel = Space.kernel
26
28
 
27
29
 
28
- def _containers() -> nu.Nu:
29
- """Every kernel container made real, so a subscription on it resolves."""
30
- return (
31
- _kernel.runs.init(nu.Dict.create())
32
- >> _kernel.running.init(nu.Set.create())
33
- >> _kernel.workers.init(nu.Dict.create())
34
- >> _kernel.workers_running.init(nu.Set.create())
35
- >> _kernel.planes_running.init(nu.Dict.create())
36
- )
37
-
38
-
39
30
  def reconcile() -> nu.Nu:
40
- """End every live plane run and worker ``killed``, empty the indexes. One commit."""
41
- return atomic(
42
- _containers()
43
- >> nu.ForEachDo(
31
+ """The stores' containers made, then every live run and worker ended ``killed``, indexes emptied."""
32
+ return init_stores() >> atomic(
33
+ nu.ForEachDo(
44
34
  nu.list(_kernel.workers_running), lambda wid: backends.ended(nu.Str(wid), EXIT_KILLED)
45
35
  )
46
36
  >> nu.ForEachDo(nu.list(_kernel.running), lambda rid: end_run(nu.Str(rid), EXIT_KILLED))
@@ -89,7 +89,9 @@ def end_run(run_id: nu.StrArg, exit_: nu.StrArg, error: nu.StrArg | None = None)
89
89
  nu.list(row.cells_running),
90
90
  lambda cr: end_cell_run(run_id, nu.Str(cr), exit_, error),
91
91
  )
92
- >> nu.IfDo(row.terminated_at.missing(), own >> row.terminated_at.set(nustd.time.time()))
92
+ >> nu.IfDo(
93
+ row.terminated_at.exists().not_(), own >> row.terminated_at.set(nustd.time.time())
94
+ )
93
95
  >> backends.released(run_id, workers)
94
96
  >> off_running(run_id)
95
97
  )
@@ -135,7 +137,7 @@ def _cell_arm(run_id: nu.StrArg, cell_run_id: nu.StrArg, backend: nu.StrArg) ->
135
137
 
136
138
  run = nu.let(RunCell(backend, run_id, cell_run_id), lost)
137
139
  go = nu.IfDo(
138
- snap(cr.worker.missing()),
140
+ snap(cr.worker.exists().not_()),
139
141
  nu.TryCatch(place >> run, catch=lambda err: nu.let(ErrorText(err), failed)),
140
142
  failed(_ORPHAN),
141
143
  )
@@ -45,8 +45,8 @@ def _service(plane_id: str, cell_id: str, shim: str) -> nu.Nu:
45
45
  prog (eg written by hand). The cell's ``has_ui`` is worked out first,
46
46
  outside the bracket, and only when the cell is missing.
47
47
  """
48
- no_plane = Space.planes[plane_id].contains("name").not_()
49
- no_cell = Space.cells[cell_id].contains("prog").not_()
48
+ no_plane = Space.planes[plane_id].name.exists().not_()
49
+ no_cell = Space.cells[cell_id].prog.exists().not_()
50
50
  made = nu.IfDo(no_plane, plane_writes(plane_id, backend="mp", name=plane_id, system=True))
51
51
 
52
52
  def both(ui: nu.ObjectRef) -> nu.Nu: