wup 0.2.2__tar.gz → 0.2.6__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.2.2
3
+ Version: 0.2.6
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.2.2-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.90-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.2.6-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-$1.05-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.9000 (6 commits)
34
+ - 🤖 **LLM usage:** $1.0500 (7 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.2.2-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.2.6-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
 
@@ -121,6 +121,9 @@ wup watch ./my-project --config custom-config.yaml
121
121
 
122
122
  # TestQL mode
123
123
  wup watch ./my-project --mode testql
124
+
125
+ # Discover endpoints from TestQL scenarios
126
+ wup testql-endpoints /path/to/scenarios --output testql-deps.json
124
127
  ```
125
128
 
126
129
  ### Initialize Configuration
@@ -149,24 +152,24 @@ wup status --deps my-deps.json
149
152
 
150
153
  ```
151
154
  ┌─────────────────────────────────────────────────────────────┐
152
- │ DETECTION LAYER
155
+ │ DETECTION LAYER
153
156
  │ File watching with watchdog + heuristics │
154
157
  │ Skips: .git, __pycache__, node_modules, .venv │
155
158
  └──────────────────────┬──────────────────────────────────────┘
156
159
  │ File change
157
160
 
158
161
  ┌─────────────────────────────────────────────────────────────┐
159
- │ PRIORITY LAYER
162
+ │ PRIORITY LAYER
160
163
  │ Quick test: 3 endpoints max per service │
161
- │ Duration: ~1-2 seconds
162
- │ Result: Pass → Done, Fail → Escalate
164
+ │ Duration: ~1-2 seconds
165
+ │ Result: Pass → Done, Fail → Escalate
163
166
  └──────────────────────┬──────────────────────────────────────┘
164
167
  │ Failure
165
168
 
166
169
  ┌─────────────────────────────────────────────────────────────┐
167
- │ DETAIL LAYER
170
+ │ DETAIL LAYER
168
171
  │ Full test: All endpoints with blame report │
169
- │ Duration: ~3-5 seconds
172
+ │ Duration: ~3-5 seconds
170
173
  │ Result: Regression report with file/line/commit │
171
174
  └─────────────────────────────────────────────────────────────┘
172
175
  ```
@@ -214,24 +217,31 @@ watch:
214
217
  - "*.txt"
215
218
  - "migrations/**"
216
219
 
220
+ # File types to watch (empty = watch all files)
221
+ # Only changes to these file extensions will trigger tests
222
+ file_types:
223
+ - ".py"
224
+ - ".ts"
225
+ - ".jsx"
226
+
217
227
  services:
218
- # Service configurations
219
- - name: "users"
220
- root: "app/users"
228
+ # Service configurations - simplified with auto-detection
229
+ # If paths are empty, WUP auto-detects files by service name
230
+
231
+ - name: "users-shell"
232
+ type: "shell"
233
+ # Auto-detects files containing "users-shell"
234
+
235
+ - name: "users-web"
236
+ type: "web"
237
+ # Auto-detects files containing "users-web"
238
+ # Will detect coincidence with users-shell
239
+
240
+ # Or use explicit paths (old style still works)
241
+ - name: "payments"
221
242
  paths:
222
- - "app/users/**"
223
- - "routes/users/**"
224
- quick_tests:
225
- scope: "read,write"
226
- max_endpoints: 3
227
- detail_tests:
228
- scope: "all"
229
- max_endpoints: 10
230
- cpu_throttle: 0.7
231
- notify:
232
- type: "http+file"
233
- url: "http://localhost:8001/notify"
234
- file: "wup/notify-users.json"
243
+ - "app/payments/**"
244
+ type: "auto"
235
245
 
236
246
  test_strategy:
237
247
  quick:
@@ -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.2.2-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.90-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.2.6-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-$1.05-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.9000 (6 commits)
9
+ - 🤖 **LLM usage:** $1.0500 (7 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.2.2-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.2.6-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
 
@@ -96,6 +96,9 @@ wup watch ./my-project --config custom-config.yaml
96
96
 
97
97
  # TestQL mode
98
98
  wup watch ./my-project --mode testql
99
+
100
+ # Discover endpoints from TestQL scenarios
101
+ wup testql-endpoints /path/to/scenarios --output testql-deps.json
99
102
  ```
100
103
 
101
104
  ### Initialize Configuration
@@ -124,24 +127,24 @@ wup status --deps my-deps.json
124
127
 
125
128
  ```
126
129
  ┌─────────────────────────────────────────────────────────────┐
127
- │ DETECTION LAYER
130
+ │ DETECTION LAYER
128
131
  │ File watching with watchdog + heuristics │
129
132
  │ Skips: .git, __pycache__, node_modules, .venv │
130
133
  └──────────────────────┬──────────────────────────────────────┘
131
134
  │ File change
132
135
 
133
136
  ┌─────────────────────────────────────────────────────────────┐
134
- │ PRIORITY LAYER
137
+ │ PRIORITY LAYER
135
138
  │ Quick test: 3 endpoints max per service │
136
- │ Duration: ~1-2 seconds
137
- │ Result: Pass → Done, Fail → Escalate
139
+ │ Duration: ~1-2 seconds
140
+ │ Result: Pass → Done, Fail → Escalate
138
141
  └──────────────────────┬──────────────────────────────────────┘
139
142
  │ Failure
140
143
 
141
144
  ┌─────────────────────────────────────────────────────────────┐
142
- │ DETAIL LAYER
145
+ │ DETAIL LAYER
143
146
  │ Full test: All endpoints with blame report │
144
- │ Duration: ~3-5 seconds
147
+ │ Duration: ~3-5 seconds
145
148
  │ Result: Regression report with file/line/commit │
146
149
  └─────────────────────────────────────────────────────────────┘
147
150
  ```
@@ -189,24 +192,31 @@ watch:
189
192
  - "*.txt"
190
193
  - "migrations/**"
191
194
 
195
+ # File types to watch (empty = watch all files)
196
+ # Only changes to these file extensions will trigger tests
197
+ file_types:
198
+ - ".py"
199
+ - ".ts"
200
+ - ".jsx"
201
+
192
202
  services:
193
- # Service configurations
194
- - name: "users"
195
- root: "app/users"
203
+ # Service configurations - simplified with auto-detection
204
+ # If paths are empty, WUP auto-detects files by service name
205
+
206
+ - name: "users-shell"
207
+ type: "shell"
208
+ # Auto-detects files containing "users-shell"
209
+
210
+ - name: "users-web"
211
+ type: "web"
212
+ # Auto-detects files containing "users-web"
213
+ # Will detect coincidence with users-shell
214
+
215
+ # Or use explicit paths (old style still works)
216
+ - name: "payments"
196
217
  paths:
197
- - "app/users/**"
198
- - "routes/users/**"
199
- quick_tests:
200
- scope: "read,write"
201
- max_endpoints: 3
202
- detail_tests:
203
- scope: "all"
204
- max_endpoints: 10
205
- cpu_throttle: 0.7
206
- notify:
207
- type: "http+file"
208
- url: "http://localhost:8001/notify"
209
- file: "wup/notify-users.json"
218
+ - "app/payments/**"
219
+ type: "auto"
210
220
 
211
221
  test_strategy:
212
222
  quick:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wup"
7
- version = "0.2.2"
7
+ version = "0.2.6"
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.2.2"
10
+ __version__ = "0.2.6"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -263,6 +263,69 @@ def init(
263
263
  console.print(f"[dim]Edit this file to customize your WUP setup[/dim]")
264
264
 
265
265
 
266
+ @app.command()
267
+ def testql_endpoints(
268
+ scenarios_dir: str = typer.Argument(..., help="Path to TestQL scenarios directory"),
269
+ output: str = typer.Option("testql-deps.json", "--output", "-o", help="Output dependency map file path"),
270
+ testql_bin: str = typer.Option("testql", "--testql-bin", help="TestQL executable name or path"),
271
+ ):
272
+ """
273
+ Discover endpoints from TestQL scenario files and build dependency map.
274
+ """
275
+ from .testql_discovery import TestQLEndpointDiscovery
276
+ from rich.table import Table
277
+
278
+ scenarios_path = Path(scenarios_dir)
279
+
280
+ if not scenarios_path.exists():
281
+ console.print(f"[red]Error: Scenarios directory '{scenarios_dir}' does not exist[/red]")
282
+ raise typer.Exit(1)
283
+
284
+ console.print(f"[cyan]🔍 Discovering endpoints from TestQL scenarios...[/cyan]")
285
+ console.print(f"[dim]Scenarios directory: {scenarios_dir}[/dim]")
286
+ console.print()
287
+
288
+ discovery = TestQLEndpointDiscovery(scenarios_dir, testql_bin)
289
+ dependency_map = discovery.to_dependency_map()
290
+
291
+ # Display results
292
+ table = Table(title="Discovered Endpoints")
293
+ table.add_column("Service", style="cyan")
294
+ table.add_column("Endpoints", style="green")
295
+ table.add_column("Scenarios", style="yellow")
296
+
297
+ total_endpoints = 0
298
+ total_scenarios = 0
299
+
300
+ for service, info in sorted(dependency_map.get("services", {}).items()):
301
+ endpoints_count = len(info.get("endpoints", []))
302
+ scenarios_count = len(info.get("scenarios", []))
303
+ total_endpoints += endpoints_count
304
+ total_scenarios += scenarios_count
305
+
306
+ table.add_row(
307
+ service,
308
+ str(endpoints_count),
309
+ str(scenarios_count)
310
+ )
311
+
312
+ console.print(table)
313
+ console.print()
314
+ console.print(f"[bold]Summary:[/bold]")
315
+ console.print(f" Services: {len(dependency_map.get('services', {}))}")
316
+ console.print(f" Total endpoints: {total_endpoints}")
317
+ console.print(f" Total scenarios: {total_scenarios}")
318
+ console.print()
319
+
320
+ # Save to file
321
+ import json
322
+ output_path = Path(output)
323
+ with open(output_path, 'w') as f:
324
+ json.dump(dependency_map, f, indent=2)
325
+
326
+ console.print(f"[green]✓ Dependency map saved to {output_path}[/green]")
327
+
328
+
266
329
  @app.command()
267
330
  def version():
268
331
  """Show WUP version."""
@@ -99,7 +99,8 @@ def validate_config(raw: dict) -> WupConfig:
99
99
  watch_raw = raw.get("watch", {})
100
100
  watch = WatchConfig(
101
101
  paths=watch_raw.get("paths", []),
102
- exclude_patterns=watch_raw.get("exclude_patterns", ["*.md", "*.txt"])
102
+ exclude_patterns=watch_raw.get("exclude_patterns", ["*.md", "*.txt"]),
103
+ file_types=watch_raw.get("file_types", [])
103
104
  )
104
105
 
105
106
  # Parse services
@@ -117,6 +118,7 @@ def validate_config(raw: dict) -> WupConfig:
117
118
  name=svc_raw["name"],
118
119
  root=svc_raw.get("root", ""),
119
120
  paths=svc_raw.get("paths", []),
121
+ type=svc_raw.get("type", "auto"),
120
122
  quick_tests=ServiceTestConfig(
121
123
  scope=quick_tests_raw.get("scope", "all"),
122
124
  max_endpoints=quick_tests_raw.get("max_endpoints", 10)
@@ -147,7 +149,8 @@ def validate_config(raw: dict) -> WupConfig:
147
149
  scenario_dir=testql_raw.get("scenario_dir", "scenarios/tests"),
148
150
  smoke_scenario=testql_raw.get("smoke_scenario", "smoke.testql.toon.yaml"),
149
151
  output_format=testql_raw.get("output_format", "json"),
150
- extra_args=testql_raw.get("extra_args", ["--timeout 10s"])
152
+ extra_args=testql_raw.get("extra_args", ["--timeout 10s"]),
153
+ endpoint_discovery=testql_raw.get("endpoint_discovery", True)
151
154
  )
152
155
 
153
156
  return WupConfig(
@@ -107,9 +107,7 @@ class WupWatcher:
107
107
  """
108
108
  Infer service name from file path.
109
109
 
110
- Examples:
111
- app/users/routes.py → "app/users"
112
- src/components/auth.ts → "src/components"
110
+ Uses config services first, then dependency mapper, then heuristics.
113
111
  """
114
112
  rel_path = self._to_relative_path(file_path)
115
113
  parts = rel_path.parts
@@ -117,10 +115,17 @@ class WupWatcher:
117
115
  # Try to match against configured services first
118
116
  if self.config.services:
119
117
  for svc in self.config.services:
120
- for svc_path in svc.paths:
121
- # Convert glob pattern to simple check
122
- if str(rel_path).startswith(svc_path.replace("**", "")):
123
- return svc.name
118
+ if svc.paths:
119
+ # Use explicit paths if provided
120
+ for svc_path in svc.paths:
121
+ if str(rel_path).startswith(svc_path.replace("**", "")):
122
+ return svc.name
123
+ else:
124
+ # Auto-detect: check if service name appears in path
125
+ service_name_parts = svc.name.replace("/", " ").replace("-", " ").split()
126
+ for part in service_name_parts:
127
+ if part.lower() in str(rel_path).lower():
128
+ return svc.name
124
129
 
125
130
  # Use dependency mapper if available
126
131
  service = self.dependency_mapper.get_service_for_file(file_path)
@@ -133,6 +138,75 @@ class WupWatcher:
133
138
 
134
139
  return None
135
140
 
141
+ def detect_service_coincidences(self, changed_service: str) -> List[str]:
142
+ """
143
+ Detect coincidences between services (e.g., shell <-> web).
144
+
145
+ When a service changes, this finds related services that should also be tested.
146
+
147
+ Args:
148
+ changed_service: The service that changed
149
+
150
+ Returns:
151
+ List of related services that should also be tested
152
+ """
153
+ related_services = []
154
+
155
+ if not self.config.services:
156
+ return related_services
157
+
158
+ # Get the changed service config
159
+ changed_svc_config = None
160
+ for svc in self.config.services:
161
+ if svc.name == changed_service:
162
+ changed_svc_config = svc
163
+ break
164
+
165
+ if not changed_svc_config:
166
+ return related_services
167
+
168
+ # Find coincidences based on service type
169
+ for svc in self.config.services:
170
+ if svc.name == changed_service:
171
+ continue
172
+
173
+ # Coincidence: shell <-> web for same domain
174
+ if changed_svc_config.type != "auto" and svc.type != "auto":
175
+ # If both have explicit types, check for opposites
176
+ if (changed_svc_config.type == "shell" and svc.type == "web") or \
177
+ (changed_svc_config.type == "web" and svc.type == "shell"):
178
+ # Check if they share a common domain (same base name)
179
+ if self._services_share_domain(changed_service, svc.name):
180
+ related_services.append(svc.name)
181
+
182
+ # Coincidence: auto-detect by name similarity
183
+ elif changed_svc_config.type == "auto" or svc.type == "auto":
184
+ if self._services_share_domain(changed_service, svc.name):
185
+ related_services.append(svc.name)
186
+
187
+ return related_services
188
+
189
+ def _services_share_domain(self, service1: str, service2: str) -> bool:
190
+ """
191
+ Check if two services share a common domain/base name.
192
+
193
+ Examples:
194
+ users-shell and users-web -> True
195
+ api/auth and api/users -> False
196
+ payments and payments-shell -> True
197
+ """
198
+ # Extract base names (remove type suffixes like -shell, -web)
199
+ def extract_base(name: str) -> str:
200
+ for suffix in ["-shell", "-web", "_shell", "_web"]:
201
+ if name.endswith(suffix):
202
+ return name[:-len(suffix)]
203
+ return name
204
+
205
+ base1 = extract_base(service1)
206
+ base2 = extract_base(service2)
207
+
208
+ return base1 == base2
209
+
136
210
  def get_service_config(self, service_name: str) -> Optional[ServiceConfig]:
137
211
  """
138
212
  Get service configuration by name.
@@ -316,6 +390,17 @@ class WupWatcher:
316
390
  if pattern in str(rel_path):
317
391
  return
318
392
 
393
+ # Filter by file type if specified in config
394
+ if self.config.watch.file_types:
395
+ # Ensure file extensions start with dot
396
+ file_ext = rel_path.suffix if rel_path.suffix else ""
397
+ if not file_ext.startswith("."):
398
+ file_ext = f".{file_ext}"
399
+
400
+ # Check if file extension matches any of the configured types
401
+ if file_ext not in self.config.watch.file_types:
402
+ return
403
+
319
404
  # Infer service from file path
320
405
  service = self.infer_service(file_path)
321
406
 
@@ -14,6 +14,7 @@ from pathlib import Path
14
14
  from typing import Dict, List, Set, Optional
15
15
  from collections import defaultdict
16
16
  import re
17
+ from .testql_discovery import TestQLEndpointDiscovery
17
18
 
18
19
 
19
20
  class DependencyMapper:
@@ -257,3 +258,27 @@ class DependencyMapper:
257
258
  self.service_to_files[service] = set(info.get("files", []))
258
259
 
259
260
  self.file_to_endpoints = defaultdict(list, data.get("files", {}))
261
+
262
+ def build_from_testql_scenarios(self, scenarios_dir: str, testql_bin: str = "testql") -> Dict:
263
+ """
264
+ Build dependency map from TestQL scenario files.
265
+
266
+ Args:
267
+ scenarios_dir: Path to TestQL scenarios directory
268
+ testql_bin: TestQL executable name or path
269
+
270
+ Returns:
271
+ Dictionary containing the full dependency map
272
+ """
273
+ discovery = TestQLEndpointDiscovery(scenarios_dir, testql_bin)
274
+ dependency_map = discovery.to_dependency_map()
275
+
276
+ # Merge with existing mappings
277
+ for service, info in dependency_map.get("services", {}).items():
278
+ self.service_to_endpoints[service].extend(info["endpoints"])
279
+ self.service_to_files[service].update(info["files"])
280
+
281
+ for file_path, endpoints in dependency_map.get("files", {}).items():
282
+ self.file_to_endpoints[file_path].extend(endpoints)
283
+
284
+ return self.to_dict()
@@ -27,8 +27,9 @@ class ServiceTestConfig:
27
27
  class ServiceConfig:
28
28
  """Configuration for a single service."""
29
29
  name: str
30
- root: str
31
- paths: List[str] = field(default_factory=list)
30
+ root: str = "" # Optional - auto-detected if empty
31
+ paths: List[str] = field(default_factory=list) # Optional - auto-detected if empty
32
+ type: str = "auto" # "web", "shell", "auto" - for coincidence detection
32
33
  quick_tests: ServiceTestConfig = field(default_factory=ServiceTestConfig)
33
34
  detail_tests: ServiceTestConfig = field(default_factory=ServiceTestConfig)
34
35
  cpu_throttle: float = 0.8
@@ -40,6 +41,7 @@ class WatchConfig:
40
41
  """Configuration for file watching."""
41
42
  paths: List[str] = field(default_factory=list)
42
43
  exclude_patterns: List[str] = field(default_factory=lambda: ["*.md", "*.txt"])
44
+ file_types: List[str] = field(default_factory=list) # e.g., [".py", ".ts", ".jsx"]
43
45
 
44
46
 
45
47
  @dataclass
@@ -56,6 +58,7 @@ class TestQLConfig:
56
58
  smoke_scenario: str = "smoke.testql.toon.yaml"
57
59
  output_format: str = "json"
58
60
  extra_args: List[str] = field(default_factory=lambda: ["--timeout 10s"])
61
+ endpoint_discovery: bool = True # Enable automatic endpoint discovery from scenarios
59
62
 
60
63
 
61
64
  @dataclass
@@ -0,0 +1,229 @@
1
+ """
2
+ TestQL endpoint discovery module.
3
+
4
+ Discovers API endpoints from TestQL scenario files (.testql.toon.yaml)
5
+ and maps them to services for intelligent testing.
6
+ """
7
+
8
+ import re
9
+ import subprocess
10
+ from pathlib import Path
11
+ from typing import Dict, List, Optional, Set
12
+
13
+ import yaml
14
+
15
+
16
+ class TestQLEndpointDiscovery:
17
+ """Discover endpoints from TestQL scenario files."""
18
+
19
+ def __init__(self, scenarios_dir: str, testql_bin: str = "testql"):
20
+ """
21
+ Initialize TestQL endpoint discovery.
22
+
23
+ Args:
24
+ scenarios_dir: Path to TestQL scenarios directory
25
+ testql_bin: TestQL executable name or path
26
+ """
27
+ self.scenarios_dir = Path(scenarios_dir)
28
+ self.testql_bin = testql_bin
29
+ self.endpoints_by_service: Dict[str, Set[str]] = {}
30
+ self.scenarios_by_service: Dict[str, List[Path]] = {}
31
+
32
+ def discover_scenarios(self) -> List[Path]:
33
+ """
34
+ Find all TestQL scenario files.
35
+
36
+ Returns:
37
+ List of paths to .testql.toon.yaml files
38
+ """
39
+ if not self.scenarios_dir.exists():
40
+ return []
41
+
42
+ return sorted(self.scenarios_dir.rglob("*.testql.toon.yaml"))
43
+
44
+ def parse_scenario_endpoints(self, scenario_path: Path) -> List[str]:
45
+ """
46
+ Extract endpoints from a TestQL scenario file.
47
+
48
+ Args:
49
+ scenario_path: Path to scenario file
50
+
51
+ Returns:
52
+ List of endpoint paths found in the scenario
53
+ """
54
+ endpoints = []
55
+
56
+ try:
57
+ with open(scenario_path, 'r') as f:
58
+ content = f.read()
59
+
60
+ # Parse TestQL API blocks
61
+ # Pattern: METHOD, /path
62
+ api_pattern = re.compile(r'^\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*,\s*([^\s,]+)', re.MULTILINE)
63
+ matches = api_pattern.findall(content)
64
+
65
+ for method, path in matches:
66
+ endpoints.append(f"{method.upper()} {path}")
67
+
68
+ # Also try parsing as YAML to extract structured data
69
+ try:
70
+ data = yaml.safe_load(content)
71
+ if data and isinstance(data, dict):
72
+ # Look for API sections
73
+ if 'API' in data:
74
+ api_data = data['API']
75
+ if isinstance(api_data, list):
76
+ for item in api_data:
77
+ if isinstance(item, (list, tuple)) and len(item) >= 2:
78
+ method = item[0]
79
+ path = item[1]
80
+ endpoints.append(f"{method.upper()} {path}")
81
+ except:
82
+ pass
83
+
84
+ except Exception as e:
85
+ print(f"Warning: Could not parse {scenario_path}: {e}")
86
+
87
+ return list(set(endpoints)) # Remove duplicates
88
+
89
+ def infer_service_from_scenario(self, scenario_path: Path) -> Optional[str]:
90
+ """
91
+ Infer service name from scenario file path.
92
+
93
+ Args:
94
+ scenario_path: Path to scenario file
95
+
96
+ Returns:
97
+ Service name or None
98
+ """
99
+ # Extract service from path
100
+ # e.g., scenarios/tests/users/users-api.testql.toon.yaml -> users
101
+ rel_path = scenario_path.relative_to(self.scenarios_dir)
102
+ parts = rel_path.parts
103
+
104
+ # Look for service-like patterns in the path
105
+ for part in parts:
106
+ if part not in ['tests', 'scenarios', 'api', 'views']:
107
+ # Clean up the name
108
+ service = part.replace('-', '/').replace('_', '/')
109
+ return service
110
+
111
+ # Fallback to parent directory name
112
+ if scenario_path.parent.name != self.scenarios_dir.name:
113
+ return scenario_path.parent.name
114
+
115
+ return None
116
+
117
+ def discover_all_endpoints(self) -> Dict[str, Dict]:
118
+ """
119
+ Discover all endpoints from scenarios.
120
+
121
+ Returns:
122
+ Dictionary mapping service names to endpoint info:
123
+ {
124
+ "service_name": {
125
+ "endpoints": ["GET /api/users", ...],
126
+ "scenarios": [path1, path2, ...]
127
+ }
128
+ }
129
+ """
130
+ scenarios = self.discover_scenarios()
131
+
132
+ for scenario in scenarios:
133
+ endpoints = self.parse_scenario_endpoints(scenario)
134
+ service = self.infer_service_from_scenario(scenario)
135
+
136
+ if not service:
137
+ continue
138
+
139
+ if service not in self.endpoints_by_service:
140
+ self.endpoints_by_service[service] = set()
141
+ self.scenarios_by_service[service] = []
142
+
143
+ self.endpoints_by_service[service].update(endpoints)
144
+ self.scenarios_by_service[service].append(scenario)
145
+
146
+ # Convert to output format
147
+ result = {}
148
+ for service in self.endpoints_by_service:
149
+ result[service] = {
150
+ "endpoints": sorted(list(self.endpoints_by_service[service])),
151
+ "scenarios": [str(s) for s in self.scenarios_by_service[service]]
152
+ }
153
+
154
+ return result
155
+
156
+ def discover_via_testql_cli(self, service: Optional[str] = None) -> List[str]:
157
+ """
158
+ Use TestQL CLI to discover endpoints.
159
+
160
+ Args:
161
+ service: Optional service name to filter
162
+
163
+ Returns:
164
+ List of discovered endpoints
165
+ """
166
+ try:
167
+ cmd = [self.testql_bin, "endpoints", str(self.scenarios_dir)]
168
+ if service:
169
+ cmd.extend(["--service", service])
170
+
171
+ result = subprocess.run(
172
+ cmd,
173
+ capture_output=True,
174
+ text=True,
175
+ timeout=30
176
+ )
177
+
178
+ if result.returncode == 0:
179
+ # Parse output - assuming one endpoint per line
180
+ endpoints = [line.strip() for line in result.stdout.split('\n') if line.strip()]
181
+ return endpoints
182
+ else:
183
+ print(f"TestQL CLI error: {result.stderr}")
184
+ return []
185
+
186
+ except FileNotFoundError:
187
+ print(f"TestQL binary '{self.testql_bin}' not found. Using file-based discovery.")
188
+ return []
189
+ except subprocess.TimeoutExpired:
190
+ print("TestQL CLI timeout. Using file-based discovery.")
191
+ return []
192
+ except Exception as e:
193
+ print(f"TestQL CLI error: {e}")
194
+ return []
195
+
196
+ def to_dependency_map(self) -> Dict:
197
+ """
198
+ Convert discovered endpoints to dependency map format.
199
+
200
+ Returns:
201
+ Dictionary in dependency map format:
202
+ {
203
+ "services": {
204
+ "service_name": {
205
+ "endpoints": [...],
206
+ "files": [...]
207
+ }
208
+ },
209
+ "files": {...}
210
+ }
211
+ """
212
+ discovery = self.discover_all_endpoints()
213
+
214
+ dependency_map = {
215
+ "services": {},
216
+ "files": {}
217
+ }
218
+
219
+ for service, info in discovery.items():
220
+ dependency_map["services"][service] = {
221
+ "endpoints": info["endpoints"],
222
+ "files": [str(s) for s in info["scenarios"]]
223
+ }
224
+
225
+ # Map files to endpoints
226
+ for scenario in info["scenarios"]:
227
+ dependency_map["files"][scenario] = info["endpoints"]
228
+
229
+ return dependency_map
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.2
3
+ Version: 0.2.6
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.2.2-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.90-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.2.6-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-$1.05-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.9000 (6 commits)
34
+ - 🤖 **LLM usage:** $1.0500 (7 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.2.2-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.2.6-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
 
@@ -121,6 +121,9 @@ wup watch ./my-project --config custom-config.yaml
121
121
 
122
122
  # TestQL mode
123
123
  wup watch ./my-project --mode testql
124
+
125
+ # Discover endpoints from TestQL scenarios
126
+ wup testql-endpoints /path/to/scenarios --output testql-deps.json
124
127
  ```
125
128
 
126
129
  ### Initialize Configuration
@@ -149,24 +152,24 @@ wup status --deps my-deps.json
149
152
 
150
153
  ```
151
154
  ┌─────────────────────────────────────────────────────────────┐
152
- │ DETECTION LAYER
155
+ │ DETECTION LAYER
153
156
  │ File watching with watchdog + heuristics │
154
157
  │ Skips: .git, __pycache__, node_modules, .venv │
155
158
  └──────────────────────┬──────────────────────────────────────┘
156
159
  │ File change
157
160
 
158
161
  ┌─────────────────────────────────────────────────────────────┐
159
- │ PRIORITY LAYER
162
+ │ PRIORITY LAYER
160
163
  │ Quick test: 3 endpoints max per service │
161
- │ Duration: ~1-2 seconds
162
- │ Result: Pass → Done, Fail → Escalate
164
+ │ Duration: ~1-2 seconds
165
+ │ Result: Pass → Done, Fail → Escalate
163
166
  └──────────────────────┬──────────────────────────────────────┘
164
167
  │ Failure
165
168
 
166
169
  ┌─────────────────────────────────────────────────────────────┐
167
- │ DETAIL LAYER
170
+ │ DETAIL LAYER
168
171
  │ Full test: All endpoints with blame report │
169
- │ Duration: ~3-5 seconds
172
+ │ Duration: ~3-5 seconds
170
173
  │ Result: Regression report with file/line/commit │
171
174
  └─────────────────────────────────────────────────────────────┘
172
175
  ```
@@ -214,24 +217,31 @@ watch:
214
217
  - "*.txt"
215
218
  - "migrations/**"
216
219
 
220
+ # File types to watch (empty = watch all files)
221
+ # Only changes to these file extensions will trigger tests
222
+ file_types:
223
+ - ".py"
224
+ - ".ts"
225
+ - ".jsx"
226
+
217
227
  services:
218
- # Service configurations
219
- - name: "users"
220
- root: "app/users"
228
+ # Service configurations - simplified with auto-detection
229
+ # If paths are empty, WUP auto-detects files by service name
230
+
231
+ - name: "users-shell"
232
+ type: "shell"
233
+ # Auto-detects files containing "users-shell"
234
+
235
+ - name: "users-web"
236
+ type: "web"
237
+ # Auto-detects files containing "users-web"
238
+ # Will detect coincidence with users-shell
239
+
240
+ # Or use explicit paths (old style still works)
241
+ - name: "payments"
221
242
  paths:
222
- - "app/users/**"
223
- - "routes/users/**"
224
- quick_tests:
225
- scope: "read,write"
226
- max_endpoints: 3
227
- detail_tests:
228
- scope: "all"
229
- max_endpoints: 10
230
- cpu_throttle: 0.7
231
- notify:
232
- type: "http+file"
233
- url: "http://localhost:8001/notify"
234
- file: "wup/notify-users.json"
243
+ - "app/payments/**"
244
+ type: "auto"
235
245
 
236
246
  test_strategy:
237
247
  quick:
@@ -8,6 +8,7 @@ wup/cli.py
8
8
  wup/config.py
9
9
  wup/core.py
10
10
  wup/dependency_mapper.py
11
+ wup/testql_discovery.py
11
12
  wup/testql_watcher.py
12
13
  wup.egg-info/PKG-INFO
13
14
  wup.egg-info/SOURCES.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