wup 0.2.71__tar.gz → 0.2.72__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.
- {wup-0.2.71/wup.egg-info → wup-0.2.72}/PKG-INFO +6 -6
- {wup-0.2.71 → wup-0.2.72}/README.md +5 -5
- {wup-0.2.71 → wup-0.2.72}/pyproject.toml +1 -1
- {wup-0.2.71 → wup-0.2.72}/wup/__init__.py +1 -1
- wup-0.2.72/wup/discovery.py +271 -0
- {wup-0.2.71 → wup-0.2.72/wup.egg-info}/PKG-INFO +6 -6
- {wup-0.2.71 → wup-0.2.72}/wup.egg-info/SOURCES.txt +1 -0
- {wup-0.2.71 → wup-0.2.72}/LICENSE +0 -0
- {wup-0.2.71 → wup-0.2.72}/setup.cfg +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_assistant.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_auto_detection.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_cli_bridge.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_cli_filtering.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_control.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_e2e.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_endpoints_init_cli.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_genericity.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_health_summary_passed.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_multi_project.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_planfile_reporter_dedupe.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_probe_mutex.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_service_inference.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_status_data.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_sync.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_testql_monitor.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_testql_watcher.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_visual_diff_periodic_skip.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_visual_diff_progress.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_watch_exclude.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_watch_no_paths_exit.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_web_client.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_wup.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/tests/test_wup_generate.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/_ast_detector.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/_base_detector.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/_hash_detector.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/_yaml_detector.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/anomaly_detector.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/anomaly_models.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/assistant.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/assistant_discovery.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/assistant_validator.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/bus.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/cli.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/cli_bridge.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/cli_config_generator.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/cli_scanner.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/config.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/control.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/core.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/dependency_mapper.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/endpoints.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/event_store.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/file_watcher/events/file_events.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/generate.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/init_cli.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/models/__init__.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/models/config.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/models/target.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/monitoring_manifest.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/multi.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/paths.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/planfile_reporter.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/status_data.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/sync.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/testing/events/health_events.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/testing/events/test_results.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/testing/handlers/event_handlers.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/testing/handlers/health_handlers.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/testing/queries/health_queries.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/testql_cli_generator.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/testql_discovery.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/testql_monitor.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/testql_watcher.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/validate.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/visual_diff.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup/web_client.py +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.71 → wup-0.2.72}/wup.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.72
|
|
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
|
|
@@ -42,17 +42,17 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
## AI Cost Tracking
|
|
44
44
|
|
|
45
|
-
    
|
|
46
|
+
  
|
|
47
47
|
|
|
48
|
-
- 🤖 **LLM usage:** $7.
|
|
49
|
-
- 👤 **Human dev:** ~$
|
|
48
|
+
- 🤖 **LLM usage:** $7.2386 (100 commits)
|
|
49
|
+
- 👤 **Human dev:** ~$5235 (52.4h @ $100/h, 30min dedup)
|
|
50
50
|
|
|
51
51
|
Generated on 2026-07-17 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
-
    
|
|
56
56
|
|
|
57
57
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
58
58
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $7.2386 (100 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$5235 (52.4h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-07-17 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
19
19
|
|
|
@@ -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.
|
|
10
|
+
__version__ = "0.2.72"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pluggable endpoint-discovery adapters.
|
|
3
|
+
|
|
4
|
+
Each adapter knows how to (a) detect whether a project uses its ecosystem and
|
|
5
|
+
(b) extract HTTP endpoints from the source. `DependencyMapper` delegates to the
|
|
6
|
+
registry here so `deps.json` is language-agnostic: adding support for a new
|
|
7
|
+
framework means adding an adapter, not editing the mapper.
|
|
8
|
+
|
|
9
|
+
Adapters are deliberately regex-based (no imports of the target frameworks) so
|
|
10
|
+
discovery stays fast and dependency-free.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Callable, Dict, Iterator, List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
# Source extensions grouped by ecosystem.
|
|
22
|
+
PY_EXTENSIONS = (".py",)
|
|
23
|
+
JS_EXTENSIONS = (".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs")
|
|
24
|
+
GO_EXTENSIONS = (".go",)
|
|
25
|
+
OPENAPI_EXTENSIONS = (".yaml", ".yml", ".json")
|
|
26
|
+
ALL_SOURCE_EXTENSIONS = PY_EXTENSIONS + JS_EXTENSIONS + GO_EXTENSIONS
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Endpoint:
|
|
31
|
+
"""A discovered HTTP endpoint."""
|
|
32
|
+
method: str
|
|
33
|
+
path: str
|
|
34
|
+
file: str
|
|
35
|
+
framework: str
|
|
36
|
+
|
|
37
|
+
def as_dict(self) -> Dict[str, str]:
|
|
38
|
+
return {"method": self.method, "path": self.path, "file": self.file, "framework": self.framework}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SourceIndex:
|
|
42
|
+
"""
|
|
43
|
+
Reads and caches project source files once, so every adapter's detect() and
|
|
44
|
+
scan() reuse the same content instead of re-walking the tree.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, project_root: Path):
|
|
48
|
+
self.project_root = Path(project_root)
|
|
49
|
+
self._cache: Dict[str, List[Tuple[str, str]]] = {}
|
|
50
|
+
|
|
51
|
+
def _read_ext(self, ext: str) -> List[Tuple[str, str]]:
|
|
52
|
+
if ext in self._cache:
|
|
53
|
+
return self._cache[ext]
|
|
54
|
+
items: List[Tuple[str, str]] = []
|
|
55
|
+
if self.project_root.exists():
|
|
56
|
+
for path in self.project_root.rglob(f"*{ext}"):
|
|
57
|
+
if not path.is_file():
|
|
58
|
+
continue
|
|
59
|
+
try:
|
|
60
|
+
content = path.read_text()
|
|
61
|
+
except (UnicodeDecodeError, PermissionError, OSError):
|
|
62
|
+
continue
|
|
63
|
+
items.append((str(path.relative_to(self.project_root)), content))
|
|
64
|
+
self._cache[ext] = items
|
|
65
|
+
return items
|
|
66
|
+
|
|
67
|
+
def files(self, extensions: Tuple[str, ...]) -> Iterator[Tuple[str, str]]:
|
|
68
|
+
"""Yield (relative_path, content) for every file with one of the extensions."""
|
|
69
|
+
for ext in extensions:
|
|
70
|
+
yield from self._read_ext(ext)
|
|
71
|
+
|
|
72
|
+
def contains(self, marker: str, extensions: Tuple[str, ...]) -> bool:
|
|
73
|
+
"""True if any file with the given extensions contains the marker string."""
|
|
74
|
+
return any(marker in content for _, content in self.files(extensions))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class DiscoveryAdapter:
|
|
78
|
+
"""Base adapter. Subclasses set name/extensions/markers or override scan()."""
|
|
79
|
+
|
|
80
|
+
name: str = "generic"
|
|
81
|
+
extensions: Tuple[str, ...] = ()
|
|
82
|
+
detect_markers: Tuple[str, ...] = ()
|
|
83
|
+
# (compiled regex, method-group index or None, path-group index)
|
|
84
|
+
route_patterns: Tuple[Tuple[re.Pattern, Optional[int], int], ...] = ()
|
|
85
|
+
default_method: str = "GET"
|
|
86
|
+
# When True, a matching route pattern alone counts as detection (for
|
|
87
|
+
# ecosystems whose route syntax is distinctive enough, e.g. Express
|
|
88
|
+
# `router.get(`). Kept False where syntax is too generic (Hono, Fastify's
|
|
89
|
+
# `url:`), which rely on import markers instead to avoid false positives.
|
|
90
|
+
detect_via_routes: bool = False
|
|
91
|
+
|
|
92
|
+
def detect(self, index: SourceIndex) -> bool:
|
|
93
|
+
if any(index.contains(m, self.extensions) for m in self.detect_markers):
|
|
94
|
+
return True
|
|
95
|
+
if self.detect_via_routes:
|
|
96
|
+
for _, content in index.files(self.extensions):
|
|
97
|
+
if any(p.search(content) for p, _, _ in self.route_patterns):
|
|
98
|
+
return True
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def scan(self, index: SourceIndex) -> List[Endpoint]:
|
|
102
|
+
endpoints: List[Endpoint] = []
|
|
103
|
+
for rel_path, content in index.files(self.extensions):
|
|
104
|
+
for pattern, method_group, path_group in self.route_patterns:
|
|
105
|
+
for match in pattern.finditer(content):
|
|
106
|
+
method = match.group(method_group).upper() if method_group else self.default_method
|
|
107
|
+
endpoints.append(Endpoint(method, match.group(path_group), rel_path, self.name))
|
|
108
|
+
return endpoints
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# --- concrete adapters ----------------------------------------------------
|
|
112
|
+
|
|
113
|
+
class FastAPIAdapter(DiscoveryAdapter):
|
|
114
|
+
name = "fastapi"
|
|
115
|
+
extensions = PY_EXTENSIONS
|
|
116
|
+
detect_markers = ("FastAPI", "APIRouter", "from fastapi")
|
|
117
|
+
route_patterns = (
|
|
118
|
+
(re.compile(r'@(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*["\']([^"\']+)["\']'), 1, 2),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class FlaskAdapter(DiscoveryAdapter):
|
|
123
|
+
name = "flask"
|
|
124
|
+
extensions = PY_EXTENSIONS
|
|
125
|
+
detect_markers = ("from flask", "Flask(", "Blueprint(")
|
|
126
|
+
route_patterns = (
|
|
127
|
+
(re.compile(r'@(?:app|bp)\.(get|post|put|delete|patch)\s*\(\s*["\']([^"\']+)["\']'), 1, 2),
|
|
128
|
+
(re.compile(r'@(?:app|bp)\.route\s*\(\s*["\']([^"\']+)["\']'), None, 1),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class DjangoAdapter(DiscoveryAdapter):
|
|
133
|
+
name = "django"
|
|
134
|
+
extensions = PY_EXTENSIONS
|
|
135
|
+
detect_markers = ("urlpatterns", "from django.urls")
|
|
136
|
+
route_patterns = (
|
|
137
|
+
(re.compile(r'(?:^|\W)(?:path|re_path|url)\s*\(\s*["\']([^"\']*)["\']'), None, 1),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ExpressAdapter(DiscoveryAdapter):
|
|
142
|
+
name = "express"
|
|
143
|
+
extensions = JS_EXTENSIONS
|
|
144
|
+
detect_markers = ("express()", "require('express')", 'require("express")', "from 'express'", 'from "express"')
|
|
145
|
+
route_patterns = (
|
|
146
|
+
(re.compile(r'(?:app|router)\.(get|post|put|delete|patch|all)\s*\(\s*["\'`]([^"\'`]+)["\'`]'), 1, 2),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class FastifyAdapter(DiscoveryAdapter):
|
|
151
|
+
name = "fastify"
|
|
152
|
+
extensions = JS_EXTENSIONS
|
|
153
|
+
detect_markers = ("require('fastify')", 'require("fastify")', "from 'fastify'", 'from "fastify"')
|
|
154
|
+
route_patterns = (
|
|
155
|
+
(re.compile(r'fastify\.(get|post|put|delete|patch)\s*\(\s*["\'`]([^"\'`]+)["\'`]'), 1, 2),
|
|
156
|
+
# fastify.route({ method: 'GET', url: '/x' })
|
|
157
|
+
(re.compile(r'url\s*:\s*["\'`]([^"\'`]+)["\'`]'), None, 1),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class HonoAdapter(DiscoveryAdapter):
|
|
162
|
+
name = "hono"
|
|
163
|
+
extensions = JS_EXTENSIONS
|
|
164
|
+
detect_markers = ("from 'hono'", 'from "hono"', "new Hono(")
|
|
165
|
+
route_patterns = (
|
|
166
|
+
(re.compile(r'\b\w+\.(get|post|put|delete|patch)\s*\(\s*["\'`](/[^"\'`]*)["\'`]'), 1, 2),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class NestJSAdapter(DiscoveryAdapter):
|
|
171
|
+
name = "nestjs"
|
|
172
|
+
extensions = JS_EXTENSIONS
|
|
173
|
+
detect_markers = ("@nestjs/common", "@Controller(")
|
|
174
|
+
route_patterns = (
|
|
175
|
+
(re.compile(r'@(Get|Post|Put|Delete|Patch)\s*\(\s*["\'`]?([^"\'`)]*)["\'`]?\s*\)'), 1, 2),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class GoAdapter(DiscoveryAdapter):
|
|
180
|
+
name = "go"
|
|
181
|
+
extensions = GO_EXTENSIONS
|
|
182
|
+
detect_markers = ("net/http", "gin-gonic/gin", "labstack/echo", "gorilla/mux")
|
|
183
|
+
route_patterns = (
|
|
184
|
+
# gin/echo: r.GET("/x", ...), e.POST("/x", ...)
|
|
185
|
+
(re.compile(r'\.\b(GET|POST|PUT|DELETE|PATCH)\s*\(\s*"([^"]+)"'), 1, 2),
|
|
186
|
+
# net/http: http.HandleFunc("/x", ...) / mux.HandleFunc("/x", ...)
|
|
187
|
+
(re.compile(r'\.HandleFunc\s*\(\s*"([^"]+)"'), None, 1),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class OpenAPIAdapter(DiscoveryAdapter):
|
|
192
|
+
"""Extract paths from an OpenAPI/Swagger document (yaml or json)."""
|
|
193
|
+
name = "openapi"
|
|
194
|
+
extensions = OPENAPI_EXTENSIONS
|
|
195
|
+
|
|
196
|
+
def _load_spec(self, content: str) -> Optional[dict]:
|
|
197
|
+
text = content.lstrip()
|
|
198
|
+
try:
|
|
199
|
+
if text.startswith("{"):
|
|
200
|
+
return json.loads(content)
|
|
201
|
+
import yaml
|
|
202
|
+
data = yaml.safe_load(content)
|
|
203
|
+
return data if isinstance(data, dict) else None
|
|
204
|
+
except Exception:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
def detect(self, index: SourceIndex) -> bool:
|
|
208
|
+
for _, content in index.files(self.extensions):
|
|
209
|
+
spec = self._load_spec(content)
|
|
210
|
+
if spec and ("openapi" in spec or "swagger" in spec) and isinstance(spec.get("paths"), dict):
|
|
211
|
+
return True
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
def scan(self, index: SourceIndex) -> List[Endpoint]:
|
|
215
|
+
endpoints: List[Endpoint] = []
|
|
216
|
+
methods = {"get", "post", "put", "delete", "patch", "head", "options"}
|
|
217
|
+
for rel_path, content in index.files(self.extensions):
|
|
218
|
+
spec = self._load_spec(content)
|
|
219
|
+
if not (spec and isinstance(spec.get("paths"), dict)):
|
|
220
|
+
continue
|
|
221
|
+
for path, ops in spec["paths"].items():
|
|
222
|
+
if not isinstance(ops, dict):
|
|
223
|
+
continue
|
|
224
|
+
for method in ops:
|
|
225
|
+
if str(method).lower() in methods:
|
|
226
|
+
endpoints.append(Endpoint(str(method).upper(), str(path), rel_path, self.name))
|
|
227
|
+
return endpoints
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# Order matters only for auto-detection reporting; scanning runs all matches.
|
|
231
|
+
ADAPTERS: Tuple[DiscoveryAdapter, ...] = (
|
|
232
|
+
FastAPIAdapter(),
|
|
233
|
+
FlaskAdapter(),
|
|
234
|
+
DjangoAdapter(),
|
|
235
|
+
NestJSAdapter(), # before Express: @Controller is more specific
|
|
236
|
+
ExpressAdapter(),
|
|
237
|
+
FastifyAdapter(),
|
|
238
|
+
HonoAdapter(),
|
|
239
|
+
GoAdapter(),
|
|
240
|
+
OpenAPIAdapter(),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
ADAPTERS_BY_NAME: Dict[str, DiscoveryAdapter] = {a.name: a for a in ADAPTERS}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def detect_frameworks(index: SourceIndex) -> List[str]:
|
|
247
|
+
"""Return the names of every adapter whose ecosystem is present."""
|
|
248
|
+
return [a.name for a in ADAPTERS if a.detect(index)]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def discover_endpoints(
|
|
252
|
+
project_root: Path,
|
|
253
|
+
framework: str = "auto",
|
|
254
|
+
index: Optional[SourceIndex] = None,
|
|
255
|
+
) -> List[Endpoint]:
|
|
256
|
+
"""
|
|
257
|
+
Discover endpoints across all matching adapters.
|
|
258
|
+
|
|
259
|
+
framework="auto" runs every adapter that detects its ecosystem; an explicit
|
|
260
|
+
framework name runs just that adapter.
|
|
261
|
+
"""
|
|
262
|
+
index = index or SourceIndex(project_root)
|
|
263
|
+
if framework and framework != "auto":
|
|
264
|
+
adapter = ADAPTERS_BY_NAME.get(framework)
|
|
265
|
+
return adapter.scan(index) if adapter else []
|
|
266
|
+
|
|
267
|
+
endpoints: List[Endpoint] = []
|
|
268
|
+
for adapter in ADAPTERS:
|
|
269
|
+
if adapter.detect(index):
|
|
270
|
+
endpoints.extend(adapter.scan(index))
|
|
271
|
+
return endpoints
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.72
|
|
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
|
|
@@ -42,17 +42,17 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
## AI Cost Tracking
|
|
44
44
|
|
|
45
|
-
    
|
|
46
|
+
  
|
|
47
47
|
|
|
48
|
-
- 🤖 **LLM usage:** $7.
|
|
49
|
-
- 👤 **Human dev:** ~$
|
|
48
|
+
- 🤖 **LLM usage:** $7.2386 (100 commits)
|
|
49
|
+
- 👤 **Human dev:** ~$5235 (52.4h @ $100/h, 30min dedup)
|
|
50
50
|
|
|
51
51
|
Generated on 2026-07-17 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
-
    
|
|
56
56
|
|
|
57
57
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
58
58
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|