peaches 0.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.
- peaches-0.1.0/LICENSE +21 -0
- peaches-0.1.0/PKG-INFO +77 -0
- peaches-0.1.0/README.md +54 -0
- peaches-0.1.0/cli_tests/test_cli.py +27 -0
- peaches-0.1.0/profile_tests/test_native_review.py +57 -0
- peaches-0.1.0/profile_tests/test_profiles.py +40 -0
- peaches-0.1.0/pyproject.toml +43 -0
- peaches-0.1.0/pyproject.toml.orig +34 -0
- peaches-0.1.0/scripts/qualify.py +46 -0
- peaches-0.1.0/src/peaches/__init__.py +5 -0
- peaches-0.1.0/src/peaches/__main__.py +3 -0
- peaches-0.1.0/src/peaches/cli.py +51 -0
- peaches-0.1.0/src/peaches/py.typed +0 -0
- peaches-0.1.0/src/peaches/workbench.py +60 -0
- peaches-0.1.0/tests/test_workbench.py +22 -0
peaches-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ella Inng
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
peaches-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: peaches
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A selected text workflow workbench over independent Python composition libraries.
|
|
5
|
+
Author: ellainng
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: wispsy>=0.0.1
|
|
9
|
+
Requires-Dist: scriptty>=0.1.0
|
|
10
|
+
Requires-Dist: scriptty[cli]>=0.1.0 ; extra == 'cli'
|
|
11
|
+
Requires-Dist: scriptty[sequence]>=0.1.0 ; python_full_version >= '3.14' and extra == 'sequence'
|
|
12
|
+
Requires-Dist: wispsy[table]>=0.0.1 ; python_full_version >= '3.14' and extra == 'table'
|
|
13
|
+
Requires-Dist: scriptty[terminal]>=0.1.0 ; python_full_version >= '3.14' and extra == 'terminal'
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Project-URL: Source, https://github.com/wispsy/peaches
|
|
16
|
+
Project-URL: Issues, https://github.com/wispsy/peaches/issues
|
|
17
|
+
Project-URL: Project, https://github.com/orgs/wispsy/projects/23
|
|
18
|
+
Provides-Extra: cli
|
|
19
|
+
Provides-Extra: sequence
|
|
20
|
+
Provides-Extra: table
|
|
21
|
+
Provides-Extra: terminal
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Peaches
|
|
25
|
+
|
|
26
|
+
A text workflow workbench: inspect, normalize and report text, then optionally
|
|
27
|
+
review progress in a terminal or consume typed tables and Markdown.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import asyncio
|
|
31
|
+
from peaches import TextWorkbench
|
|
32
|
+
|
|
33
|
+
workbench = TextWorkbench()
|
|
34
|
+
outcome = asyncio.run(workbench.report(" Hello WORLD "))
|
|
35
|
+
assert outcome["outputs"]["report"] == {"text": "hello world", "words": 2, "characters": 11}
|
|
36
|
+
document = workbench.inspect()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The library and optional CLI support Python 3.11+. Lifecycle, table and terminal
|
|
40
|
+
profiles require Python 3.14+. Install selected source profiles from this checkout:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
uv run --python 3.11 --extra cli peaches report ' Hello WORLD '
|
|
44
|
+
uv run --python 3.11 --extra cli peaches inspect
|
|
45
|
+
uv run --python 3.14 --extra cli --extra sequence peaches report ' Hello WORLD ' --checkpoint progress.json --pace function
|
|
46
|
+
uv run --python 3.14 --extra cli --extra sequence peaches report ' Hello WORLD ' --checkpoint progress.json --resume
|
|
47
|
+
uv run --python 3.14 --extra cli --extra table peaches markdown --text ' Hello WORLD ' --text ' More WORDS '
|
|
48
|
+
uv run --python 3.14 --extra terminal peaches review ' Hello WORLD ' --checkpoint review.json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Run requires a new checkpoint path; --resume requires existing progress. --retry
|
|
52
|
+
explicitly repeats an uncertain step; --restart repeats the workflow. Outcomes
|
|
53
|
+
use Scriptty's schema-1 JSON contract. Commands keep UTF-8 results on stdout and
|
|
54
|
+
diagnostics/errors on stderr independently of console encoding.
|
|
55
|
+
|
|
56
|
+
`workbench.table(texts)` returns the existing typed Headless Spreadsheets Table;
|
|
57
|
+
`markdown(texts)` selects its detached, escaped renderer. `terminal_model` supplies
|
|
58
|
+
the same Scriptty/Carets model for embedding/testing; `await review(...)` selects
|
|
59
|
+
native terminal ownership. The native host requires a supported POSIX TTY. Every
|
|
60
|
+
surface uses the same curated text functions, explicit mappings and selected
|
|
61
|
+
capabilities. Terminal review requires capability confirmation before execution.
|
|
62
|
+
|
|
63
|
+
Peaches composes Wispsy's typed reference, Hidden Moves catalogs, moveset pipelines
|
|
64
|
+
and Scriptty's passive documents/application API. Func_seq owns optional durable
|
|
65
|
+
execution; Carets owns optional terminal behavior; Headless Spreadsheets owns
|
|
66
|
+
typed tables and rendering. The workbench introduces no second parser/executor
|
|
67
|
+
or new grammar. Inspect returns the existing passive schema-1 document.
|
|
68
|
+
|
|
69
|
+
This is a functional source-development package at 0.0.1.dev0 with immutable
|
|
70
|
+
sibling revisions. It is independently versioned and requires no hosted service.
|
|
71
|
+
Release/license/dependency publication choices remain in the
|
|
72
|
+
[Wispsy Composition Project](https://github.com/orgs/wispsy/projects/23).
|
|
73
|
+
Build with `uv build --out-dir dist`; `python scripts/qualify.py dist` installs
|
|
74
|
+
wheel/source artifacts outside the checkout, checks optional-profile isolation,
|
|
75
|
+
actual commands/restart, synthetic terminal/table equivalence and native POSIX
|
|
76
|
+
keyboard selection/run/quit with terminal restoration. CI qualifies Python 3.11
|
|
77
|
+
on Linux/Windows and Python 3.14 on Linux/macOS/Windows.
|
peaches-0.1.0/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Peaches
|
|
2
|
+
|
|
3
|
+
A text workflow workbench: inspect, normalize and report text, then optionally
|
|
4
|
+
review progress in a terminal or consume typed tables and Markdown.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
import asyncio
|
|
8
|
+
from peaches import TextWorkbench
|
|
9
|
+
|
|
10
|
+
workbench = TextWorkbench()
|
|
11
|
+
outcome = asyncio.run(workbench.report(" Hello WORLD "))
|
|
12
|
+
assert outcome["outputs"]["report"] == {"text": "hello world", "words": 2, "characters": 11}
|
|
13
|
+
document = workbench.inspect()
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The library and optional CLI support Python 3.11+. Lifecycle, table and terminal
|
|
17
|
+
profiles require Python 3.14+. Install selected source profiles from this checkout:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
uv run --python 3.11 --extra cli peaches report ' Hello WORLD '
|
|
21
|
+
uv run --python 3.11 --extra cli peaches inspect
|
|
22
|
+
uv run --python 3.14 --extra cli --extra sequence peaches report ' Hello WORLD ' --checkpoint progress.json --pace function
|
|
23
|
+
uv run --python 3.14 --extra cli --extra sequence peaches report ' Hello WORLD ' --checkpoint progress.json --resume
|
|
24
|
+
uv run --python 3.14 --extra cli --extra table peaches markdown --text ' Hello WORLD ' --text ' More WORDS '
|
|
25
|
+
uv run --python 3.14 --extra terminal peaches review ' Hello WORLD ' --checkpoint review.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run requires a new checkpoint path; --resume requires existing progress. --retry
|
|
29
|
+
explicitly repeats an uncertain step; --restart repeats the workflow. Outcomes
|
|
30
|
+
use Scriptty's schema-1 JSON contract. Commands keep UTF-8 results on stdout and
|
|
31
|
+
diagnostics/errors on stderr independently of console encoding.
|
|
32
|
+
|
|
33
|
+
`workbench.table(texts)` returns the existing typed Headless Spreadsheets Table;
|
|
34
|
+
`markdown(texts)` selects its detached, escaped renderer. `terminal_model` supplies
|
|
35
|
+
the same Scriptty/Carets model for embedding/testing; `await review(...)` selects
|
|
36
|
+
native terminal ownership. The native host requires a supported POSIX TTY. Every
|
|
37
|
+
surface uses the same curated text functions, explicit mappings and selected
|
|
38
|
+
capabilities. Terminal review requires capability confirmation before execution.
|
|
39
|
+
|
|
40
|
+
Peaches composes Wispsy's typed reference, Hidden Moves catalogs, moveset pipelines
|
|
41
|
+
and Scriptty's passive documents/application API. Func_seq owns optional durable
|
|
42
|
+
execution; Carets owns optional terminal behavior; Headless Spreadsheets owns
|
|
43
|
+
typed tables and rendering. The workbench introduces no second parser/executor
|
|
44
|
+
or new grammar. Inspect returns the existing passive schema-1 document.
|
|
45
|
+
|
|
46
|
+
This is a functional source-development package at 0.0.1.dev0 with immutable
|
|
47
|
+
sibling revisions. It is independently versioned and requires no hosted service.
|
|
48
|
+
Release/license/dependency publication choices remain in the
|
|
49
|
+
[Wispsy Composition Project](https://github.com/orgs/wispsy/projects/23).
|
|
50
|
+
Build with `uv build --out-dir dist`; `python scripts/qualify.py dist` installs
|
|
51
|
+
wheel/source artifacts outside the checkout, checks optional-profile isolation,
|
|
52
|
+
actual commands/restart, synthetic terminal/table equivalence and native POSIX
|
|
53
|
+
keyboard selection/run/quit with terminal restoration. CI qualifies Python 3.11
|
|
54
|
+
on Linux/Windows and Python 3.14 on Linux/macOS/Windows.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
from wispsy.reference import run_reference
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InstalledCommandTests(unittest.TestCase):
|
|
13
|
+
def test_module_and_console_entrypoints_match_library_outcomes(self):
|
|
14
|
+
text = " HéLLo 🌍 "
|
|
15
|
+
console = Path(sys.executable).with_name("peaches.exe" if sys.platform == "win32" else "peaches")
|
|
16
|
+
with tempfile.TemporaryDirectory() as outside:
|
|
17
|
+
for prefix in ([sys.executable, "-I", "-m", "peaches"], [str(console)]):
|
|
18
|
+
result = subprocess.run([*prefix, "report", text], cwd=outside, env={**os.environ, "PYTHONIOENCODING": "cp1252"}, capture_output=True)
|
|
19
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
20
|
+
self.assertEqual(json.loads(result.stdout)["outputs"], {"report": run_reference(text)})
|
|
21
|
+
inspected = subprocess.run([str(console), "inspect"], cwd=outside, capture_output=True)
|
|
22
|
+
self.assertEqual(inspected.returncode, 0, inspected.stderr)
|
|
23
|
+
self.assertEqual(json.loads(inspected.stdout)["status"], "inspected")
|
|
24
|
+
failure = subprocess.run([str(console), "report"], cwd=outside, capture_output=True)
|
|
25
|
+
self.assertEqual(failure.returncode, 2)
|
|
26
|
+
self.assertEqual(failure.stdout, b"")
|
|
27
|
+
self.assertEqual(json.loads(failure.stderr.splitlines()[-1])["schema"], 1)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import time
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@unittest.skipUnless(os.name == "posix", "Native POSIX review host")
|
|
12
|
+
class NativeReviewTests(unittest.TestCase):
|
|
13
|
+
def test_actual_terminal_selection_run_and_quit_restore_the_session(self):
|
|
14
|
+
import fcntl
|
|
15
|
+
import pty
|
|
16
|
+
import select
|
|
17
|
+
import struct
|
|
18
|
+
import termios
|
|
19
|
+
|
|
20
|
+
master, slave = pty.openpty()
|
|
21
|
+
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", 24, 80, 0, 0))
|
|
22
|
+
original = termios.tcgetattr(slave)
|
|
23
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
24
|
+
checkpoint = Path(directory) / "review.json"
|
|
25
|
+
process = subprocess.Popen([sys.executable, "-I", "-m", "peaches", "review", " Hello WORLD ",
|
|
26
|
+
"--checkpoint", str(checkpoint)], stdin=slave, stderr=slave, stdout=subprocess.PIPE, cwd=directory)
|
|
27
|
+
def drain():
|
|
28
|
+
if select.select([master], [], [], 0.02)[0]:
|
|
29
|
+
os.read(master, 65536)
|
|
30
|
+
try:
|
|
31
|
+
deadline = time.monotonic() + 5
|
|
32
|
+
while termios.tcgetattr(slave)[3] & termios.ICANON and process.poll() is None and time.monotonic() < deadline:
|
|
33
|
+
drain()
|
|
34
|
+
self.assertFalse(termios.tcgetattr(slave)[3] & termios.ICANON, "Native raw session did not start")
|
|
35
|
+
# Input editor → explicitly select both capabilities → actions → run.
|
|
36
|
+
os.write(master, b"\t \x1b[B \t\r")
|
|
37
|
+
completed = False
|
|
38
|
+
deadline = time.monotonic() + 5
|
|
39
|
+
while process.poll() is None and time.monotonic() < deadline:
|
|
40
|
+
drain()
|
|
41
|
+
if checkpoint.exists() and json.loads(checkpoint.read_text())["status"] == "completed":
|
|
42
|
+
completed = True
|
|
43
|
+
break
|
|
44
|
+
self.assertTrue(completed, "Native keyboard execution did not complete")
|
|
45
|
+
os.write(master, b"q")
|
|
46
|
+
output, _ = process.communicate(timeout=5)
|
|
47
|
+
self.assertEqual(process.returncode, 0, output)
|
|
48
|
+
self.assertEqual(json.loads(output)["outputs"], {"report": {"text": "hello world", "words": 2, "characters": 11}})
|
|
49
|
+
restored = termios.tcgetattr(slave)
|
|
50
|
+
mask = termios.ICANON | termios.ECHO
|
|
51
|
+
self.assertEqual(restored[3] & mask, original[3] & mask)
|
|
52
|
+
finally:
|
|
53
|
+
if process.poll() is None:
|
|
54
|
+
process.kill()
|
|
55
|
+
process.communicate()
|
|
56
|
+
os.close(master)
|
|
57
|
+
os.close(slave)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
from carets import KeyMsg
|
|
9
|
+
from carets.testing import TestProgram
|
|
10
|
+
from peaches import TextWorkbench
|
|
11
|
+
from wispsy.reference import run_reference
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProfileTests(unittest.IsolatedAsyncioTestCase):
|
|
15
|
+
async def test_typed_table_markdown_and_terminal_reuse_report_contract(self):
|
|
16
|
+
workbench, text = TextWorkbench(), " Hello WORLD "
|
|
17
|
+
self.assertEqual(workbench.table([text]).to_records(), [run_reference(text)])
|
|
18
|
+
self.assertIn("<script>", workbench.markdown(["<script>"]))
|
|
19
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
20
|
+
model = workbench.terminal_model(text, checkpoint=str(Path(directory) / "terminal.json"))
|
|
21
|
+
test = TestProgram(model)
|
|
22
|
+
model.attach(test.program)
|
|
23
|
+
async with test:
|
|
24
|
+
await test.send(KeyMsg("tab"), KeyMsg(" "), KeyMsg("down"), KeyMsg(" "), KeyMsg("tab"), KeyMsg("enter"))
|
|
25
|
+
await test.wait_for(lambda: model.status == "completed")
|
|
26
|
+
self.assertEqual(model.outcome["outputs"], {"report": run_reference(text)})
|
|
27
|
+
|
|
28
|
+
async def test_installed_commands_pause_and_resume_in_separate_processes(self):
|
|
29
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
30
|
+
checkpoint = str(Path(directory) / "progress.json")
|
|
31
|
+
def command(*options):
|
|
32
|
+
result = subprocess.run([sys.executable, "-I", "-m", "peaches", "report", " Hello WORLD ", "--checkpoint", checkpoint, *options], cwd=directory, capture_output=True)
|
|
33
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
34
|
+
return json.loads(result.stdout)
|
|
35
|
+
first = command("--pace", "function")
|
|
36
|
+
self.assertEqual((first["status"], first["completed"]), ("paused", ["normalize"]))
|
|
37
|
+
resumed = command("--resume")
|
|
38
|
+
self.assertEqual(resumed["status"], "completed")
|
|
39
|
+
self.assertEqual(resumed["run_id"], first["run_id"])
|
|
40
|
+
self.assertEqual(resumed["outputs"], {"report": run_reference(" Hello WORLD ")})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "peaches"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
license = "MIT"
|
|
5
|
+
license-files = ["LICENSE"]
|
|
6
|
+
description = "A selected text workflow workbench over independent Python composition libraries."
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"wispsy>=0.0.1",
|
|
11
|
+
"scriptty>=0.1.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[project.authors]]
|
|
15
|
+
name = "ellainng"
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
cli = ["scriptty[cli]>=0.1.0"]
|
|
19
|
+
sequence = ["scriptty[sequence]>=0.1.0 ; python_version >= '3.14'"]
|
|
20
|
+
terminal = ["scriptty[terminal]>=0.1.0 ; python_version >= '3.14'"]
|
|
21
|
+
table = ["wispsy[table]>=0.0.1 ; python_version >= '3.14'"]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
peaches = "peaches.cli:main"
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Source = "https://github.com/wispsy/peaches"
|
|
28
|
+
Issues = "https://github.com/wispsy/peaches/issues"
|
|
29
|
+
Project = "https://github.com/orgs/wispsy/projects/23"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["uv_build>=0.12.19,<0.13.0"]
|
|
33
|
+
build-backend = "uv_build"
|
|
34
|
+
|
|
35
|
+
[tool.uv.build-backend]
|
|
36
|
+
source-include = [
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"tests/*.py",
|
|
39
|
+
"cli_tests/*.py",
|
|
40
|
+
"profile_tests/*.py",
|
|
41
|
+
"scripts/*.py",
|
|
42
|
+
"docs/*.md",
|
|
43
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "peaches"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
license = "MIT"
|
|
5
|
+
license-files = ["LICENSE"]
|
|
6
|
+
description = "A selected text workflow workbench over independent Python composition libraries."
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
authors = [{ name = "ellainng" }]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"wispsy>=0.0.1",
|
|
12
|
+
"scriptty>=0.1.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
cli = ["scriptty[cli]>=0.1.0"]
|
|
17
|
+
sequence = ["scriptty[sequence]>=0.1.0 ; python_version >= '3.14'"]
|
|
18
|
+
terminal = ["scriptty[terminal]>=0.1.0 ; python_version >= '3.14'"]
|
|
19
|
+
table = ["wispsy[table]>=0.0.1 ; python_version >= '3.14'"]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
peaches = "peaches.cli:main"
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Source = "https://github.com/wispsy/peaches"
|
|
26
|
+
Issues = "https://github.com/wispsy/peaches/issues"
|
|
27
|
+
Project = "https://github.com/orgs/wispsy/projects/23"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["uv_build>=0.12.19,<0.13.0"]
|
|
31
|
+
build-backend = "uv_build"
|
|
32
|
+
|
|
33
|
+
[tool.uv.build-backend]
|
|
34
|
+
source-include = ["LICENSE", "tests/*.py", "cli_tests/*.py", "profile_tests/*.py", "scripts/*.py", "docs/*.md"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Install both workbench artifacts outside source and select optional profiles."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import zipfile
|
|
10
|
+
from email.parser import BytesParser
|
|
11
|
+
|
|
12
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
directory = Path(sys.argv[1]).resolve()
|
|
17
|
+
artifacts = sorted([*directory.glob("*.whl"), *directory.glob("*.tar.gz")])
|
|
18
|
+
assert len(artifacts) == 2
|
|
19
|
+
wheel, = directory.glob("*.whl")
|
|
20
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
21
|
+
assert "peaches/py.typed" in archive.namelist()
|
|
22
|
+
metadata, = [name for name in archive.namelist() if name.endswith("/METADATA")]
|
|
23
|
+
message = BytesParser().parsebytes(archive.read(metadata))
|
|
24
|
+
assert message["Name"] == "peaches" and message["Requires-Python"] == ">=3.11"
|
|
25
|
+
for artifact in artifacts:
|
|
26
|
+
with tempfile.TemporaryDirectory() as temporary:
|
|
27
|
+
outside = Path(temporary)
|
|
28
|
+
environment = outside / "env"
|
|
29
|
+
subprocess.run(["uv", "venv", str(environment), "--python", sys.executable], check=True)
|
|
30
|
+
python = environment / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python")
|
|
31
|
+
def run(*command):
|
|
32
|
+
subprocess.run(command, check=True, cwd=outside)
|
|
33
|
+
run("uv", "pip", "install", "--python", str(python), str(artifact))
|
|
34
|
+
run(str(python), "-I", "-c", "import importlib.util, peaches; assert all(importlib.util.find_spec(name) is None for name in ('func_seq','carets','headless_spreadsheets','expose_interface'))")
|
|
35
|
+
run(str(python), "-I", "-m", "unittest", "discover", "-s", str(ROOT / "tests"), "-v")
|
|
36
|
+
run("uv", "pip", "install", "--python", str(python), f"{artifact}[cli]")
|
|
37
|
+
run(str(python), "-I", "-m", "unittest", "discover", "-s", str(ROOT / "cli_tests"), "-v")
|
|
38
|
+
if sys.version_info >= (3, 14):
|
|
39
|
+
run("uv", "pip", "install", "--python", str(python), f"{artifact}[cli,sequence,table,terminal]")
|
|
40
|
+
run(str(python), "-I", "-m", "unittest", "discover", "-s", str(ROOT / "profile_tests"), "-v")
|
|
41
|
+
run("uv", "pip", "check", "--python", str(python))
|
|
42
|
+
print(json.dumps({"schema": 1, "artifact": artifact.name, "sha256": hashlib.sha256(artifact.read_bytes()).hexdigest()}))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Expose the curated workbench as ordinary application commands."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from expose_interface import Interface, UsageError, option
|
|
7
|
+
from moveset.core import canonical
|
|
8
|
+
|
|
9
|
+
from .workbench import TextWorkbench
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_interface():
|
|
13
|
+
app = Interface("peaches", description="Inspect and run the selected text workbench.")
|
|
14
|
+
|
|
15
|
+
@app.command(name="inspect")
|
|
16
|
+
def inspect_workflow():
|
|
17
|
+
return {"schema": 1, "status": "inspected", "workflow": TextWorkbench().inspect()}
|
|
18
|
+
|
|
19
|
+
@app.command(name="report")
|
|
20
|
+
async def report(text: str, *, checkpoint: str | None = None, resume: bool = False,
|
|
21
|
+
retry: bool = False, restart: bool = False, pace: str = "auto"):
|
|
22
|
+
"""Normalize/report text; optional checkpoints retain explicit recovery."""
|
|
23
|
+
return await TextWorkbench().report(text, checkpoint=checkpoint, resume=resume,
|
|
24
|
+
retry=retry, restart=restart, pace=pace)
|
|
25
|
+
|
|
26
|
+
@app.command(name="markdown")
|
|
27
|
+
@option("--text", dest="texts", multiple=True, required=True)
|
|
28
|
+
def markdown(*, texts: list[str]):
|
|
29
|
+
"""Render typed reports through the optional table profile."""
|
|
30
|
+
return {"schema": 1, "status": "rendered", "format": "markdown", "text": TextWorkbench().markdown(texts)}
|
|
31
|
+
|
|
32
|
+
@app.command(name="review")
|
|
33
|
+
async def review(text: str, *, checkpoint: str):
|
|
34
|
+
"""Select the optional interactive POSIX terminal profile."""
|
|
35
|
+
return await TextWorkbench().review(text, checkpoint=checkpoint)
|
|
36
|
+
|
|
37
|
+
return app
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main(argv=None):
|
|
41
|
+
try:
|
|
42
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
43
|
+
outcome = build_interface().invoke(argv)
|
|
44
|
+
if outcome is not None:
|
|
45
|
+
sys.stdout.buffer.write(canonical(outcome) + b"\n")
|
|
46
|
+
sys.stdout.buffer.flush()
|
|
47
|
+
except Exception as error:
|
|
48
|
+
record = {"schema": 1, "status": "error", "error": type(error).__name__, "message": str(error)}
|
|
49
|
+
sys.stderr.buffer.write(b"\n" + canonical(record) + b"\n")
|
|
50
|
+
sys.stderr.buffer.flush()
|
|
51
|
+
raise SystemExit(2 if isinstance(error, UsageError) else 1) from None
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Application choices over the qualified reference and scripting API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from scriptty import loads
|
|
7
|
+
from scriptty.application import execute
|
|
8
|
+
from wispsy.composition import text_workflow_document
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TextWorkbench:
|
|
12
|
+
"""Inspect/report text without requiring a terminal, storage or HTTP service."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *, catalog_version="wispsy-text-v1"):
|
|
15
|
+
self.catalog_version = catalog_version
|
|
16
|
+
self.workflow = loads(json.dumps(text_workflow_document()), source="peaches:text-report")
|
|
17
|
+
|
|
18
|
+
def inspect(self):
|
|
19
|
+
return self.workflow.inspect()
|
|
20
|
+
|
|
21
|
+
def catalog(self):
|
|
22
|
+
"""Explicitly assemble only the curated reference capabilities on demand."""
|
|
23
|
+
from wispsy.integration import build_catalog
|
|
24
|
+
|
|
25
|
+
return build_catalog()
|
|
26
|
+
|
|
27
|
+
async def report(self, text: str, *, checkpoint=None, resume=False, retry=False,
|
|
28
|
+
restart=False, pace="auto", control=None, observer=None):
|
|
29
|
+
"""Return the same schema-1 outcome used by scripting/terminal hosts."""
|
|
30
|
+
return await execute(self.workflow, self.catalog(), catalog_version=self.catalog_version,
|
|
31
|
+
inputs={"text": text}, checkpoint=checkpoint, resume=resume, retry=retry,
|
|
32
|
+
restart=restart, pace=pace, control=control, observer=observer)
|
|
33
|
+
|
|
34
|
+
def table(self, texts: list[str]):
|
|
35
|
+
if sys.version_info < (3, 14):
|
|
36
|
+
raise ImportError("Install peaches[table] on Python 3.14+")
|
|
37
|
+
try:
|
|
38
|
+
from wispsy.tabular import normalize_table
|
|
39
|
+
except ImportError:
|
|
40
|
+
raise ImportError("Install peaches[table] on Python 3.14+") from None
|
|
41
|
+
return normalize_table(texts)
|
|
42
|
+
|
|
43
|
+
def markdown(self, texts: list[str]) -> str:
|
|
44
|
+
return self.table(texts).to_document().to_markdown()
|
|
45
|
+
|
|
46
|
+
def terminal_model(self, text: str, *, checkpoint):
|
|
47
|
+
try:
|
|
48
|
+
from scriptty.terminal import WorkflowTerminal
|
|
49
|
+
except ImportError:
|
|
50
|
+
raise ImportError("Install peaches[terminal] on Python 3.14+") from None
|
|
51
|
+
return WorkflowTerminal(self.workflow, self.catalog(), catalog_version=self.catalog_version,
|
|
52
|
+
checkpoint=checkpoint, inputs={"text": text})
|
|
53
|
+
|
|
54
|
+
async def review(self, text: str, *, checkpoint, **terminal_options):
|
|
55
|
+
try:
|
|
56
|
+
from scriptty.terminal import run_terminal
|
|
57
|
+
except ImportError:
|
|
58
|
+
raise ImportError("Install peaches[terminal] on Python 3.14+") from None
|
|
59
|
+
return await run_terminal(self.workflow, self.catalog(), catalog_version=self.catalog_version,
|
|
60
|
+
checkpoint=checkpoint, inputs={"text": text}, **terminal_options)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from peaches import TextWorkbench
|
|
4
|
+
from wispsy.reference import run_reference
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class WorkbenchTests(unittest.IsolatedAsyncioTestCase):
|
|
8
|
+
async def test_report_reuses_the_same_typed_reference_without_lifecycle(self):
|
|
9
|
+
workbench = TextWorkbench()
|
|
10
|
+
for text in (" Hello WORLD ", " Straße 🌍 ", ""):
|
|
11
|
+
outcome = await workbench.report(text)
|
|
12
|
+
self.assertEqual((outcome["schema"], outcome["status"], outcome["outputs"]),
|
|
13
|
+
(1, "completed", {"report": run_reference(text)}))
|
|
14
|
+
self.assertIsNone(outcome["run_id"])
|
|
15
|
+
with self.assertRaises(ValueError):
|
|
16
|
+
await workbench.report(2)
|
|
17
|
+
|
|
18
|
+
async def test_inspection_is_passive_and_returns_detached_metadata(self):
|
|
19
|
+
workbench = TextWorkbench()
|
|
20
|
+
workbench.inspect()["steps"].clear()
|
|
21
|
+
self.assertEqual(len(workbench.inspect()["steps"]), 2)
|
|
22
|
+
self.assertEqual([step["capability"] for step in workbench.inspect()["steps"]], ["text.normalize", "text.report"])
|