wup 0.1.9__tar.gz → 0.1.10__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.1.9
3
+ Version: 0.1.10
4
4
  Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -28,17 +28,17 @@ Dynamic: license-file
28
28
 
29
29
  ## AI Cost Tracking
30
30
 
31
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
32
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.45-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
31
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.10-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
32
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.60-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
33
33
 
34
- - 🤖 **LLM usage:** $0.4500 (3 commits)
34
+ - 🤖 **LLM usage:** $0.6000 (4 commits)
35
35
  - 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
36
36
 
37
37
  Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
38
38
 
39
39
  ---
40
40
 
41
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.1.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
41
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.1.10-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
42
 
43
43
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
44
44
 
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.45-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.10-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.60-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $0.4500 (3 commits)
9
+ - 🤖 **LLM usage:** $0.6000 (4 commits)
10
10
  - 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.1.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.1.10-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
19
19
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wup"
7
- version = "0.1.9"
7
+ version = "0.1.10"
8
8
  description = "WUP (What's Up) - Intelligent file watcher for regression testing in large projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -7,7 +7,7 @@ WUP monitors file changes and runs intelligent regression tests using a 3-layer
7
7
  3. Detail Layer: Full tests with blame reports (only on failure)
8
8
  """
9
9
 
10
- __version__ = "0.1.9"
10
+ __version__ = "0.1.10"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .core import WupWatcher
@@ -0,0 +1,247 @@
1
+ """TestQL integration for WUP watcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import re
8
+ import subprocess
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Dict, List, Optional, Sequence
12
+ from urllib import error, request
13
+
14
+ from .core import WupWatcher
15
+
16
+
17
+ class BrowserNotifier:
18
+ """Send watcher events to browser-facing service and local file."""
19
+
20
+ def __init__(self, service_url: Optional[str], events_file: Path):
21
+ self.service_url = service_url
22
+ self.events_file = events_file
23
+ self.events_file.parent.mkdir(parents=True, exist_ok=True)
24
+
25
+ def notify(self, payload: Dict) -> None:
26
+ payload_with_ts = {"timestamp": int(time.time()), **payload}
27
+ self.events_file.write_text(json.dumps(payload_with_ts, indent=2), encoding="utf-8")
28
+
29
+ if not self.service_url:
30
+ return
31
+
32
+ body = json.dumps(payload_with_ts).encode("utf-8")
33
+ req = request.Request(
34
+ self.service_url,
35
+ data=body,
36
+ headers={"Content-Type": "application/json"},
37
+ method="POST",
38
+ )
39
+ try:
40
+ with request.urlopen(req, timeout=5):
41
+ return
42
+ except (error.URLError, TimeoutError):
43
+ return
44
+
45
+
46
+ class TestQLWatcher(WupWatcher):
47
+ """WUP watcher running selective TestQL scenarios for changed services."""
48
+
49
+ def __init__(
50
+ self,
51
+ project_root: str,
52
+ scenarios_dir: str = "testql-scenarios",
53
+ testql_bin: str = "testql",
54
+ track_dir: str = ".wup/tracks",
55
+ browser_service_url: Optional[str] = None,
56
+ quick_limit: int = 3,
57
+ **kwargs,
58
+ ):
59
+ super().__init__(project_root=project_root, **kwargs)
60
+ self.scenarios_dir = self.project_root / scenarios_dir
61
+ self.testql_bin = testql_bin
62
+ self.quick_limit = quick_limit
63
+ self.track_dir = self.project_root / track_dir
64
+ self.track_dir.mkdir(parents=True, exist_ok=True)
65
+ self.browser_notifier = BrowserNotifier(
66
+ service_url=browser_service_url,
67
+ events_file=self.project_root / ".wup" / "browser-events" / "latest.json",
68
+ )
69
+ self.last_track_path: Optional[Path] = None
70
+
71
+ def _tokenize_service(self, service: str) -> List[str]:
72
+ raw_tokens = re.split(r"[^a-zA-Z0-9]+", service.lower())
73
+ return [token for token in raw_tokens if len(token) >= 3]
74
+
75
+ def _discover_scenarios(self) -> List[Path]:
76
+ if not self.scenarios_dir.exists():
77
+ return []
78
+ return sorted(self.scenarios_dir.rglob("*.testql.toon.yaml"))
79
+
80
+ def _select_scenarios_for_service(self, service: str) -> List[Path]:
81
+ all_scenarios = self._discover_scenarios()
82
+ if not all_scenarios:
83
+ return []
84
+
85
+ tokens = self._tokenize_service(service)
86
+ scored: List[tuple[int, Path]] = []
87
+
88
+ for scenario in all_scenarios:
89
+ name = scenario.name.lower()
90
+ score = 0
91
+ if any(token in name for token in tokens):
92
+ score += 3
93
+ if "api" in name or "endpoint" in name:
94
+ score += 2
95
+ if "infra" in name or "smoke" in name:
96
+ score += 1
97
+ scored.append((score, scenario))
98
+
99
+ scored.sort(key=lambda item: (item[0], item[1].name), reverse=True)
100
+ selected = [scenario for score, scenario in scored if score > 0]
101
+
102
+ if selected:
103
+ return selected
104
+
105
+ return all_scenarios[: self.quick_limit]
106
+
107
+ def _run_testql(self, args: Sequence[str], timeout: int) -> subprocess.CompletedProcess:
108
+ cmd = [self.testql_bin, *args]
109
+ try:
110
+ return subprocess.run(
111
+ cmd,
112
+ cwd=str(self.project_root),
113
+ capture_output=True,
114
+ text=True,
115
+ timeout=timeout,
116
+ )
117
+ except FileNotFoundError:
118
+ fallback_cmd = ["python3", "-m", "testql.cli", *args]
119
+ return subprocess.run(
120
+ fallback_cmd,
121
+ cwd=str(self.project_root),
122
+ capture_output=True,
123
+ text=True,
124
+ timeout=timeout,
125
+ )
126
+
127
+ def _write_track(self, *, service: str, stage: str, scenario: Optional[Path], result: subprocess.CompletedProcess) -> Path:
128
+ ts = int(time.time())
129
+ safe_service = service.replace("/", "_").replace("\\", "_")
130
+ scenario_name = scenario.name if scenario else "unknown"
131
+ stderr_line = (result.stderr or "").strip().splitlines()[:1]
132
+ stdout_line = (result.stdout or "").strip().splitlines()[:1]
133
+
134
+ payload = {
135
+ "service": service,
136
+ "stage": stage,
137
+ "scenario": str(scenario) if scenario else None,
138
+ "command": result.args,
139
+ "returncode": result.returncode,
140
+ "stderr_head": stderr_line[0] if stderr_line else "",
141
+ "stdout_head": stdout_line[0] if stdout_line else "",
142
+ "track": {
143
+ "file": str(scenario) if scenario else "",
144
+ "line": 1,
145
+ "hint": scenario_name,
146
+ },
147
+ }
148
+
149
+ track_path = self.track_dir / f"{ts}_{safe_service}_{stage}.json"
150
+ track_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
151
+ self.last_track_path = track_path
152
+
153
+ self.browser_notifier.notify(
154
+ {
155
+ "type": "wup_testql_error",
156
+ "service": service,
157
+ "stage": stage,
158
+ "track_file": str(track_path),
159
+ "scenario": str(scenario) if scenario else None,
160
+ "message": payload["stderr_head"] or payload["stdout_head"] or "TestQL command failed",
161
+ }
162
+ )
163
+ return track_path
164
+
165
+ async def run_quick_test(self, service: str, endpoints: List[str]) -> bool:
166
+ scenarios = self._select_scenarios_for_service(service)[: self.quick_limit]
167
+ if not scenarios:
168
+ self.console.print(f"[yellow]⚠ No TestQL scenarios found for {service}[/yellow]")
169
+ return True
170
+
171
+ self.console.print(
172
+ f"[cyan]🧪 Quick TestQL for {service} ({len(scenarios)} scenarios / {len(endpoints)} endpoints)[/cyan]"
173
+ )
174
+
175
+ for scenario in scenarios:
176
+ result = self._run_testql(["run", str(scenario), "--dry-run"], timeout=60)
177
+ if result.returncode != 0:
178
+ track_path = self._write_track(
179
+ service=service,
180
+ stage="quick",
181
+ scenario=scenario,
182
+ result=result,
183
+ )
184
+ self.console.print(
185
+ f"[red]✗ Quick failed: {scenario.name} | track: {track_path}[/red]"
186
+ )
187
+ return False
188
+
189
+ self.console.print(f"[green]✓ Quick TestQL passed for {service}[/green]")
190
+ return True
191
+
192
+ async def run_detail_test(self, service: str, endpoints: List[str]) -> Dict:
193
+ scenarios = self._select_scenarios_for_service(service)
194
+ results = {
195
+ "service": service,
196
+ "total_scenarios": len(scenarios),
197
+ "passed": 0,
198
+ "failed": 0,
199
+ "failed_scenarios": [],
200
+ "track_files": [],
201
+ }
202
+
203
+ self.console.print(
204
+ f"[cyan]🔍 Detail TestQL for {service} ({len(scenarios)} scenarios / {len(endpoints)} endpoints)[/cyan]"
205
+ )
206
+
207
+ for scenario in scenarios:
208
+ result = self._run_testql(["run", str(scenario), "--output", "json"], timeout=180)
209
+ if result.returncode == 0:
210
+ results["passed"] += 1
211
+ continue
212
+
213
+ results["failed"] += 1
214
+ results["failed_scenarios"].append(str(scenario))
215
+ track_path = self._write_track(
216
+ service=service,
217
+ stage="detail",
218
+ scenario=scenario,
219
+ result=result,
220
+ )
221
+ results["track_files"].append(str(track_path))
222
+ self.console.print(
223
+ f"[red]✗ Detail failed: {scenario.name} | track: {track_path}[/red]"
224
+ )
225
+
226
+ if results["failed"] == 0:
227
+ self.console.print(
228
+ f"[green]✓ Detail TestQL passed for {service} ({results['passed']} scenarios)[/green]"
229
+ )
230
+
231
+ return results
232
+
233
+ async def process_changed_file_once(self, file_path: str) -> Dict:
234
+ self.on_file_change(file_path)
235
+
236
+ processed = 0
237
+ while self.test_queue and processed < 4:
238
+ await self.process_test_queue_once()
239
+ processed += 1
240
+ await asyncio.sleep(0)
241
+
242
+ return {
243
+ "file": file_path,
244
+ "processed_items": processed,
245
+ "remaining_queue": len(self.test_queue),
246
+ "last_track_path": str(self.last_track_path) if self.last_track_path else None,
247
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.1.9
3
+ Version: 0.1.10
4
4
  Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -28,17 +28,17 @@ Dynamic: license-file
28
28
 
29
29
  ## AI Cost Tracking
30
30
 
31
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
32
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.45-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
31
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.10-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
32
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.60-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
33
33
 
34
- - 🤖 **LLM usage:** $0.4500 (3 commits)
34
+ - 🤖 **LLM usage:** $0.6000 (4 commits)
35
35
  - 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
36
36
 
37
37
  Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
38
38
 
39
39
  ---
40
40
 
41
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.1.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
41
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.1.10-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
42
 
43
43
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
44
44
 
@@ -6,6 +6,7 @@ wup/__init__.py
6
6
  wup/cli.py
7
7
  wup/core.py
8
8
  wup/dependency_mapper.py
9
+ wup/testql_watcher.py
9
10
  wup.egg-info/PKG-INFO
10
11
  wup.egg-info/SOURCES.txt
11
12
  wup.egg-info/dependency_links.txt
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes