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.
Files changed (82) hide show
  1. {wup-0.2.71/wup.egg-info → wup-0.2.72}/PKG-INFO +6 -6
  2. {wup-0.2.71 → wup-0.2.72}/README.md +5 -5
  3. {wup-0.2.71 → wup-0.2.72}/pyproject.toml +1 -1
  4. {wup-0.2.71 → wup-0.2.72}/wup/__init__.py +1 -1
  5. wup-0.2.72/wup/discovery.py +271 -0
  6. {wup-0.2.71 → wup-0.2.72/wup.egg-info}/PKG-INFO +6 -6
  7. {wup-0.2.71 → wup-0.2.72}/wup.egg-info/SOURCES.txt +1 -0
  8. {wup-0.2.71 → wup-0.2.72}/LICENSE +0 -0
  9. {wup-0.2.71 → wup-0.2.72}/setup.cfg +0 -0
  10. {wup-0.2.71 → wup-0.2.72}/tests/test_assistant.py +0 -0
  11. {wup-0.2.71 → wup-0.2.72}/tests/test_auto_detection.py +0 -0
  12. {wup-0.2.71 → wup-0.2.72}/tests/test_cli_bridge.py +0 -0
  13. {wup-0.2.71 → wup-0.2.72}/tests/test_cli_filtering.py +0 -0
  14. {wup-0.2.71 → wup-0.2.72}/tests/test_control.py +0 -0
  15. {wup-0.2.71 → wup-0.2.72}/tests/test_e2e.py +0 -0
  16. {wup-0.2.71 → wup-0.2.72}/tests/test_endpoints_init_cli.py +0 -0
  17. {wup-0.2.71 → wup-0.2.72}/tests/test_genericity.py +0 -0
  18. {wup-0.2.71 → wup-0.2.72}/tests/test_health_summary_passed.py +0 -0
  19. {wup-0.2.71 → wup-0.2.72}/tests/test_monitoring_manifest.py +0 -0
  20. {wup-0.2.71 → wup-0.2.72}/tests/test_multi_project.py +0 -0
  21. {wup-0.2.71 → wup-0.2.72}/tests/test_planfile_reporter_dedupe.py +0 -0
  22. {wup-0.2.71 → wup-0.2.72}/tests/test_probe_mutex.py +0 -0
  23. {wup-0.2.71 → wup-0.2.72}/tests/test_service_inference.py +0 -0
  24. {wup-0.2.71 → wup-0.2.72}/tests/test_status_data.py +0 -0
  25. {wup-0.2.71 → wup-0.2.72}/tests/test_sync.py +0 -0
  26. {wup-0.2.71 → wup-0.2.72}/tests/test_testql_monitor.py +0 -0
  27. {wup-0.2.71 → wup-0.2.72}/tests/test_testql_watcher.py +0 -0
  28. {wup-0.2.71 → wup-0.2.72}/tests/test_visual_diff_periodic_skip.py +0 -0
  29. {wup-0.2.71 → wup-0.2.72}/tests/test_visual_diff_progress.py +0 -0
  30. {wup-0.2.71 → wup-0.2.72}/tests/test_watch_exclude.py +0 -0
  31. {wup-0.2.71 → wup-0.2.72}/tests/test_watch_no_paths_exit.py +0 -0
  32. {wup-0.2.71 → wup-0.2.72}/tests/test_web_client.py +0 -0
  33. {wup-0.2.71 → wup-0.2.72}/tests/test_wup.py +0 -0
  34. {wup-0.2.71 → wup-0.2.72}/tests/test_wup_generate.py +0 -0
  35. {wup-0.2.71 → wup-0.2.72}/wup/_ast_detector.py +0 -0
  36. {wup-0.2.71 → wup-0.2.72}/wup/_base_detector.py +0 -0
  37. {wup-0.2.71 → wup-0.2.72}/wup/_hash_detector.py +0 -0
  38. {wup-0.2.71 → wup-0.2.72}/wup/_yaml_detector.py +0 -0
  39. {wup-0.2.71 → wup-0.2.72}/wup/anomaly_detector.py +0 -0
  40. {wup-0.2.71 → wup-0.2.72}/wup/anomaly_models.py +0 -0
  41. {wup-0.2.71 → wup-0.2.72}/wup/assistant.py +0 -0
  42. {wup-0.2.71 → wup-0.2.72}/wup/assistant_discovery.py +0 -0
  43. {wup-0.2.71 → wup-0.2.72}/wup/assistant_validator.py +0 -0
  44. {wup-0.2.71 → wup-0.2.72}/wup/bus.py +0 -0
  45. {wup-0.2.71 → wup-0.2.72}/wup/cli.py +0 -0
  46. {wup-0.2.71 → wup-0.2.72}/wup/cli_bridge.py +0 -0
  47. {wup-0.2.71 → wup-0.2.72}/wup/cli_config_generator.py +0 -0
  48. {wup-0.2.71 → wup-0.2.72}/wup/cli_scanner.py +0 -0
  49. {wup-0.2.71 → wup-0.2.72}/wup/config.py +0 -0
  50. {wup-0.2.71 → wup-0.2.72}/wup/control.py +0 -0
  51. {wup-0.2.71 → wup-0.2.72}/wup/core.py +0 -0
  52. {wup-0.2.71 → wup-0.2.72}/wup/dependency_mapper.py +0 -0
  53. {wup-0.2.71 → wup-0.2.72}/wup/endpoints.py +0 -0
  54. {wup-0.2.71 → wup-0.2.72}/wup/event_store.py +0 -0
  55. {wup-0.2.71 → wup-0.2.72}/wup/file_watcher/events/file_events.py +0 -0
  56. {wup-0.2.71 → wup-0.2.72}/wup/generate.py +0 -0
  57. {wup-0.2.71 → wup-0.2.72}/wup/init_cli.py +0 -0
  58. {wup-0.2.71 → wup-0.2.72}/wup/models/__init__.py +0 -0
  59. {wup-0.2.71 → wup-0.2.72}/wup/models/config.py +0 -0
  60. {wup-0.2.71 → wup-0.2.72}/wup/models/target.py +0 -0
  61. {wup-0.2.71 → wup-0.2.72}/wup/monitoring_manifest.py +0 -0
  62. {wup-0.2.71 → wup-0.2.72}/wup/multi.py +0 -0
  63. {wup-0.2.71 → wup-0.2.72}/wup/paths.py +0 -0
  64. {wup-0.2.71 → wup-0.2.72}/wup/planfile_reporter.py +0 -0
  65. {wup-0.2.71 → wup-0.2.72}/wup/status_data.py +0 -0
  66. {wup-0.2.71 → wup-0.2.72}/wup/sync.py +0 -0
  67. {wup-0.2.71 → wup-0.2.72}/wup/testing/events/health_events.py +0 -0
  68. {wup-0.2.71 → wup-0.2.72}/wup/testing/events/test_results.py +0 -0
  69. {wup-0.2.71 → wup-0.2.72}/wup/testing/handlers/event_handlers.py +0 -0
  70. {wup-0.2.71 → wup-0.2.72}/wup/testing/handlers/health_handlers.py +0 -0
  71. {wup-0.2.71 → wup-0.2.72}/wup/testing/queries/health_queries.py +0 -0
  72. {wup-0.2.71 → wup-0.2.72}/wup/testql_cli_generator.py +0 -0
  73. {wup-0.2.71 → wup-0.2.72}/wup/testql_discovery.py +0 -0
  74. {wup-0.2.71 → wup-0.2.72}/wup/testql_monitor.py +0 -0
  75. {wup-0.2.71 → wup-0.2.72}/wup/testql_watcher.py +0 -0
  76. {wup-0.2.71 → wup-0.2.72}/wup/validate.py +0 -0
  77. {wup-0.2.71 → wup-0.2.72}/wup/visual_diff.py +0 -0
  78. {wup-0.2.71 → wup-0.2.72}/wup/web_client.py +0 -0
  79. {wup-0.2.71 → wup-0.2.72}/wup.egg-info/dependency_links.txt +0 -0
  80. {wup-0.2.71 → wup-0.2.72}/wup.egg-info/entry_points.txt +0 -0
  81. {wup-0.2.71 → wup-0.2.72}/wup.egg-info/requires.txt +0 -0
  82. {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.71
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.71-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.22-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
45
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.72-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.24-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
47
47
 
48
- - 🤖 **LLM usage:** $7.2227 (99 commits)
49
- - 👤 **Human dev:** ~$5215 (52.1h @ $100/h, 30min dedup)
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
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.71-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
55
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.72-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.71-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-$7.22-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.1h-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.72-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-$7.24-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.2227 (99 commits)
10
- - 👤 **Human dev:** ~$5215 (52.1h @ $100/h, 30min dedup)
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
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.71-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.72-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.2.71"
7
+ version = "0.2.72"
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.10"
@@ -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.71"
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.71
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.71-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.22-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
45
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.72-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.24-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-52.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
47
47
 
48
- - 🤖 **LLM usage:** $7.2227 (99 commits)
49
- - 👤 **Human dev:** ~$5215 (52.1h @ $100/h, 30min dedup)
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
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.71-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
55
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.72-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
56
56
 
57
57
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
58
58
 
@@ -45,6 +45,7 @@ wup/config.py
45
45
  wup/control.py
46
46
  wup/core.py
47
47
  wup/dependency_mapper.py
48
+ wup/discovery.py
48
49
  wup/endpoints.py
49
50
  wup/event_store.py
50
51
  wup/generate.py
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