noscroll 0.1.1__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.
- noscroll-0.1.1/.env.example +70 -0
- noscroll-0.1.1/.gitignore +22 -0
- noscroll-0.1.1/LICENSE +21 -0
- noscroll-0.1.1/PKG-INFO +64 -0
- noscroll-0.1.1/README.md +45 -0
- noscroll-0.1.1/automation/.env.example +4 -0
- noscroll-0.1.1/automation/README.md +134 -0
- noscroll-0.1.1/automation/__init__.py +4 -0
- noscroll-0.1.1/automation/__main__.py +6 -0
- noscroll-0.1.1/automation/adapters/__init__.py +98 -0
- noscroll-0.1.1/automation/agents.py +253 -0
- noscroll-0.1.1/automation/config.py +39 -0
- noscroll-0.1.1/automation/evals/__init__.py +110 -0
- noscroll-0.1.1/automation/latest +1 -0
- noscroll-0.1.1/automation/loop.py +419 -0
- noscroll-0.1.1/automation/prompts/diagnostic.txt +24 -0
- noscroll-0.1.1/automation/prompts/executor.txt +32 -0
- noscroll-0.1.1/automation/prompts/fixer.txt +29 -0
- noscroll-0.1.1/automation/tasks/__init__.py +95 -0
- noscroll-0.1.1/prompts/system.txt +90 -0
- noscroll-0.1.1/pyproject.toml +37 -0
- noscroll-0.1.1/src/noscroll/__init__.py +3 -0
- noscroll-0.1.1/src/noscroll/__main__.py +5 -0
- noscroll-0.1.1/src/noscroll/cli.py +1120 -0
- noscroll-0.1.1/src/noscroll/config.py +189 -0
- noscroll-0.1.1/src/noscroll/crawler.py +676 -0
- noscroll-0.1.1/src/noscroll/duration.py +312 -0
- noscroll-0.1.1/src/noscroll/fetch.py +36 -0
- noscroll-0.1.1/src/noscroll/hackernews.py +737 -0
- noscroll-0.1.1/src/noscroll/llm.py +741 -0
- noscroll-0.1.1/src/noscroll/opml.py +76 -0
- noscroll-0.1.1/src/noscroll/rss.py +161 -0
- noscroll-0.1.1/src/noscroll/runner.py +359 -0
- noscroll-0.1.1/src/noscroll/utils.py +88 -0
- noscroll-0.1.1/subscriptions/subscriptions.toml +184 -0
- noscroll-0.1.1/tests/integration/__init__.py +1 -0
- noscroll-0.1.1/tests/integration/conftest.py +47 -0
- noscroll-0.1.1/tests/integration/test_cli_config.py +46 -0
- noscroll-0.1.1/tests/integration/test_cli_run.py +215 -0
- noscroll-0.1.1/tests/integration/test_real_run.py +164 -0
- noscroll-0.1.1/tests/test_cli.py +600 -0
- noscroll-0.1.1/tests/test_config.py +273 -0
- noscroll-0.1.1/tests/test_duration.py +323 -0
- noscroll-0.1.1/tests/test_hackernews.py +531 -0
- noscroll-0.1.1/tests/test_llm.py +412 -0
- noscroll-0.1.1/tests/test_opml.py +293 -0
- noscroll-0.1.1/tests/test_rss.py +223 -0
- noscroll-0.1.1/tests/test_runner.py +497 -0
- noscroll-0.1.1/tests/test_utils.py +182 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# NoScroll Configuration
|
|
2
|
+
# All environment variables can also be set via CLI arguments.
|
|
3
|
+
# Priority: CLI > Environment Variable > Config File > Default
|
|
4
|
+
|
|
5
|
+
# =============================================================================
|
|
6
|
+
# LLM Configuration (also available as CLI: --llm-api-url, --llm-api-key, etc.)
|
|
7
|
+
# =============================================================================
|
|
8
|
+
|
|
9
|
+
# LLM API Settings
|
|
10
|
+
LLM_API_URL="https://api.openai.com/v1" # CLI: --llm-api-url
|
|
11
|
+
LLM_API_KEY="" # CLI: --llm-api-key
|
|
12
|
+
LLM_MODEL="gpt-4o" # CLI: --llm-model
|
|
13
|
+
LLM_SUMMARY_MODEL="gpt-4o-mini" # CLI: --llm-summary-model
|
|
14
|
+
LLM_API_MODE="chat" # CLI: --llm-api-mode (chat/completions/responses)
|
|
15
|
+
|
|
16
|
+
# LLM Timeout & Concurrency
|
|
17
|
+
LLM_TIMEOUT_MS=600000 # CLI: --llm-timeout
|
|
18
|
+
LLM_GLOBAL_CONCURRENCY=5 # CLI: --llm-concurrency
|
|
19
|
+
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# Paths (also available as CLI arguments)
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
SUBSCRIPTIONS_PATH="subscriptions/subscriptions.toml" # CLI: --subscriptions
|
|
25
|
+
SYSTEM_PROMPT_PATH="prompts/system.txt" # CLI: --system-prompt
|
|
26
|
+
LLM_LOG_PATH="logs/llm-trace.log" # CLI: --llm-log
|
|
27
|
+
FEED_LOG_PATH="logs/feed-items.log" # CLI: --feed-log
|
|
28
|
+
OUTPUT_DIR="outputs"
|
|
29
|
+
|
|
30
|
+
# Config file path (optional - defaults to ~/.config/noscroll/config.toml)
|
|
31
|
+
# NOSCROLL_CONFIG="/path/to/config.toml"
|
|
32
|
+
|
|
33
|
+
# =============================================================================
|
|
34
|
+
# Proxy (uses standard environment variables)
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# NoScroll automatically uses standard proxy environment variables:
|
|
37
|
+
# HTTPS_PROXY, HTTP_PROXY, ALL_PROXY
|
|
38
|
+
# Example:
|
|
39
|
+
# export HTTPS_PROXY="http://127.0.0.1:7890"
|
|
40
|
+
# export ALL_PROXY="socks5://127.0.0.1:1080"
|
|
41
|
+
|
|
42
|
+
# =============================================================================
|
|
43
|
+
# CLI-Specific Options (NOSCROLL_* prefix)
|
|
44
|
+
# These map directly to `noscroll run` arguments
|
|
45
|
+
# =============================================================================
|
|
46
|
+
|
|
47
|
+
# Time Window
|
|
48
|
+
# NOSCROLL_LAST="10d" # CLI: --last (e.g., 10d, 36h, 2w)
|
|
49
|
+
# NOSCROLL_FROM="" # CLI: --from (RFC3339 or YYYY-MM-DD)
|
|
50
|
+
# NOSCROLL_TO="" # CLI: --to (default: now)
|
|
51
|
+
|
|
52
|
+
# Output Splitting
|
|
53
|
+
# NOSCROLL_BUCKET="" # CLI: --bucket (day, hour, or duration)
|
|
54
|
+
# NOSCROLL_NAME_TEMPLATE="{start:%Y-%m-%d}.md" # CLI: --name-template
|
|
55
|
+
|
|
56
|
+
# Output
|
|
57
|
+
# NOSCROLL_OUT="./noscroll.md" # CLI: --out
|
|
58
|
+
# NOSCROLL_FORMAT="markdown" # CLI: --format (markdown/json)
|
|
59
|
+
|
|
60
|
+
# Source Filtering
|
|
61
|
+
# NOSCROLL_SOURCE_TYPES="rss,web,hn" # CLI: --source-types
|
|
62
|
+
|
|
63
|
+
# LLM Request Options
|
|
64
|
+
# NOSCROLL_SERIAL="false" # CLI: --serial
|
|
65
|
+
# NOSCROLL_DELAY=0 # CLI: --delay (ms)
|
|
66
|
+
# NOSCROLL_LANG="en" # CLI: --lang (en, zh, ja, etc.)
|
|
67
|
+
|
|
68
|
+
# Debug
|
|
69
|
+
# NOSCROLL_DEBUG="false" # CLI: --debug
|
|
70
|
+
# DEBUG="false" # Alternative debug flag
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.env
|
|
2
|
+
node_modules/
|
|
3
|
+
output.json
|
|
4
|
+
|
|
5
|
+
# VS Code
|
|
6
|
+
.vscode/settings.json
|
|
7
|
+
|
|
8
|
+
# Python
|
|
9
|
+
.venv/
|
|
10
|
+
__pycache__/
|
|
11
|
+
*.pyc
|
|
12
|
+
*.pyo
|
|
13
|
+
*.egg-info/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
htmlcov/
|
|
16
|
+
.coverage
|
|
17
|
+
|
|
18
|
+
# Logs and crawled content
|
|
19
|
+
logs/
|
|
20
|
+
crawled/
|
|
21
|
+
test_outputs/
|
|
22
|
+
outputs/
|
noscroll-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuxin
|
|
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.
|
noscroll-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: noscroll
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Pull, don't scroll. RSS aggregator with LLM-powered summarization.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: feedparser>=6.0.0
|
|
8
|
+
Requires-Dist: httpx[socks]>=0.27.0
|
|
9
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
10
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
12
|
+
Provides-Extra: crawler
|
|
13
|
+
Requires-Dist: crawl4ai>=0.3.0; extra == 'crawler'
|
|
14
|
+
Requires-Dist: pydantic>=2.0.0; extra == 'crawler'
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# NoScroll - Pull, don't scroll
|
|
21
|
+
[](https://www.python.org/)
|
|
22
|
+
[](LICENSE)
|
|
23
|
+
[](https://github.com/zhuanyongxigua/noscroll)
|
|
24
|
+
[](https://github.com/zhuanyongxigua/noscroll)
|
|
25
|
+
|
|
26
|
+
## What is NoScroll
|
|
27
|
+
NoScroll is a Python CLI that pulls information from RSS feeds, web pages, and Hacker News, then uses an LLM to summarize and rank the most useful items.
|
|
28
|
+
|
|
29
|
+
It is designed for a pull-based reading workflow: define sources once, run on schedule, read only the high-signal digest.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pipx install noscroll
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Ask Command
|
|
38
|
+
|
|
39
|
+
Use natural language directly:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
noscroll --env-file .env ask "Collect content from the past five days, one file per day"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This will generate daily digest files in `outputs/`.
|
|
46
|
+
|
|
47
|
+
Example generated text:
|
|
48
|
+
|
|
49
|
+
```markdown
|
|
50
|
+
## AI (3)
|
|
51
|
+
1) Off Grid: Running text/image/vision models offline on mobile | Value: 4/5 | Type: Practice
|
|
52
|
+
- Conclusion: This open-source project demonstrates on-device multimodal inference on smartphones, with strong privacy and offline usability.
|
|
53
|
+
- Why it matters: On-device AI can reduce privacy risk and cloud inference cost, and is a good fit for offline-first products.
|
|
54
|
+
- Evidence links: https://github.com/alichherawalla/off-grid-mobile
|
|
55
|
+
|
|
56
|
+
## Other News (2)
|
|
57
|
+
4) uBlock rule: hide YouTube Shorts with one click | Value: 4/5 | Domain: Tech
|
|
58
|
+
|
|
59
|
+
## Life & Health (2)
|
|
60
|
+
6) AI avatars for rural healthcare support | Value: 3/5 | Domain: Health
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
MIT. See [LICENSE](LICENSE).
|
noscroll-0.1.1/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# NoScroll - Pull, don't scroll
|
|
2
|
+
[](https://www.python.org/)
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://github.com/zhuanyongxigua/noscroll)
|
|
5
|
+
[](https://github.com/zhuanyongxigua/noscroll)
|
|
6
|
+
|
|
7
|
+
## What is NoScroll
|
|
8
|
+
NoScroll is a Python CLI that pulls information from RSS feeds, web pages, and Hacker News, then uses an LLM to summarize and rank the most useful items.
|
|
9
|
+
|
|
10
|
+
It is designed for a pull-based reading workflow: define sources once, run on schedule, read only the high-signal digest.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pipx install noscroll
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Ask Command
|
|
19
|
+
|
|
20
|
+
Use natural language directly:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
noscroll --env-file .env ask "Collect content from the past five days, one file per day"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This will generate daily digest files in `outputs/`.
|
|
27
|
+
|
|
28
|
+
Example generated text:
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
## AI (3)
|
|
32
|
+
1) Off Grid: Running text/image/vision models offline on mobile | Value: 4/5 | Type: Practice
|
|
33
|
+
- Conclusion: This open-source project demonstrates on-device multimodal inference on smartphones, with strong privacy and offline usability.
|
|
34
|
+
- Why it matters: On-device AI can reduce privacy risk and cloud inference cost, and is a good fit for offline-first products.
|
|
35
|
+
- Evidence links: https://github.com/alichherawalla/off-grid-mobile
|
|
36
|
+
|
|
37
|
+
## Other News (2)
|
|
38
|
+
4) uBlock rule: hide YouTube Shorts with one click | Value: 4/5 | Domain: Tech
|
|
39
|
+
|
|
40
|
+
## Life & Health (2)
|
|
41
|
+
6) AI avatars for rural healthcare support | Value: 3/5 | Domain: Health
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# NoScroll Automation Harness
|
|
2
|
+
|
|
3
|
+
Automated **run → test → eval → fix** loop using Claude Agent SDK.
|
|
4
|
+
|
|
5
|
+
This is an **agent harness** layer, separate from business logic in `src/`.
|
|
6
|
+
|
|
7
|
+
## Directory Structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
automation/
|
|
11
|
+
├── __init__.py # Package entry
|
|
12
|
+
├── __main__.py # python -m automation
|
|
13
|
+
├── loop.py # Main loop: run → test → eval → fix → repeat
|
|
14
|
+
├── config.py # Configuration
|
|
15
|
+
├── agents.py # Claude Agent SDK agents (executor, diagnostic, fixer)
|
|
16
|
+
├── tasks/ # Task/scenario definitions
|
|
17
|
+
│ └── __init__.py # Predefined tasks
|
|
18
|
+
├── evals/ # Evaluation logic
|
|
19
|
+
│ └── __init__.py # Output validation
|
|
20
|
+
├── prompts/ # System prompts (file-based for easy editing)
|
|
21
|
+
│ ├── executor.txt # Executor agent prompt
|
|
22
|
+
│ ├── diagnostic.txt # Diagnostic agent prompt
|
|
23
|
+
│ └── fixer.txt # Fixer agent prompt
|
|
24
|
+
├── adapters/ # External tool adapters
|
|
25
|
+
│ └── __init__.py # pytest, ruff, git adapters
|
|
26
|
+
└── artifacts/ # Loop outputs: logs, diffs, reports
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## The Loop
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
33
|
+
│ Start Task │
|
|
34
|
+
└─────────────────────────────────────────────────────────────┘
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
38
|
+
│ 1. Execute: Translate instruction → Run noscroll command │
|
|
39
|
+
└─────────────────────────────────────────────────────────────┘
|
|
40
|
+
│
|
|
41
|
+
▼
|
|
42
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
43
|
+
│ 2. Evaluate: Check output files, validate content │
|
|
44
|
+
└─────────────────────────────────────────────────────────────┘
|
|
45
|
+
│
|
|
46
|
+
▼
|
|
47
|
+
┌───────────────┐
|
|
48
|
+
│ Passed? │
|
|
49
|
+
└───────────────┘
|
|
50
|
+
│ │
|
|
51
|
+
Yes │ │ No
|
|
52
|
+
▼ ▼
|
|
53
|
+
┌─────────────┐ ┌─────────────────────────────────┐
|
|
54
|
+
│ SUCCESS │ │ 3. Diagnose: Analyze failure │
|
|
55
|
+
│ (break) │ │ (describe phenomena only) │
|
|
56
|
+
└─────────────┘ └─────────────────────────────────┘
|
|
57
|
+
│
|
|
58
|
+
▼
|
|
59
|
+
┌─────────────────────────────────┐
|
|
60
|
+
│ 4. Fix: Apply code changes │
|
|
61
|
+
└─────────────────────────────────┘
|
|
62
|
+
│
|
|
63
|
+
▼
|
|
64
|
+
┌───────────────┐
|
|
65
|
+
│ Max loops? │
|
|
66
|
+
└───────────────┘
|
|
67
|
+
│ │
|
|
68
|
+
Yes │ │ No
|
|
69
|
+
▼ ▼
|
|
70
|
+
┌─────────┐ (back to 1)
|
|
71
|
+
│ FAIL │
|
|
72
|
+
└─────────┘
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Stop Conditions
|
|
76
|
+
|
|
77
|
+
1. **Success**: Evaluation passes ✓
|
|
78
|
+
2. **Max loops**: Reached iteration limit (default: 3)
|
|
79
|
+
3. **Fix failed**: Fixer couldn't apply changes
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Claude Agent SDK requires Claude Code CLI
|
|
85
|
+
pip install claude-agent-sdk
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Usage
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# List available tasks
|
|
92
|
+
python -m automation --list
|
|
93
|
+
|
|
94
|
+
# Run a specific task
|
|
95
|
+
python -m automation --task basic_run_5d
|
|
96
|
+
|
|
97
|
+
# Run a suite
|
|
98
|
+
python -m automation --suite basic
|
|
99
|
+
|
|
100
|
+
# Custom task
|
|
101
|
+
python -m automation --custom "运行 noscroll,获取过去 3 天的 HN 内容"
|
|
102
|
+
|
|
103
|
+
# Options
|
|
104
|
+
python -m automation --task basic_run_5d --max-loops 5
|
|
105
|
+
python -m automation --task basic_run_5d --quiet
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Artifacts
|
|
109
|
+
|
|
110
|
+
Each run produces artifacts in `automation/artifacts/<task_name>/`:
|
|
111
|
+
|
|
112
|
+
- `loop_result.json` - Full iteration history
|
|
113
|
+
- Output files from the noscroll command
|
|
114
|
+
|
|
115
|
+
## Design Principles
|
|
116
|
+
|
|
117
|
+
1. **Separation of concerns**: This is a harness layer, not business logic
|
|
118
|
+
2. **File-based prompts**: Easy to edit and version control
|
|
119
|
+
3. **Adapters for tools**: Clean interface to pytest, git, etc.
|
|
120
|
+
4. **Artifacts tracking**: Every run produces traceable outputs
|
|
121
|
+
|
|
122
|
+
## Why Not in `src/`?
|
|
123
|
+
|
|
124
|
+
The `src/` layout convention keeps only publishable package code in `src/`.
|
|
125
|
+
Automation tools, scripts, and harnesses belong at the project root level:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
project/
|
|
129
|
+
├── src/noscroll/ # Business logic (pip installable)
|
|
130
|
+
├── tests/ # Unit/integration tests
|
|
131
|
+
└── automation/ # Agent harness (development tool)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This keeps import paths clean and separates runtime code from development tooling.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Adapters for external tools (pytest, git, etc.)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CommandResult:
|
|
13
|
+
"""Result from running an external command."""
|
|
14
|
+
command: str
|
|
15
|
+
return_code: int
|
|
16
|
+
stdout: str
|
|
17
|
+
stderr: str
|
|
18
|
+
success: bool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run_command(
|
|
22
|
+
command: list[str],
|
|
23
|
+
cwd: Optional[Path] = None,
|
|
24
|
+
timeout: int = 300,
|
|
25
|
+
) -> CommandResult:
|
|
26
|
+
"""Run a shell command and capture output."""
|
|
27
|
+
cmd_str = " ".join(command)
|
|
28
|
+
try:
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
command,
|
|
31
|
+
cwd=str(cwd) if cwd else None,
|
|
32
|
+
capture_output=True,
|
|
33
|
+
text=True,
|
|
34
|
+
timeout=timeout,
|
|
35
|
+
)
|
|
36
|
+
return CommandResult(
|
|
37
|
+
command=cmd_str,
|
|
38
|
+
return_code=result.returncode,
|
|
39
|
+
stdout=result.stdout,
|
|
40
|
+
stderr=result.stderr,
|
|
41
|
+
success=result.returncode == 0,
|
|
42
|
+
)
|
|
43
|
+
except subprocess.TimeoutExpired:
|
|
44
|
+
return CommandResult(
|
|
45
|
+
command=cmd_str,
|
|
46
|
+
return_code=-1,
|
|
47
|
+
stdout="",
|
|
48
|
+
stderr=f"Command timed out after {timeout}s",
|
|
49
|
+
success=False,
|
|
50
|
+
)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
return CommandResult(
|
|
53
|
+
command=cmd_str,
|
|
54
|
+
return_code=-1,
|
|
55
|
+
stdout="",
|
|
56
|
+
stderr=str(e),
|
|
57
|
+
success=False,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def run_pytest(
|
|
62
|
+
test_path: Optional[Path] = None,
|
|
63
|
+
cwd: Optional[Path] = None,
|
|
64
|
+
args: Optional[list[str]] = None,
|
|
65
|
+
) -> CommandResult:
|
|
66
|
+
"""Run pytest with optional arguments."""
|
|
67
|
+
command = ["python", "-m", "pytest"]
|
|
68
|
+
if test_path:
|
|
69
|
+
command.append(str(test_path))
|
|
70
|
+
if args:
|
|
71
|
+
command.extend(args)
|
|
72
|
+
return run_command(command, cwd=cwd)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_ruff(
|
|
76
|
+
path: Optional[Path] = None,
|
|
77
|
+
cwd: Optional[Path] = None,
|
|
78
|
+
fix: bool = False,
|
|
79
|
+
) -> CommandResult:
|
|
80
|
+
"""Run ruff linter."""
|
|
81
|
+
command = ["python", "-m", "ruff", "check"]
|
|
82
|
+
if fix:
|
|
83
|
+
command.append("--fix")
|
|
84
|
+
if path:
|
|
85
|
+
command.append(str(path))
|
|
86
|
+
else:
|
|
87
|
+
command.append(".")
|
|
88
|
+
return run_command(command, cwd=cwd)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def git_diff(cwd: Optional[Path] = None) -> CommandResult:
|
|
92
|
+
"""Get git diff of current changes."""
|
|
93
|
+
return run_command(["git", "diff"], cwd=cwd)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def git_status(cwd: Optional[Path] = None) -> CommandResult:
|
|
97
|
+
"""Get git status."""
|
|
98
|
+
return run_command(["git", "status", "--porcelain"], cwd=cwd)
|