nuspace 0.6.2__tar.gz → 0.6.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 (137) hide show
  1. {nuspace-0.6.2 → nuspace-0.6.4}/CHANGELOG.md +22 -2
  2. {nuspace-0.6.2 → nuspace-0.6.4}/PKG-INFO +6 -6
  3. {nuspace-0.6.2 → nuspace-0.6.4}/pyproject.toml +7 -6
  4. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/__init__.py +2 -4
  5. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_telemetry/__init__.py +7 -5
  6. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_telemetry/config.py +18 -4
  7. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_telemetry/send.py +44 -9
  8. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/content/kind.py +2 -1
  9. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/content/search.py +12 -2
  10. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ext/__init__.py +1 -2
  11. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ext/specs.py +31 -12
  12. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/host/presets.py +2 -2
  13. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/host/space.py +5 -4
  14. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ops/__init__.py +2 -0
  15. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ops/cell.py +1 -0
  16. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ops/kernel.py +18 -12
  17. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ops/read.py +7 -5
  18. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ops/state.py +2 -1
  19. nuspace-0.6.4/src/nuspace/ops/stores.py +51 -0
  20. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ops/tree.py +2 -2
  21. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ops/utils.py +18 -8
  22. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/backends/base.py +4 -5
  23. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/backends/pool.py +64 -45
  24. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/shell.py +2 -1
  25. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/utils.py +20 -50
  26. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/viewer/feed.py +50 -34
  27. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/viewer/ref.py +26 -12
  28. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/kernel/body.py +2 -2
  29. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/kernel/envs.py +1 -1
  30. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/kernel/kernel.py +6 -2
  31. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/kernel/out.py +4 -5
  32. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/kernel/reconcile.py +8 -18
  33. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/kernel/runs.py +90 -18
  34. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/kernel/space.py +22 -14
  35. nuspace-0.6.4/src/nuspace/system/kernel/utils.py +8 -0
  36. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/services/bootstrap.py +2 -2
  37. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/services/init.py +30 -21
  38. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/services/nav.py +60 -40
  39. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/services/reactions.py +44 -32
  40. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/services/reload.py +8 -5
  41. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/services/supervisor.py +49 -60
  42. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/ui/home.py +22 -18
  43. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/ui/search.py +25 -21
  44. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/ui/seed.py +1 -1
  45. nuspace-0.6.4/src/nuspace/system/utils.py +67 -0
  46. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ui.py +3 -3
  47. {nuspace-0.6.2 → nuspace-0.6.4}/tests/_lib/_support/kernel.py +2 -1
  48. {nuspace-0.6.2 → nuspace-0.6.4}/tests/_lib/_support/specs.py +1 -1
  49. {nuspace-0.6.2 → nuspace-0.6.4}/tests/conftest.py +10 -2
  50. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_cli.py +2 -1
  51. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_content.py +1 -1
  52. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_home.py +8 -57
  53. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_host.py +12 -3
  54. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_kernel.py +132 -4
  55. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_ops_state.py +14 -1
  56. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_reactions.py +104 -37
  57. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_search.py +145 -6
  58. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_services.py +98 -7
  59. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_shapes.py +7 -6
  60. nuspace-0.6.4/tests/nuspace/test_system_utils.py +129 -0
  61. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_telemetry.py +39 -13
  62. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_ui.py +1 -1
  63. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_web.py +59 -9
  64. nuspace-0.6.2/src/nuspace/system/kernel/utils.py +0 -24
  65. nuspace-0.6.2/src/nuspace/system/utils.py +0 -106
  66. {nuspace-0.6.2 → nuspace-0.6.4}/.gitignore +0 -0
  67. {nuspace-0.6.2 → nuspace-0.6.4}/.pre-commit-config.yaml +0 -0
  68. {nuspace-0.6.2 → nuspace-0.6.4}/LICENSE.md +0 -0
  69. {nuspace-0.6.2 → nuspace-0.6.4}/Makefile +0 -0
  70. {nuspace-0.6.2 → nuspace-0.6.4}/README.md +0 -0
  71. {nuspace-0.6.2 → nuspace-0.6.4}/examples/space.py +0 -0
  72. {nuspace-0.6.2 → nuspace-0.6.4}/pkgs/ts/nuspace-ui/python/nuspace_ui/build +0 -0
  73. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_branding/__init__.py +0 -0
  74. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_branding/header.py +0 -0
  75. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_branding/logo.py +0 -0
  76. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_branding/palette.py +0 -0
  77. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_cli/__init__.py +0 -0
  78. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_cli/__main__.py +0 -0
  79. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_cli/_installed.py +0 -0
  80. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_cli/_meta.py +0 -0
  81. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_cli/commands/__init__.py +0 -0
  82. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_cli/commands/init.py +0 -0
  83. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_cli/commands/run.py +0 -0
  84. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_cli/commands/telemetry.py +0 -0
  85. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_cli/commands/verse.py +0 -0
  86. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/_cli/main.py +0 -0
  87. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/content/__init__.py +0 -0
  88. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ext/registry.py +0 -0
  89. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/host/__init__.py +0 -0
  90. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ops/extend.py +0 -0
  91. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ops/pin.py +0 -0
  92. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ops/plane.py +0 -0
  93. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/ops/usage.py +0 -0
  94. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/shapes/__init__.py +0 -0
  95. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/shapes/cell.py +0 -0
  96. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/shapes/connection.py +0 -0
  97. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/shapes/contents.py +0 -0
  98. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/shapes/kernel.py +0 -0
  99. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/shapes/plane.py +0 -0
  100. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/shapes/reroot.py +0 -0
  101. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/shapes/space.py +0 -0
  102. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/shapes/state.py +0 -0
  103. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/shapes/states.py +0 -0
  104. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/__init__.py +0 -0
  105. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/backends/__init__.py +0 -0
  106. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/backends/async_.py +0 -0
  107. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/backends/mp.py +0 -0
  108. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/__init__.py +0 -0
  109. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/__init__.py +0 -0
  110. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/device.py +0 -0
  111. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/env.py +0 -0
  112. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/session.py +0 -0
  113. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/sidebar/__init__.py +0 -0
  114. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/sidebar/feed.py +0 -0
  115. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/sidebar/interactions.py +0 -0
  116. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/sidebar/ref.py +0 -0
  117. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/viewer/__init__.py +0 -0
  118. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/devices/web/viewer/interactions.py +0 -0
  119. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/info.py +0 -0
  120. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/kernel/__init__.py +0 -0
  121. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/kernel/dispatch.py +0 -0
  122. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/services/__init__.py +0 -0
  123. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/ui/__init__.py +0 -0
  124. {nuspace-0.6.2 → nuspace-0.6.4}/src/nuspace/system/ui/settings.py +0 -0
  125. {nuspace-0.6.2 → nuspace-0.6.4}/tests/_ext/nuspace_testing/__init__.py +0 -0
  126. {nuspace-0.6.2 → nuspace-0.6.4}/tests/_ext/pyproject.toml +0 -0
  127. {nuspace-0.6.2 → nuspace-0.6.4}/tests/_lib/_support/__init__.py +0 -0
  128. {nuspace-0.6.2 → nuspace-0.6.4}/tests/_lib/_support/actions.py +0 -0
  129. {nuspace-0.6.2 → nuspace-0.6.4}/tests/_lib/_support/contents.py +0 -0
  130. {nuspace-0.6.2 → nuspace-0.6.4}/tests/_lib/_support/kernel_envs.py +0 -0
  131. {nuspace-0.6.2 → nuspace-0.6.4}/tests/_lib/_support/made.py +0 -0
  132. {nuspace-0.6.2 → nuspace-0.6.4}/tests/_lib/_support/probe.py +0 -0
  133. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_ops_kernel.py +0 -0
  134. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_ops_structure.py +0 -0
  135. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_packaging.py +0 -0
  136. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_session_pipe.py +0 -0
  137. {nuspace-0.6.2 → nuspace-0.6.4}/tests/nuspace/test_settings.py +0 -0
@@ -2,13 +2,33 @@
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.4
6
+ - **nuspace-ui** (the compiled web bundle) — 0.6.4
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.4 - 2026-11-07
13
+
14
+ - Release nuspace and nuspace-ui 0.6.4 on nu 0.7.1, virtuals 0.2.1 and ui-kit 0.4.0
15
+ - Telemetry sends only known nuspace events and stays off in every major CI
16
+ - Cmd+Enter at the end of text leaves no empty line behind
17
+ - Writing keeps to prose islands: arrows stop at other cells, input lines join in
18
+ - Plane menus share one layout; sidebar menu gains copy id and close items
19
+ - Text cells read as one document: arrows cross seams, Backspace merges, Cmd+Enter splits
20
+ - Writing in a plane: honest hints, slash in empty text, arrows across text cells
21
+ - Move onto the new reactive flows; failing arms now restart, isolate or fail their run
22
+ - Workers come from the hardened pool: one stop grace, no orphans, clean exit causes
23
+
24
+ ## 0.6.3 - 2026-11-05
25
+
26
+ - Release nuspace and nuspace-ui 0.6.3 on nu 0.7.0 and virtuals 0.2.0
27
+ - Reads of containers that may never exist give empty instead of failing
28
+ - A space creates every fixed store container at open, so reads find them
29
+ - Kernel, supervisor and boot list create their containers up front at open
30
+ - Move onto nu's NULL model: absent reads checked by existence, safe defaults
31
+
12
32
  ## 0.6.2 — 2026-10-30
13
33
 
14
34
  - 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.4
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.1
21
+ Requires-Dist: nuspace-ui>=0.6.4
22
+ Requires-Dist: nustd[kv,mp-pool,proxy,ui,valkey]>=0.7.1
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.1
28
28
  Provides-Extra: all
29
- Requires-Dist: nustd[all]>=0.6.5; extra == 'all'
29
+ Requires-Dist: nustd[all]>=0.7.1; 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.4"
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.1",
18
+ "nustd[kv,ui,valkey,mp_pool,proxy]>=0.7.1",
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.1",
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.4",
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.1",
59
59
  ]
60
60
 
61
61
  [project.urls]
@@ -76,6 +76,7 @@ exclude = [
76
76
  ]
77
77
 
78
78
  [tool.uv.sources]
79
+ virtuals-py = { path = "../virtuals", editable = true }
79
80
  nuspace-ui = { path = "./pkgs/ts/nuspace-ui", editable = true }
80
81
  nucore = { path = "../nu", editable = true }
81
82
  nustd = { path = "../nu/pkgs/nustd", editable = true }
@@ -5,8 +5,7 @@ What a host and a program need, without knowing the layers:
5
5
  - :func:`open_space`, and what an installed package registers with: an
6
6
  :class:`Extension` of :class:`PlaneSpec`, :class:`CellSpec` and
7
7
  :class:`ContentSpec` (the store's own shapes are ``shapes.Plane`` and
8
- ``shapes.Cell``), and :data:`TEXT_CELL`, ``nuverse.text``, the Cell
9
- document editing works on;
8
+ ``shapes.Cell``);
10
9
  - :class:`Content`, a kind of findable item, ``class
11
10
  TextContent(nuspace.Content[Text], name="Text")``, registered through a
12
11
  :class:`ContentSpec` (:mod:`nuspace.content`);
@@ -23,7 +22,7 @@ Workers import this package: nothing here may pull in a server.
23
22
 
24
23
  from nuspace import content, ops, shapes, ui
25
24
  from nuspace.content import Content
26
- from nuspace.ext import TEXT_CELL, CellSpec, ContentSpec, Extension, PlaneSpec
25
+ from nuspace.ext import CellSpec, ContentSpec, Extension, PlaneSpec
27
26
  from nuspace.host import open_space
28
27
  from nuspace.ops import env
29
28
  from nuspace.shapes import CellState, PlaneState, Space, States
@@ -38,7 +37,6 @@ from nuspace.system.services import (
38
37
 
39
38
 
40
39
  __all__ = [
41
- "TEXT_CELL",
42
40
  "CellSpec",
43
41
  "CellState",
44
42
  "Content",
@@ -8,10 +8,12 @@
8
8
  What is sent: ``first_run`` once per user and ``space_opened`` per open, by
9
9
  the host however the space is opened (:func:`.opened`), ``space_loaded`` per
10
10
  page load in the browser (through the web device, never from the browser
11
- itself), and ``plane_created``, ``plane_removed``, ``cell_created``,
11
+ itself), ``plane_created``, ``plane_removed``, ``cell_created``,
12
12
  ``cell_removed`` by the ops that do it, in whatever process runs them
13
- (:mod:`nuspace.ops.usage`). Each carries the versions and the machine's
14
- kind, nothing about the space.
13
+ (:mod:`nuspace.ops.usage`), and the ``quickstart_*`` steps nuverse tracks
14
+ through those ops. Each carries the versions and the machine's kind,
15
+ nothing about the space. Those names, :data:`.send.EVENTS`, are all that is
16
+ ever sent: any other is dropped before it is queued.
15
17
 
16
18
  Stdlib only and silent on every failure. Queuing is free and never sends:
17
19
  the sender drops everything when telemetry is off, on a dev install, under
@@ -28,14 +30,14 @@ from .config import (
28
30
  set_telemetry,
29
31
  telemetry_enabled,
30
32
  )
31
- from .send import PREFIX, base_properties, capture, first_run, flush, opened, track
33
+ from .send import EVENTS, PREFIX, base_properties, first_run, flush, opened, track
32
34
 
33
35
 
34
36
  __all__ = [
35
37
  "ENV",
38
+ "EVENTS",
36
39
  "PREFIX",
37
40
  "base_properties",
38
- "capture",
39
41
  "config_path",
40
42
  "distinct_id",
41
43
  "env_override",
@@ -7,8 +7,9 @@ test or a sandbox pointing ``HOME`` elsewhere gets its own.
7
7
 
8
8
  The environment only ever turns telemetry off, whatever the file says:
9
9
  ``NUSPACE_TELEMETRY`` set to ``0``, ``false`` or ``off``, ``DO_NOT_TRACK``
10
- set to anything but that (the cross tool convention), or ``CI`` set the same
11
- way, which every major CI does, so nobody's pipeline is counted as use.
10
+ set to anything but that (the cross tool convention), or ``CI`` or a major
11
+ CI's own variable (``GITHUB_ACTIONS``, ``JENKINS_URL``, ...) set the same
12
+ way, so nobody's pipeline is counted as use.
12
13
  """
13
14
 
14
15
  from __future__ import annotations
@@ -42,8 +43,21 @@ ENV = "NUSPACE_TELEMETRY"
42
43
  #: What ``NUSPACE_TELEMETRY`` reads as off, case ignored.
43
44
  _OFF = frozenset({"0", "false", "off", "no"})
44
45
 
45
- #: Conventions that turn telemetry off when set to anything but empty or off.
46
- _OFF_WHEN_SET = ("DO_NOT_TRACK", "CI")
46
+ #: Conventions that turn telemetry off when set to anything but empty or off:
47
+ #: the cross tool one, then ``CI`` and the variables CIs set of their own.
48
+ _OFF_WHEN_SET = (
49
+ "DO_NOT_TRACK",
50
+ "CI",
51
+ "GITHUB_ACTIONS",
52
+ "GITLAB_CI",
53
+ "BUILDKITE",
54
+ "CIRCLECI",
55
+ "TF_BUILD",
56
+ "JENKINS_URL",
57
+ "TEAMCITY_VERSION",
58
+ "CODEBUILD_BUILD_ID",
59
+ "BITBUCKET_BUILD_NUMBER",
60
+ )
47
61
 
48
62
 
49
63
  def home() -> Path:
@@ -1,6 +1,6 @@
1
1
  """Fire and forget PostHog capture, no dependency beyond the stdlib.
2
2
 
3
- One sender thread per process, fed by a bounded queue. :func:`capture`
3
+ One sender thread per process, fed by a bounded queue. :func:`track`
4
4
  puts the event's name and time on the queue and returns, O(1): it never
5
5
  waits, never touches the network or the disk, and drops the event when the
6
6
  queue is full. The thread does the rest, the properties, the id, the opt out
@@ -9,9 +9,14 @@ and the JSON, and drains what is queued into one ``/batch/`` POST at a time. Eve
9
9
  breaks because of telemetry. At exit the thread gets a short grace to send
10
10
  what is left.
11
11
 
12
+ Only nuspace's own events are ever queued: a name not in :data:`EVENTS`,
13
+ or anything but a str, is dropped before it reaches the queue, so no caller
14
+ can send a name of its own.
15
+
12
16
  Nothing is sent when telemetry is off (see :mod:`.config`: the flag, or
13
- ``NUSPACE_TELEMETRY``, ``DO_NOT_TRACK`` or ``CI``), on a dev install, or with no token: the thread checks before each POST and drops the
14
- batch. ``NUSPACE_POSTHOG_TOKEN`` and ``NUSPACE_POSTHOG_HOST`` point it at
17
+ ``NUSPACE_TELEMETRY``, ``DO_NOT_TRACK``, ``CI`` or a CI's own variable), on
18
+ a dev install, or with no token: the thread checks before each POST and
19
+ drops the batch. ``NUSPACE_POSTHOG_TOKEN`` and ``NUSPACE_POSTHOG_HOST`` point it at
15
20
  another PostHog project, eg to test.
16
21
  """
17
22
 
@@ -40,10 +45,10 @@ if TYPE_CHECKING:
40
45
 
41
46
 
42
47
  __all__ = [
48
+ "EVENTS",
43
49
  "FIRST_RUN_EVENT",
44
50
  "PREFIX",
45
51
  "base_properties",
46
- "capture",
47
52
  "first_run",
48
53
  "flush",
49
54
  "opened",
@@ -65,6 +70,28 @@ _BATCH_PATH = "/batch/"
65
70
  #: What every event is sent as: the product, then what happened.
66
71
  PREFIX = "nuspace_"
67
72
 
73
+ #: Every event that may be sent, by its name before :data:`PREFIX`. Nothing else ever is.
74
+ EVENTS = frozenset(
75
+ {
76
+ "first_run",
77
+ "space_opened",
78
+ "space_loaded",
79
+ "plane_created",
80
+ "plane_removed",
81
+ "cell_created",
82
+ "cell_removed",
83
+ # nuverse's quickstart, through ``nuspace.ops.track``.
84
+ "quickstart_demo_opened",
85
+ "quickstart_page_made",
86
+ "quickstart_edited",
87
+ "quickstart_put_back",
88
+ "quickstart_moved",
89
+ }
90
+ )
91
+
92
+ #: :data:`EVENTS` as sent, prefixed.
93
+ _SENT = frozenset(f"{PREFIX}{event}" for event in EVENTS)
94
+
68
95
  #: Sent once per user, retried on every open until PostHog acks it.
69
96
  FIRST_RUN_EVENT = f"{PREFIX}first_run"
70
97
 
@@ -253,11 +280,16 @@ def _the_sender() -> _Sender:
253
280
 
254
281
 
255
282
  def _queue(event: str, on_success: Callable[[], None] | None) -> None:
256
- _the_sender().put((event, time.time(), on_success))
283
+ """Put ``event``, a prefixed name, on the queue. Every send goes through here.
284
+
285
+ A name not in :data:`EVENTS`, or anything but a str, is dropped.
286
+ """
287
+ if isinstance(event, str) and event in _SENT:
288
+ _the_sender().put((event, time.time(), on_success))
257
289
 
258
290
 
259
- def capture(event: str) -> None:
260
- """Queue ``event`` for the sender thread. O(1), never waits, never raises."""
291
+ def _capture(event: str) -> None:
292
+ """Queue ``event``, a prefixed name, for the sender thread. O(1), never waits, never raises."""
261
293
  try:
262
294
  _queue(event, None)
263
295
  except Exception: # noqa: S110
@@ -265,9 +297,12 @@ def capture(event: str) -> None:
265
297
 
266
298
 
267
299
  def track(event: str) -> None:
268
- """:func:`capture` ``event`` as nuspace's own, eg ``plane_created``. Anything but a str is dropped."""
300
+ """Queue ``event`` as nuspace's own, eg ``plane_created``. Never raises.
301
+
302
+ Anything not in :data:`EVENTS`, or not a str, is dropped.
303
+ """
269
304
  if isinstance(event, str):
270
- capture(f"{PREFIX}{event}")
305
+ _capture(f"{PREFIX}{event}")
271
306
 
272
307
 
273
308
  def flush(timeout: float = _EXIT_GRACE_SECONDS) -> bool:
@@ -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
  )
@@ -27,13 +27,12 @@ from .registry import (
27
27
  short_name,
28
28
  space_registry,
29
29
  )
30
- from .specs import EMPTY_PLANE, TEXT_CELL, CellSpec, ContentSpec, PlaneSpec
30
+ from .specs import EMPTY_PLANE, CellSpec, ContentSpec, PlaneSpec
31
31
 
32
32
 
33
33
  __all__ = [
34
34
  "EMPTY_PLANE",
35
35
  "GROUP",
36
- "TEXT_CELL",
37
36
  "CellSpec",
38
37
  "ContentSpec",
39
38
  "Extension",
@@ -20,12 +20,7 @@ if TYPE_CHECKING:
20
20
  from nuspace.content import Content
21
21
 
22
22
 
23
- __all__ = ["EMPTY_PLANE", "TEXT_CELL", "CellSpec", "ContentSpec", "PlaneSpec"]
24
-
25
-
26
- #: The ``nuverse.text`` Cell: nuspace's document editing works on its cells.
27
- #: See :class:`CellSpec`.
28
- TEXT_CELL = "nuverse.text"
23
+ __all__ = ["EMPTY_PLANE", "CellSpec", "ContentSpec", "PlaneSpec"]
29
24
 
30
25
 
31
26
  @dataclass(frozen=True)
@@ -91,12 +86,18 @@ EMPTY_PLANE = PlaneSpec(
91
86
  class CellSpec:
92
87
  """Source for one cell, offered from the ``/`` menu.
93
88
 
94
- nuspace's document editing works on the cells of the ``nuverse.text``
95
- Cell (:data:`TEXT_CELL`), the plane's text cells, written like a
96
- document: typing on an empty line starts one, Enter in the title starts
97
- one at the top with the text after the caret, Cmd+Enter in one starts
98
- the next below, and Backspace in an empty one removes it. With no Cell
99
- under that name, typing opens the ``/`` menu and the rest is off.
89
+ A cell made from it keeps the spec's classes: the protocols it promises,
90
+ which the browser's sugar binds to, never to which spec made a cell. A
91
+ class is a name, its promise written here. ``prose``: the cell draws
92
+ exactly one document that registers a kit document handle (a kit
93
+ ``MarkdownRef`` does on its own), and its value is markdown. Any spec
94
+ whose cell keeps that promise declares it, whoever wrote it. A cell made
95
+ from no spec, or from one no longer installed, has no classes and gets
96
+ the plane's core behaviour only.
97
+
98
+ One spec per class is its default, what typing on the input line
99
+ creates: a spec claims it with ``default_for``. When two installed specs
100
+ claim the same class, the first in registration order wins, no error.
100
101
 
101
102
  Args:
102
103
  name: The new cell's name. Installed by a package, it registers
@@ -111,6 +112,13 @@ class CellSpec:
111
112
  come last, under no heading.
112
113
  icon: What the menu draws beside it, spelled as :class:`PlaneSpec`'s
113
114
  ``icon``. ``""`` for the default.
115
+ classes: The protocols its cells keep, ``("prose",)``.
116
+ default_for: The classes it is the default of, each one of
117
+ ``classes``.
118
+
119
+ Raises:
120
+ TypeError: ``classes`` or ``default_for`` is a bare string.
121
+ ValueError: ``default_for`` names a class not in ``classes``.
114
122
  """
115
123
 
116
124
  name: str
@@ -119,6 +127,17 @@ class CellSpec:
119
127
  description: str = ""
120
128
  group: str = ""
121
129
  icon: str = ""
130
+ classes: tuple[str, ...] = ()
131
+ default_for: tuple[str, ...] = ()
132
+
133
+ def __post_init__(self) -> None:
134
+ if isinstance(self.classes, str) or isinstance(self.default_for, str):
135
+ msg = f"CellSpec {self.name!r}: classes and default_for are tuples, eg ('prose',)"
136
+ raise TypeError(msg)
137
+ stray = [c for c in self.default_for if c not in self.classes]
138
+ if stray:
139
+ msg = f"CellSpec {self.name!r}: default_for {stray} not among classes {self.classes}"
140
+ raise ValueError(msg)
122
141
 
123
142
 
124
143
  @dataclass(frozen=True)
@@ -23,7 +23,7 @@ from nuspace import ops
23
23
  from nuspace.ext.registry import registered_name
24
24
  from nuspace.ops.utils import atomic
25
25
  from nuspace.shapes import Space
26
- from nuspace.system.utils import park, snap
26
+ from nuspace.system.utils import snap
27
27
 
28
28
  from .space import open_space
29
29
 
@@ -165,7 +165,7 @@ def with_plane(
165
165
  ]
166
166
  if web and open_browser:
167
167
  steps.append(Browse(nu.Str(f"http://{host}:{port}/") + pid, host, port))
168
- return nu.Sequential(*steps, park())
168
+ return nu.Sequential(*steps, nu.Park())
169
169
 
170
170
  body = nu.let("", opened)
171
171
  if not web:
@@ -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
@@ -48,7 +50,6 @@ from nuspace.system.services.nav import clear_connections
48
50
  from nuspace.system.ui.home import ensure_home
49
51
  from nuspace.system.ui.search import ensure_search
50
52
  from nuspace.system.ui.settings import ensure_settings
51
- from nuspace.system.utils import park
52
53
 
53
54
 
54
55
  if TYPE_CHECKING:
@@ -92,7 +93,7 @@ def init_space(path: str) -> nu.With:
92
93
  Raises:
93
94
  NotASpace: ``path`` holds something other than a space.
94
95
  """
95
- return nu.With(store(path), body=seed_space())
96
+ return nu.With(store(path), body=init_stores() >> seed_space())
96
97
 
97
98
 
98
99
  def open_space(
@@ -155,7 +156,7 @@ def open_space(
155
156
  )
156
157
  factories = _with_session(factories, web_envs)
157
158
  arms.append(served)
158
- arms.append(park() if body is None else body)
159
+ arms.append(nu.Park() if body is None else body)
159
160
  beside = arms[0] if len(arms) == 1 else nu.Race(*arms)
160
161
  kwargs = {} if name is None else {"name": name}
161
162
  return open_kernel(
@@ -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)
@@ -20,11 +20,10 @@ from __future__ import annotations
20
20
  from typing import TYPE_CHECKING
21
21
 
22
22
  import nu
23
- import nustd.kv
24
23
  from nuspace.shapes import Space
25
24
 
26
25
  from .read import cell_plane, cells, plane_exists
27
- from .utils import MintId, atomic
26
+ from .utils import MintId, atomic, listed, snapshot
28
27
 
29
28
 
30
29
  if TYPE_CHECKING:
@@ -48,6 +47,7 @@ __all__ = [
48
47
  "off_running",
49
48
  "plane_interrupt",
50
49
  "plane_kill",
50
+ "plane_live",
51
51
  "plane_run",
52
52
  "plane_stop",
53
53
  "run",
@@ -76,8 +76,13 @@ STOP_GRACE = 10.0
76
76
  _kernel = Space.kernel
77
77
 
78
78
 
79
- def _snap(term: nu.Nu) -> nu.Nu:
80
- return nustd.kv.Snapshot(term, scope=Space)
79
+ def plane_live(plane_id: nu.StrArg) -> nu.List:
80
+ """A plane's live plane run ids, ``[]`` when it has none. Bare read.
81
+
82
+ Its ``planes_running`` entry is there only while it has live runs (the
83
+ last run out takes it away), so a plane with none has no set to read.
84
+ """
85
+ return listed(_kernel.planes_running[plane_id].runs)
81
86
 
82
87
 
83
88
  # --- Running -------------------------------------------------------------------
@@ -134,10 +139,11 @@ def add_plane_run(
134
139
  >> row.by.set(by)
135
140
  >> row.envs.set(_envs(envs))
136
141
  >> row.termination_requested.set(False)
137
- >> row.cells.init({})
138
- >> row.cells_running.init(set())
139
- >> row.latest.init({})
140
- >> row.workers.init(set())
142
+ >> row.cells.init(nu.Dict.create())
143
+ >> row.cells_running.init(nu.Set.create())
144
+ >> row.latest.init(nu.Dict.create())
145
+ >> row.workers.init(nu.Set.create())
146
+ >> row.workers_running.init(nu.Set.create())
141
147
  >> nu.ForEachDo(
142
148
  cells(plane_id),
143
149
  lambda cell: nu.let(
@@ -255,8 +261,8 @@ def _until_ended(run_id: nu.StrArg) -> nu.Nu:
255
261
  Woken by its plane's live runs, which it leaves in the same commit.
256
262
  """
257
263
  live = _kernel.planes_running[_kernel.runs[run_id].plane.fallback("")].runs
258
- return nu.WaitReactive(
259
- _snap(live.on_children_change()), _snap(_kernel.running.contains(run_id).not_())
264
+ return nu.WaitUntil(
265
+ snapshot(live.on_children_change()), snapshot(_kernel.running.contains(run_id).not_())
260
266
  )
261
267
 
262
268
 
@@ -291,7 +297,7 @@ def off_running(run_id: nu.StrArg) -> nu.Nu:
291
297
 
292
298
  def live_runs_of(plane_id: nu.StrArg, body: Callable[[nu.Str], nu.Nu]) -> nu.Nu:
293
299
  """``body(run_id)`` for every live plane run of the plane. No bracket."""
294
- live = nu.list(_kernel.planes_running[plane_id].runs)
300
+ live = plane_live(plane_id)
295
301
  return nu.ForEachDo(live, lambda rid: body(nu.Str(rid)))
296
302
 
297
303
 
@@ -378,7 +384,7 @@ def runs(plane: nu.StrArg | None = None) -> nu.List:
378
384
  A run's cell runs are :func:`cell_runs`; an ended run is read by id with
379
385
  :func:`run`.
380
386
  """
381
- live = _kernel.running if plane is None else _kernel.planes_running[plane].runs
387
+ live = _kernel.running if plane is None else plane_live(plane)
382
388
  return nu.sorted(live).iter().map(lambda rid: _run_row(nu.Str(rid))).to_list()
383
389
 
384
390
 
@@ -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
  ),