beefcot 1.0.3__tar.gz → 1.1.0__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 (139) hide show
  1. {beefcot-1.0.3 → beefcot-1.1.0}/.gitignore +3 -0
  2. {beefcot-1.0.3 → beefcot-1.1.0}/PKG-INFO +13 -1
  3. {beefcot-1.0.3 → beefcot-1.1.0}/README.pypi.md +8 -0
  4. {beefcot-1.0.3 → beefcot-1.1.0}/pyproject.toml +4 -0
  5. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/__init__.py +2 -2
  6. beefcot-1.1.0/src/beefcot/__main__.py +5 -0
  7. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/checks/figures.py +1 -1
  8. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/cli.py +137 -3
  9. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/cli_presentation.py +30 -1
  10. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/cutting_plan/economics.py +227 -35
  11. beefcot-1.1.0/src/beefcot/cutting_plan/eligibility.py +158 -0
  12. beefcot-1.1.0/src/beefcot/cutting_plan/receipt.py +123 -0
  13. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/cutting_plan/run.py +164 -24
  14. beefcot-1.1.0/src/beefcot/cutting_plan/solver.py +179 -0
  15. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model/learners.py +10 -1
  16. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model/transforms.py +14 -0
  17. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model_format/blocks.py +12 -2
  18. beefcot-1.1.0/src/beefcot/project.py +202 -0
  19. beefcot-1.1.0/src/beefcot/project_registry.py +150 -0
  20. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/registry.py +1 -1
  21. beefcot-1.1.0/src/beefcot/regression/assembly.py +397 -0
  22. beefcot-1.1.0/src/beefcot/regression/assembly_network.py +61 -0
  23. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/exhaustive.py +146 -37
  24. beefcot-1.1.0/src/beefcot/regression/exhaustive_parallel.py +110 -0
  25. beefcot-1.1.0/src/beefcot/regression/exhaustive_storage.py +182 -0
  26. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/run.py +6 -1
  27. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/spec.py +35 -6
  28. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/reports/run.py +46 -5
  29. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/runtime/schemas/model-3.schema.json +3 -1
  30. beefcot-1.1.0/src/beefcot/service/__init__.py +5 -0
  31. beefcot-1.1.0/src/beefcot/service/app.py +780 -0
  32. beefcot-1.1.0/src/beefcot/service/auth.py +219 -0
  33. beefcot-1.1.0/src/beefcot/service/errors.py +19 -0
  34. beefcot-1.1.0/src/beefcot/service/history.py +250 -0
  35. beefcot-1.1.0/src/beefcot/service/job_process.py +90 -0
  36. beefcot-1.1.0/src/beefcot/service/model_evidence.py +26 -0
  37. beefcot-1.1.0/src/beefcot/service/plan_analysis.py +246 -0
  38. beefcot-1.1.0/src/beefcot/service/plan_comparison.py +133 -0
  39. beefcot-1.1.0/src/beefcot/service/plan_editor.py +237 -0
  40. beefcot-1.1.0/src/beefcot/service/plan_evidence.py +162 -0
  41. beefcot-1.1.0/src/beefcot/service/plan_export.py +122 -0
  42. beefcot-1.1.0/src/beefcot/service/plan_figures.py +41 -0
  43. beefcot-1.1.0/src/beefcot/service/plan_inputs.py +313 -0
  44. beefcot-1.1.0/src/beefcot/service/plans.py +179 -0
  45. beefcot-1.1.0/src/beefcot/service/projects.py +63 -0
  46. beefcot-1.1.0/src/beefcot/service/purchase_ceiling.py +73 -0
  47. beefcot-1.1.0/src/beefcot/service/runner.py +110 -0
  48. beefcot-1.1.0/src/beefcot/service/saved_plan.py +203 -0
  49. beefcot-1.1.0/src/beefcot/service/server.py +23 -0
  50. beefcot-1.1.0/src/beefcot/service/studies.py +206 -0
  51. beefcot-1.1.0/src/beefcot/service/views.py +509 -0
  52. beefcot-1.0.3/src/beefcot/cutting_plan/solver.py +0 -104
  53. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/anomaly.py +0 -0
  54. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/chain.py +0 -0
  55. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/checks/__init__.py +0 -0
  56. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/checks/anomaly.py +0 -0
  57. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/checks/anomaly_figures.py +0 -0
  58. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/checks/bins.py +0 -0
  59. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/checks/cut_tree.py +0 -0
  60. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/checks/forest.py +0 -0
  61. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/checks/report.py +0 -0
  62. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/checks/review.py +0 -0
  63. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/checks/run.py +0 -0
  64. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/checks/spec.py +0 -0
  65. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/checks/table.py +0 -0
  66. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/cutting_plan/__init__.py +0 -0
  67. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/decomposition/__init__.py +0 -0
  68. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/decomposition/analysis.py +0 -0
  69. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/decomposition/fits.py +0 -0
  70. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/decomposition/report.py +0 -0
  71. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/decomposition/run.py +0 -0
  72. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/decomposition/sources.py +0 -0
  73. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/decomposition/spec.py +0 -0
  74. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/diagnostics/__init__.py +0 -0
  75. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/diagnostics/analysis.py +0 -0
  76. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/diagnostics/methods.py +0 -0
  77. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/diagnostics/plots.py +0 -0
  78. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/diagnostics/report.py +0 -0
  79. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/diagnostics/run.py +0 -0
  80. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/diagnostics/sources.py +0 -0
  81. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/diagnostics/spec.py +0 -0
  82. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model/__init__.py +0 -0
  83. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model/expression.py +0 -0
  84. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model/functional.py +0 -0
  85. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model/learner_registry.py +0 -0
  86. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model/rows.py +0 -0
  87. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model/runtime.py +0 -0
  88. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model/topology.py +0 -0
  89. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model/types.py +0 -0
  90. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model_format/__init__.py +0 -0
  91. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model_format/card.py +0 -0
  92. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model_format/documents.py +0 -0
  93. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model_format/export.py +0 -0
  94. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model_format/fitted.py +0 -0
  95. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model_format/library.py +0 -0
  96. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/model_format/schema.py +0 -0
  97. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/prediction/__init__.py +0 -0
  98. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/prediction/run.py +0 -0
  99. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/producers/__init__.py +0 -0
  100. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/producers/network.py +0 -0
  101. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/producers/rf/__init__.py +0 -0
  102. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/producers/rf/loop.py +0 -0
  103. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/producers/rf/relevance.py +0 -0
  104. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/__init__.py +0 -0
  105. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/anomaly.py +0 -0
  106. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/candidates.py +0 -0
  107. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/factory.py +0 -0
  108. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/mapping_review.py +0 -0
  109. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/mapping_space.py +0 -0
  110. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/provenance.py +0 -0
  111. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/rerf.py +0 -0
  112. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/rerf_checkpoint.py +0 -0
  113. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/rerf_controller.py +0 -0
  114. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/rerf_effort.py +0 -0
  115. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/rerf_equivalence.py +0 -0
  116. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/rerf_policy.py +0 -0
  117. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/rerf_selection.py +0 -0
  118. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/rerf_settings.py +0 -0
  119. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/rerf_warm_start.py +0 -0
  120. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/residual.py +0 -0
  121. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/rf_expression.py +0 -0
  122. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/regression/table.py +0 -0
  123. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/reports/__init__.py +0 -0
  124. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/reports/metrics.py +0 -0
  125. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/reports/plots.py +0 -0
  126. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/reports/regression_space.py +0 -0
  127. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/reports/style.py +0 -0
  128. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/resources/cut_tree.default.json +0 -0
  129. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/resources/rerf_space.default.yaml +0 -0
  130. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/runtime/__init__.py +0 -0
  131. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/runtime/expression_ast.py +0 -0
  132. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/runtime/loader.py +0 -0
  133. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/runtime/model.py +0 -0
  134. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/runtime/schemas/checks-3.schema.json +0 -0
  135. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/runtime/schemas/predictors-3.schema.json +0 -0
  136. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/runtime/schemas/request-3.schema.json +0 -0
  137. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/runtime/schemas/result-3.schema.json +0 -0
  138. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/workspace.py +0 -0
  139. {beefcot-1.0.3 → beefcot-1.1.0}/src/beefcot/yamlio.py +0 -0
@@ -28,6 +28,9 @@ coverage.xml
28
28
  # Runtime caches
29
29
  .cache/
30
30
 
31
+ # Workbench passwords (scrypt hashes). Created by `beefcot user add`.
32
+ /config/auth/
33
+
31
34
  # Run records (written under the workspace's runs folder).
32
35
  /runs/
33
36
  /workflow-log/
@@ -1,8 +1,10 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: beefcot
3
- Version: 1.0.3
3
+ Version: 1.1.0
4
4
  Summary: Carcass yield models and cutting plans: one YAML in, one delivered model out
5
5
  Requires-Python: >=3.13
6
+ Requires-Dist: fastapi>=0.115
7
+ Requires-Dist: httpx>=0.27
6
8
  Requires-Dist: jarvisplot>=2.1
7
9
  Requires-Dist: numpy>=2.5
8
10
  Requires-Dist: pandas>=3.0
@@ -10,6 +12,8 @@ Requires-Dist: pytest>=8
10
12
  Requires-Dist: pyyaml>=6.0
11
13
  Requires-Dist: scikit-learn>=1.9
12
14
  Requires-Dist: scipy>=1.18
15
+ Requires-Dist: threadpoolctl>=3.6
16
+ Requires-Dist: uvicorn>=0.32
13
17
  Requires-Dist: xgboost>=3.3
14
18
  Description-Content-Type: text/markdown
15
19
 
@@ -23,8 +27,16 @@ python3 -m pip install beefcot
23
27
  beefcot --help
24
28
  beefcot validate /path/to/job.yaml
25
29
  beefcot fit /path/to/job.yaml
30
+ beefcot project create HERD
31
+ beefcot project list
26
32
  ```
27
33
 
34
+ `beefcot project create` writes one project directory. The machine list
35
+ defaults to `~/.beefcot/projects.json`. Pass `--projects` or set
36
+ `BEEFCOT_PROJECTS` to use another file. `beefcot serve` reads that list and
37
+ opens a loopback workbench. The HTML pages stay with the source checkout and
38
+ are not part of this package.
39
+
28
40
  Each fit starts from a `kind: regression_tree` YAML declaring its input table,
29
41
  feature and target columns, cut-tree mapping, split and regression space.
30
42
  Available methods are profile, RF, DNN, ReRF and exhaustive search. Structural
@@ -8,8 +8,16 @@ python3 -m pip install beefcot
8
8
  beefcot --help
9
9
  beefcot validate /path/to/job.yaml
10
10
  beefcot fit /path/to/job.yaml
11
+ beefcot project create HERD
12
+ beefcot project list
11
13
  ```
12
14
 
15
+ `beefcot project create` writes one project directory. The machine list
16
+ defaults to `~/.beefcot/projects.json`. Pass `--projects` or set
17
+ `BEEFCOT_PROJECTS` to use another file. `beefcot serve` reads that list and
18
+ opens a loopback workbench. The HTML pages stay with the source checkout and
19
+ are not part of this package.
20
+
13
21
  Each fit starts from a `kind: regression_tree` YAML declaring its input table,
14
22
  feature and target columns, cut-tree mapping, split and regression space.
15
23
  Available methods are profile, RF, DNN, ReRF and exhaustive search. Structural
@@ -16,10 +16,14 @@ dependencies = [
16
16
  "pandas>=3.0",
17
17
  "scipy>=1.18",
18
18
  "scikit-learn>=1.9",
19
+ "threadpoolctl>=3.6",
19
20
  "xgboost>=3.3",
20
21
  "PyYAML>=6.0",
21
22
  "JarvisPLOT>=2.1",
22
23
  "pytest>=8",
24
+ "fastapi>=0.115",
25
+ "uvicorn>=0.32",
26
+ "httpx>=0.27",
23
27
  ]
24
28
 
25
29
  [project.scripts]
@@ -6,7 +6,7 @@ Every job is one YAML and one verb, the same in Python as on the command line (`
6
6
 
7
7
  beefcot.fit("config/regression/murdoch_rf.yaml") # kind: regression_tree -> a run with a model
8
8
  beefcot.predict("config/prediction/murdoch_rf_241.yaml") # kind: prediction -> one table
9
- beefcot.plan("config/plan/murdoch_demo.yaml") # kind: cutting_plan -> a plan
9
+ beefcot.plan("config/plan/breakdown.yaml") # kind: cutting_plan -> a plan
10
10
  beefcot.check("config/checks/murdoch_core3_bins.yaml") # kind: bin_variance -> a data check
11
11
  beefcot.decompose("config/decomposition/murdoch_rf.yaml") # kind: error_decomposition -> noise / bias² / variance
12
12
  beefcot.run("config/pipelines/murdoch_smoke.yaml") # kind: pipeline -> several, in order
@@ -24,7 +24,7 @@ from __future__ import annotations
24
24
  from pathlib import Path
25
25
  from typing import Any
26
26
 
27
- __version__ = "1.0.3" # the one place it is written; CHANGELOG.md records each version
27
+ __version__ = "1.1.0" # the one place it is written; CHANGELOG.md records each version
28
28
 
29
29
  __all__ = ["__version__", "fit", "predict", "plan", "check", "decompose", "run", "load_model"]
30
30
 
@@ -0,0 +1,5 @@
1
+ """``python -m beefcot`` is the same command as the ``beefcot`` script."""
2
+
3
+ from beefcot.cli import main
4
+
5
+ raise SystemExit(main())
@@ -79,7 +79,7 @@ def plot_config(
79
79
  "DataSet": [{"name": "animals", "path": "../animals.csv", "type": "csv"},
80
80
  {"name": "cells", "path": "../cells.csv", "type": "csv"}],
81
81
  "Figures": figures,
82
- "output": {"dir": ".", "formats": ["png"]},
82
+ "output": {"dir": ".", "formats": ["svg", "png"]},
83
83
  }
84
84
 
85
85
 
@@ -2,12 +2,19 @@
2
2
 
3
3
  beefcot fit config/regression/murdoch_rf.yaml # kind: regression_tree -> a run with a model
4
4
  beefcot predict config/prediction/murdoch_rf_241.yaml # kind: prediction -> one table
5
- beefcot plan config/plan/murdoch_demo.yaml # kind: cutting_plan -> a plan
5
+ beefcot plan config/plan/breakdown.yaml # kind: cutting_plan -> a plan
6
6
  beefcot check config/checks/murdoch_core3_bins.yaml # kind: bin_variance -> a data check
7
7
  beefcot decompose config/decomposition/murdoch_rf.yaml # kind: error_decomposition -> noise / bias² / variance
8
8
  beefcot run config/pipelines/murdoch_smoke.yaml # kind: pipeline -> several, in order
9
9
  beefcot validate <any YAML> # read and check it, compute nothing
10
10
  beefcot models list | show ID | verify ID | import PATH | export ID OUT
11
+ beefcot serve # loopback workbench; --design draws the column grid
12
+ beefcot user add NAME # create a workbench login
13
+ beefcot project create PATH # a standard project and an empty model library
14
+ beefcot project validate [PATH] # check that project; compute nothing
15
+ beefcot project list # projects recorded on this machine
16
+ beefcot project add PATH # record an existing project
17
+ beefcot project remove ID # drop it from the list; keep the directory
11
18
  beefcot kinds | version
12
19
  beefcot -V | -v | --version # package version, no job required
13
20
 
@@ -35,6 +42,33 @@ PRODUCING = ("fit", "predict", "plan", "check", "decompose", "run")
35
42
  WITH_LIBRARY = ("predict", "plan", "run") # the jobs that may look a model up by id
36
43
 
37
44
 
45
+ def _user(args: argparse.Namespace) -> int:
46
+ """Create or list workbench accounts. The password is read from the terminal."""
47
+ import getpass
48
+
49
+ from beefcot.service.auth import Accounts, AuthError
50
+
51
+ root = Path(args.root).expanduser() if args.root else Path.cwd()
52
+ accounts = Accounts(root / "config" / "auth" / "users.yaml")
53
+ try:
54
+ if args.user_command == "list":
55
+ for name in accounts.names():
56
+ print(name)
57
+ return 0
58
+ if not sys.stdin.isatty():
59
+ raise AuthError(422, "INVALID_PASSWORD", "run beefcot user add in a terminal so the password is not saved in a command")
60
+ password = getpass.getpass("Password: ")
61
+ repeated = getpass.getpass("Repeat password: ")
62
+ if password != repeated:
63
+ raise AuthError(422, "INVALID_PASSWORD", "the two passwords did not match")
64
+ accounts.add(args.name, password)
65
+ except AuthError as failure:
66
+ print(failure.message, file=sys.stderr)
67
+ return 1
68
+ print(f"added {args.name}")
69
+ return 0
70
+
71
+
38
72
  def _produce(args: argparse.Namespace) -> dict[str, Any]:
39
73
  import beefcot
40
74
  from beefcot import registry
@@ -135,6 +169,26 @@ def _models(args: argparse.Namespace) -> dict[str, Any]:
135
169
  return {"ok": True, "id": args.id, "written": str(written)}
136
170
 
137
171
 
172
+ def _project(args: argparse.Namespace) -> dict[str, Any]:
173
+ """Create or check a project, or change the machine list. None of these fit a model."""
174
+ from beefcot.project import create, validate
175
+ from beefcot.project_registry import add, describe, location, remove
176
+
177
+ registry = location(getattr(args, "projects", None))
178
+ if args.project_command == "create":
179
+ report = create(args.path)
180
+ report.update(add(registry, report["project"]))
181
+ report["registry"] = str(registry)
182
+ return report
183
+ if args.project_command == "add":
184
+ return {"ok": True, "registry": str(registry), **add(registry, args.path)}
185
+ if args.project_command == "remove":
186
+ return {"ok": True, "registry": str(registry), **remove(registry, args.id)}
187
+ if args.project_command == "list":
188
+ return {"ok": True, "registry": str(registry), "projects": describe(registry)}
189
+ return validate(args.path or Path.cwd())
190
+
191
+
138
192
  def _kinds() -> dict[str, Any]:
139
193
  from beefcot import registry
140
194
 
@@ -146,9 +200,10 @@ def _parser(presentation: Terminal | None = None) -> argparse.ArgumentParser:
146
200
  parser = HelpParser(prog="beefcot", presentation=presentation,
147
201
  description="Define a job in YAML. Validate it, run it, inspect the delivered results.",
148
202
  help_panels=[('Regression & data', ('validate', 'fit', 'predict', 'check', 'decompose')),
149
- ('Planning & pipelines', ('plan', 'run')),
203
+ ('Planning & pipelines', ('plan', 'run', 'serve', 'user', 'project')),
150
204
  ('Models & reference', ('models', 'kinds', 'version'))],
151
205
  examples=[('Validate', 'beefcot validate path/to/job.yaml'),
206
+ ('New project', 'beefcot project create HERD'),
152
207
  ('Fit', 'beefcot fit config/regression/murdoch_core3_rf.yaml'),
153
208
  ('Command help', 'beefcot fit --help')])
154
209
  parser.add_argument('-V', '-v', '--version', action=VersionAction,
@@ -186,6 +241,63 @@ def _parser(presentation: Terminal | None = None) -> argparse.ArgumentParser:
186
241
  command(name, helps[name], library=name in WITH_LIBRARY, quiet=True).add_argument("config", metavar="YAML", help="job configuration; paths inside it are relative to this YAML")
187
242
  command("validate", "validate a YAML and its paths without running the job", library=True).add_argument(
188
243
  "config", metavar="YAML", help="the configuration to validate")
244
+ serve = sub.add_parser("serve", help="serve the local cutting-plan workbench on 127.0.0.1",
245
+ description="Serve the local cutting-plan workbench. The host is loopback only.",
246
+ examples=[("Open", "beefcot serve"), ("Grid", "beefcot serve --design")])
247
+ presentation_options(serve)
248
+ serve.add_argument("--port", type=int, default=8765, help="loopback port (default: 8765)")
249
+ serve.add_argument("--root", default=None, help="project directory that contains config/ (default: the current directory)")
250
+ serve.add_argument("--design", action="store_true", help="draw the 12-column reference grid in magenta")
251
+ serve.add_argument("--projects", default=None, help="the machine project list (default: ~/.beefcot/projects.json)")
252
+
253
+ account = sub.add_parser("user", help="add or list workbench logins",
254
+ description="Add a workbench login. The password is typed, not passed as an argument. "
255
+ "The web page cannot register a user.",
256
+ examples=[("Add", "beefcot user add NAME"), ("List", "beefcot user list")])
257
+ presentation_options(account)
258
+ account_commands = account.add_subparsers(dest="user_command", required=True)
259
+ added = account_commands.add_parser("add", help="create one user in config/auth/users.yaml")
260
+ presentation_options(added)
261
+ added.add_argument("name", help="the name typed on the login page")
262
+ added.add_argument("--root", default=None, help="project directory that contains config/ (default: the current directory)")
263
+ listed = account_commands.add_parser("list", help="list user names")
264
+ presentation_options(listed)
265
+ listed.add_argument("--root", default=None, help="project directory that contains config/ (default: the current directory)")
266
+
267
+ project = sub.add_parser(
268
+ "project",
269
+ help="create or validate a project directory",
270
+ description="A project holds tables, job YAML and a model library. The package stays separate.",
271
+ examples=[("Create", "beefcot project create HERD"),
272
+ ("Validate", "beefcot project validate HERD"),
273
+ ("Register", "beefcot project add HERD"),
274
+ ("On this machine", "beefcot project list")],
275
+ )
276
+ presentation_options(project)
277
+ project_commands = project.add_subparsers(dest="project_command", required=True)
278
+
279
+ def project_command(name: str, help_text: str, description: str) -> argparse.ArgumentParser:
280
+ made = project_commands.add_parser(name, help=help_text, description=description)
281
+ presentation_options(made)
282
+ made.add_argument("--projects", default=None, help="the machine project list (default: ~/.beefcot/projects.json)")
283
+ return made
284
+
285
+ created = project_command("create", "write a standard project and an empty model library",
286
+ "Write a standard project and an empty model library. "
287
+ "The directory must be missing or empty. No table is copied. "
288
+ "The new project is recorded in the machine list.")
289
+ created.add_argument("path", help="the new project directory")
290
+ checked = project_command("validate", "check a project directory and compute nothing",
291
+ "Check a project directory and compute nothing.")
292
+ checked.add_argument("path", nargs="?", default=None, help="the project directory (default: the current directory)")
293
+ project_command("list", "list the projects recorded on this machine",
294
+ "List the projects recorded on this machine.")
295
+ added = project_command("add", "record an existing project directory",
296
+ "Record an existing project directory. The directory is not copied.")
297
+ added.add_argument("path", help="the project directory")
298
+ removed = project_command("remove", "drop a project from the machine list",
299
+ "Drop a project from the machine list. The directory is kept.")
300
+ removed.add_argument("id", help="the project id")
189
301
 
190
302
  models = sub.add_parser("models", help="inspect, verify and move delivered models",
191
303
  description="Inspect, verify and move delivered models.",
@@ -224,6 +336,21 @@ def main(argv: Sequence[str] | None = None) -> int:
224
336
  parser.print_help()
225
337
  return 0
226
338
  args = parser.parse_args(raw)
339
+ if args.command == "serve":
340
+ from beefcot.service.errors import ServiceError
341
+ from beefcot.service.server import serve
342
+
343
+ try:
344
+ serve(Path(args.root).expanduser() if args.root else Path.cwd(), args.port,
345
+ design=args.design, projects=args.projects)
346
+ except ServiceError as failure:
347
+ print(failure.message, file=sys.stderr)
348
+ return 1
349
+ except KeyboardInterrupt:
350
+ return 130
351
+ return 0
352
+ if args.command == "user":
353
+ return _user(args)
227
354
  ui.as_json = getattr(args, 'json', False)
228
355
  ui.no_color = getattr(args, 'no_color', False)
229
356
  quiet = getattr(args, 'quiet', False)
@@ -241,6 +368,8 @@ def main(argv: Sequence[str] | None = None) -> int:
241
368
  result = _produce(args)
242
369
  elif args.command == "validate":
243
370
  result = _validate(args)
371
+ elif args.command == "project":
372
+ result = _project(args)
244
373
  elif args.command == "models":
245
374
  result = _models(args)
246
375
  elif args.command == "kinds":
@@ -254,7 +383,12 @@ def main(argv: Sequence[str] | None = None) -> int:
254
383
  result = {"ok": False, "error": "Interrupted by user."}
255
384
  except Exception as failure: # one structured failure, without a traceback
256
385
  result = {"ok": False, "error": f"{type(failure).__name__}: {failure}"}
257
- label = f'models {args.models_command}' if args.command == 'models' else args.command
386
+ if args.command == "models":
387
+ label = f"models {args.models_command}"
388
+ elif args.command == "project":
389
+ label = f"project {args.project_command}"
390
+ else:
391
+ label = args.command
258
392
  ui.result(result, label, time.monotonic() - started)
259
393
  return 130 if interrupted else 0 if result["ok"] else 1
260
394
 
@@ -159,6 +159,33 @@ class Terminal:
159
159
  return
160
160
  if 'kinds' in result:
161
161
  rows = [(item['kind'], f"{item['command']}\n{item['does']}") for item in result['kinds']]
162
+ elif 'project' in result and 'workspace' in result:
163
+ rows = []
164
+ if result.get('id'):
165
+ rows.append(('Id', result['id']))
166
+ if result.get('name'):
167
+ rows.append(('Name', result['name']))
168
+ rows += [('Project', result['project']), ('Workspace', result['workspace'] or 'Missing')]
169
+ rows.append(('Libraries', '\n'.join(result.get('models') or []) or 'None'))
170
+ rows.append(('Config', ', '.join(result.get('config') or []) or 'None'))
171
+ if result.get('registry'):
172
+ rows.append(('Registry', result['registry']))
173
+ if elapsed is not None:
174
+ rows.append(('Elapsed', f'{elapsed:.2f}s'))
175
+ elif result.get('registry') and isinstance(result.get('projects'), list):
176
+ rows = [('Registry', result['registry'])]
177
+ if not result['projects']:
178
+ rows.append(('Projects', 'None'))
179
+ for item in result['projects']:
180
+ state = 'ready' if item.get('ready') else 'not ready'
181
+ rows.append((item['id'], f"{item.get('name') or item['id']}\n{item.get('path', '')}\n{state}"))
182
+ elif result.get('registry') and result.get('id'):
183
+ rows = [('Id', result['id'])]
184
+ if result.get('name'):
185
+ rows.append(('Name', result['name']))
186
+ if result.get('path'):
187
+ rows.append(('Path', result['path']))
188
+ rows.append(('Registry', result['registry']))
162
189
  elif isinstance(result.get('models'), list) and 'libraries' in result:
163
190
  rows = [('Library', place) for place in result['libraries']]
164
191
  rows += [(f'{index:02d}', f"{item['id']}\n{item['producer']} / {item['products']} products / {item['created_at'][:10]}")
@@ -195,7 +222,9 @@ class Terminal:
195
222
  rows.append(('Report', str(Path(output) / 'report.html')))
196
223
  if elapsed is not None:
197
224
  rows.append(('Elapsed', f'{elapsed:.2f}s'))
198
- titles = {'validate': 'VALID YAML', 'kinds': 'YAML KINDS', 'models list': 'MODEL LIBRARY',
225
+ titles = {'validate': 'VALID YAML', 'kinds': 'YAML KINDS',
226
+ 'project list': 'PROJECTS', 'project add': 'PROJECT ADDED', 'project remove': 'PROJECT REMOVED',
227
+ 'models list': 'MODEL LIBRARY',
199
228
  'models show': 'MODEL DETAILS', 'models verify': 'MODEL VERIFIED',
200
229
  'models import': 'MODEL IMPORTED', 'models export': 'MODEL EXPORTED'}
201
230
  self.write(self.panel(titles.get(command, f'{command.upper()} COMPLETE'), rows, color='32'), self.out)