wupbro 0.1.2__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.
wupbro-0.1.2/PKG-INFO ADDED
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: wupbro
3
+ Version: 0.1.2
4
+ Summary: WUP Browser Dashboard — FastAPI backend for WUP regression watcher
5
+ Author: WUP contributors
6
+ Author-email: Tom Sapletta <tom@sapletta.com>
7
+ License: Apache-2.0
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: fastapi>=0.110
11
+ Requires-Dist: uvicorn[standard]>=0.27
12
+ Requires-Dist: httpx>=0.27
13
+ Requires-Dist: pydantic>=2.0
14
+ Requires-Dist: jinja2>=3.1
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8; extra == "dev"
17
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
18
+ Requires-Dist: httpx>=0.27; extra == "dev"
19
+ Requires-Dist: goal>=2.1.0; extra == "dev"
20
+ Requires-Dist: costs>=0.1.20; extra == "dev"
21
+ Requires-Dist: pfix>=0.1.60; extra == "dev"
22
+
23
+ # wupbro
24
+
25
+ FastAPI backend + minimal HTML dashboard for the WUP regression watcher.
26
+
27
+ ## Architecture
28
+
29
+ ```
30
+ ┌──────────────────────┐ POST /events ┌──────────────────────┐
31
+ │ WUP agent (shell) │ ───────────────▶ │ wupbro (FastAPI) │
32
+ │ - file watcher │ │ - /events (sink) │
33
+ │ - testql runner │ │ - /drivers/* │
34
+ │ - visual diff │ │ - /dashboard (UI) │
35
+ └──────────────────────┘ └──────────────────────┘
36
+
37
+
38
+ ┌─────────────────┐
39
+ │ Browser UI │
40
+ │ (auto-refresh) │
41
+ └─────────────────┘
42
+ ```
43
+
44
+ ## Endpoints
45
+
46
+ | Path | Method | Purpose |
47
+ |-----------------------------------|--------|---------------------------------------------|
48
+ | `/events` | POST | Receive event from a WUP agent |
49
+ | `/events` | GET | List recent events (filter by type/service) |
50
+ | `/events/stats` | GET | Aggregate counts by type |
51
+ | `/events` | DELETE | Clear store (admin/debug) |
52
+ | `/drivers/dom-diff/capture` | POST | One-shot Playwright DOM snapshot + diff |
53
+ | `/drivers/browserless/screenshot` | POST | Proxy to a `browserless/chrome` container |
54
+ | `/drivers/anomaly/report` | POST | Record numeric anomaly as ANOMALY event |
55
+ | `/drivers/health` | GET | Driver capability discovery |
56
+ | `/`, `/dashboard` | GET | HTML dashboard |
57
+ | `/healthz` | GET | Liveness probe |
58
+ | `/openapi.json`, `/docs` | GET | OpenAPI spec + Swagger UI |
59
+
60
+ ## Event types
61
+
62
+ `REGRESSION`, `PASS`, `ANOMALY`, `VISUAL_DIFF`, `HEALTH_TRANSITION`.
63
+
64
+ Schema (Pydantic):
65
+
66
+ ```json
67
+ {
68
+ "type": "REGRESSION",
69
+ "service": "users-web",
70
+ "file": "app/users/routes.py",
71
+ "endpoint": "/api/users",
72
+ "status": "fail",
73
+ "stage": "quick",
74
+ "reason": "TestQL exit code 1",
75
+ "timestamp": 1730000000
76
+ }
77
+ ```
78
+
79
+ Extra fields are preserved via `model_config = {"extra": "allow"}`.
80
+
81
+ ## Run
82
+
83
+ ```bash
84
+ # Install
85
+ pip install -e wupbro/
86
+
87
+ # Dev server (auto-reload)
88
+ wupbro --reload --port 8000
89
+
90
+ # Or directly
91
+ uvicorn wupbro.main:app --host 0.0.0.0 --port 8000 --reload
92
+ ```
93
+
94
+ Dashboard: <http://localhost:8000/>
95
+
96
+ OpenAPI docs: <http://localhost:8000/docs>
97
+
98
+ ## Configure WUP agent to send events
99
+
100
+ In your `wup.yaml`:
101
+
102
+ ```yaml
103
+ web:
104
+ enabled: true
105
+ endpoint: "http://localhost:8000"
106
+ endpoint_env: "WUPBRO_ENDPOINT" # fallback if endpoint is empty
107
+ timeout_s: 2.0 # short — must not block watcher
108
+ api_key: "" # optional bearer token
109
+ ```
110
+
111
+ Or via environment variable:
112
+
113
+ ```bash
114
+ export WUPBRO_ENDPOINT=http://localhost:8000
115
+ wup watch . --mode testql
116
+ ```
117
+
118
+ The agent will POST events fire-and-forget on:
119
+
120
+ - service health transitions (up ↔ down)
121
+ - regressions (when TestQL fails)
122
+ - visual DOM diffs (when significant changes detected)
123
+
124
+ ## Browserless integration
125
+
126
+ ```bash
127
+ docker run -d --name browserless -p 3000:3000 browserless/chrome
128
+ export BROWSERLESS_URL=http://localhost:3000
129
+ wup-web
130
+ ```
131
+
132
+ Then:
133
+
134
+ ```bash
135
+ curl -X POST http://localhost:8000/drivers/browserless/screenshot \
136
+ -H "Content-Type: application/json" \
137
+ -d '{"url": "http://example.com", "full_page": true}'
138
+ ```
139
+
140
+ ## Storage
141
+
142
+ Events are kept in an in-memory ring buffer (capacity 1000) and persisted to `.wupbro/events.jsonl` for restart durability.
143
+
144
+ ## Tests
145
+
146
+ ```bash
147
+ PYTHONPATH=wupbro python3 -m pytest wupbro/tests/ -v
148
+ ```
149
+
150
+ 17 tests covering events router, drivers (anomaly, browserless, dom-diff, health), dashboard HTML, OpenAPI schema.
151
+
152
+ ## License
153
+
154
+ Licensed under Apache-2.0.
wupbro-0.1.2/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # wupbro
2
+
3
+ FastAPI backend + minimal HTML dashboard for the WUP regression watcher.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ ┌──────────────────────┐ POST /events ┌──────────────────────┐
9
+ │ WUP agent (shell) │ ───────────────▶ │ wupbro (FastAPI) │
10
+ │ - file watcher │ │ - /events (sink) │
11
+ │ - testql runner │ │ - /drivers/* │
12
+ │ - visual diff │ │ - /dashboard (UI) │
13
+ └──────────────────────┘ └──────────────────────┘
14
+
15
+
16
+ ┌─────────────────┐
17
+ │ Browser UI │
18
+ │ (auto-refresh) │
19
+ └─────────────────┘
20
+ ```
21
+
22
+ ## Endpoints
23
+
24
+ | Path | Method | Purpose |
25
+ |-----------------------------------|--------|---------------------------------------------|
26
+ | `/events` | POST | Receive event from a WUP agent |
27
+ | `/events` | GET | List recent events (filter by type/service) |
28
+ | `/events/stats` | GET | Aggregate counts by type |
29
+ | `/events` | DELETE | Clear store (admin/debug) |
30
+ | `/drivers/dom-diff/capture` | POST | One-shot Playwright DOM snapshot + diff |
31
+ | `/drivers/browserless/screenshot` | POST | Proxy to a `browserless/chrome` container |
32
+ | `/drivers/anomaly/report` | POST | Record numeric anomaly as ANOMALY event |
33
+ | `/drivers/health` | GET | Driver capability discovery |
34
+ | `/`, `/dashboard` | GET | HTML dashboard |
35
+ | `/healthz` | GET | Liveness probe |
36
+ | `/openapi.json`, `/docs` | GET | OpenAPI spec + Swagger UI |
37
+
38
+ ## Event types
39
+
40
+ `REGRESSION`, `PASS`, `ANOMALY`, `VISUAL_DIFF`, `HEALTH_TRANSITION`.
41
+
42
+ Schema (Pydantic):
43
+
44
+ ```json
45
+ {
46
+ "type": "REGRESSION",
47
+ "service": "users-web",
48
+ "file": "app/users/routes.py",
49
+ "endpoint": "/api/users",
50
+ "status": "fail",
51
+ "stage": "quick",
52
+ "reason": "TestQL exit code 1",
53
+ "timestamp": 1730000000
54
+ }
55
+ ```
56
+
57
+ Extra fields are preserved via `model_config = {"extra": "allow"}`.
58
+
59
+ ## Run
60
+
61
+ ```bash
62
+ # Install
63
+ pip install -e wupbro/
64
+
65
+ # Dev server (auto-reload)
66
+ wupbro --reload --port 8000
67
+
68
+ # Or directly
69
+ uvicorn wupbro.main:app --host 0.0.0.0 --port 8000 --reload
70
+ ```
71
+
72
+ Dashboard: <http://localhost:8000/>
73
+
74
+ OpenAPI docs: <http://localhost:8000/docs>
75
+
76
+ ## Configure WUP agent to send events
77
+
78
+ In your `wup.yaml`:
79
+
80
+ ```yaml
81
+ web:
82
+ enabled: true
83
+ endpoint: "http://localhost:8000"
84
+ endpoint_env: "WUPBRO_ENDPOINT" # fallback if endpoint is empty
85
+ timeout_s: 2.0 # short — must not block watcher
86
+ api_key: "" # optional bearer token
87
+ ```
88
+
89
+ Or via environment variable:
90
+
91
+ ```bash
92
+ export WUPBRO_ENDPOINT=http://localhost:8000
93
+ wup watch . --mode testql
94
+ ```
95
+
96
+ The agent will POST events fire-and-forget on:
97
+
98
+ - service health transitions (up ↔ down)
99
+ - regressions (when TestQL fails)
100
+ - visual DOM diffs (when significant changes detected)
101
+
102
+ ## Browserless integration
103
+
104
+ ```bash
105
+ docker run -d --name browserless -p 3000:3000 browserless/chrome
106
+ export BROWSERLESS_URL=http://localhost:3000
107
+ wup-web
108
+ ```
109
+
110
+ Then:
111
+
112
+ ```bash
113
+ curl -X POST http://localhost:8000/drivers/browserless/screenshot \
114
+ -H "Content-Type: application/json" \
115
+ -d '{"url": "http://example.com", "full_page": true}'
116
+ ```
117
+
118
+ ## Storage
119
+
120
+ Events are kept in an in-memory ring buffer (capacity 1000) and persisted to `.wupbro/events.jsonl` for restart durability.
121
+
122
+ ## Tests
123
+
124
+ ```bash
125
+ PYTHONPATH=wupbro python3 -m pytest wupbro/tests/ -v
126
+ ```
127
+
128
+ 17 tests covering events router, drivers (anomaly, browserless, dom-diff, health), dashboard HTML, OpenAPI schema.
129
+
130
+ ## License
131
+
132
+ Licensed under Apache-2.0.
@@ -0,0 +1,74 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wupbro"
7
+ version = "0.1.2"
8
+ description = "WUP Browser Dashboard — FastAPI backend for WUP regression watcher"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [{ name = "WUP contributors" }, {name = "Tom Sapletta",email = "tom@sapletta.com"}]
12
+ dependencies = [
13
+ "fastapi>=0.110",
14
+ "uvicorn[standard]>=0.27",
15
+ "httpx>=0.27",
16
+ "pydantic>=2.0",
17
+ "jinja2>=3.1",
18
+ ]
19
+
20
+ [project.license]
21
+ text = "Apache-2.0"
22
+
23
+ [project.optional-dependencies]
24
+ dev = [
25
+ "pytest>=8",
26
+ "pytest-asyncio>=0.23",
27
+ "httpx>=0.27",
28
+ "goal>=2.1.0",
29
+ "costs>=0.1.20",
30
+ "pfix>=0.1.60",
31
+ ]
32
+
33
+ [project.scripts]
34
+ wupbro = "wupbro.__main__:main"
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["."]
38
+ include = ["wupbro*"]
39
+
40
+ [tool.setuptools.package-data]
41
+ "wupbro" = ["templates/*.html", "static/*"]
42
+
43
+ [tool.pytest.ini_options]
44
+ asyncio_mode = "auto"
45
+ testpaths = ["tests"]
46
+
47
+ [tool.pfix]
48
+ # Self-healing Python configuration
49
+ model = "openrouter/qwen/qwen3-coder-next"
50
+ auto_apply = true
51
+ auto_install_deps = true
52
+ auto_restart = false
53
+ max_retries = 3
54
+ create_backups = false
55
+ git_auto_commit = false
56
+
57
+ [tool.pfix.runtime_todo]
58
+ enabled = true
59
+ todo_file = "TODO.md"
60
+ min_severity = "low"
61
+ deduplicate = true
62
+
63
+ [tool.costs]
64
+ # AI Cost tracking configuration
65
+ badge = true
66
+ update_readme = true
67
+ readme_path = "README.md"
68
+ default_model = "openrouter/qwen/qwen3-coder-next"
69
+ analysis_mode = "byok"
70
+ full_history = true
71
+ max_commits = 500
72
+
73
+ # Cost thresholds for badge colors (USD)
74
+ badge_color_thresholds = { low = 1.0, medium = 5.0, high = 10.0, critical = 50.0 }
wupbro-0.1.2/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,35 @@
1
+ """Tests for the dashboard HTML and meta endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def test_root_serves_dashboard(client):
7
+ resp = client.get("/")
8
+ assert resp.status_code == 200
9
+ assert resp.headers["content-type"].startswith("text/html")
10
+ assert "WUP Dashboard" in resp.text
11
+
12
+
13
+ def test_dashboard_alias(client):
14
+ resp = client.get("/dashboard")
15
+ assert resp.status_code == 200
16
+ assert "WUP Dashboard" in resp.text
17
+
18
+
19
+ def test_healthz(client):
20
+ resp = client.get("/healthz")
21
+ assert resp.status_code == 200
22
+ body = resp.json()
23
+ assert body["ok"] is True
24
+ assert "version" in body
25
+
26
+
27
+ def test_openapi_schema_includes_routes(client):
28
+ resp = client.get("/openapi.json")
29
+ assert resp.status_code == 200
30
+ spec = resp.json()
31
+ paths = spec["paths"]
32
+ assert "/events" in paths
33
+ assert "/events/stats" in paths
34
+ assert "/drivers/anomaly/report" in paths
35
+ assert "/drivers/health" in paths
@@ -0,0 +1,50 @@
1
+ """Tests for /drivers/* router."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def test_anomaly_report_creates_event(client):
7
+ resp = client.post("/drivers/anomaly/report", json={
8
+ "service": "billing",
9
+ "metric": "p95_latency",
10
+ "value": 1500.0,
11
+ "threshold": 1000.0,
12
+ })
13
+ assert resp.status_code == 201
14
+ assert resp.json()["accepted"] is True
15
+
16
+ events = client.get("/events").json()
17
+ assert events["total"] == 1
18
+ e = events["items"][0]
19
+ assert e["type"] == "ANOMALY"
20
+ assert e["service"] == "billing"
21
+ assert "p95_latency" in e["reason"]
22
+
23
+
24
+ def test_driver_health_reports_capabilities(client):
25
+ resp = client.get("/drivers/health")
26
+ assert resp.status_code == 200
27
+ caps = resp.json()
28
+ assert caps["events"] is True
29
+ assert caps["anomaly"] is True
30
+ assert "dom_diff" in caps # bool depending on wup install
31
+ assert "playwright" in caps # bool depending on playwright install
32
+
33
+
34
+ def test_browserless_unreachable_returns_503(client, monkeypatch):
35
+ monkeypatch.setenv("BROWSERLESS_URL", "http://127.0.0.1:1") # closed port
36
+ resp = client.post("/drivers/browserless/screenshot", json={"url": "http://example.com"})
37
+ assert resp.status_code == 503
38
+
39
+
40
+ def test_dom_diff_endpoint_exists(client):
41
+ """Endpoint accepts request payload (actual diff requires Playwright)."""
42
+ # Using a non-routable URL — VisualDiffer logs a warning and returns
43
+ # a result with status='error' but does NOT raise.
44
+ resp = client.post("/drivers/dom-diff/capture", json={
45
+ "url": "http://127.0.0.1:1/",
46
+ "service": "demo",
47
+ "max_depth": 3,
48
+ })
49
+ # Either 200 (gracefully degraded) or 503 (wup not installed). Both acceptable.
50
+ assert resp.status_code in (200, 503)
@@ -0,0 +1,95 @@
1
+ """Tests for /events router and EventStore."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def test_post_event_accepted(client):
7
+ resp = client.post("/events", json={
8
+ "type": "REGRESSION",
9
+ "service": "users",
10
+ "file": "app/users/routes.py",
11
+ "endpoint": "/api/users",
12
+ "status": "fail",
13
+ "reason": "boom",
14
+ })
15
+ assert resp.status_code == 201
16
+ body = resp.json()
17
+ assert body["accepted"] is True
18
+ assert body["type"] == "REGRESSION"
19
+
20
+
21
+ def test_post_event_invalid_type_rejected(client):
22
+ resp = client.post("/events", json={"type": "NOT_A_TYPE"})
23
+ assert resp.status_code == 422
24
+
25
+
26
+ def test_list_events_returns_recent(client):
27
+ for i in range(3):
28
+ client.post("/events", json={"type": "PASS", "service": f"svc-{i}"})
29
+ resp = client.get("/events")
30
+ assert resp.status_code == 200
31
+ body = resp.json()
32
+ assert body["total"] == 3
33
+ services = [e["service"] for e in body["items"]]
34
+ # newest first
35
+ assert services == ["svc-2", "svc-1", "svc-0"]
36
+
37
+
38
+ def test_list_events_filter_by_type(client):
39
+ client.post("/events", json={"type": "REGRESSION", "service": "a"})
40
+ client.post("/events", json={"type": "PASS", "service": "b"})
41
+ client.post("/events", json={"type": "REGRESSION", "service": "c"})
42
+
43
+ resp = client.get("/events?type=REGRESSION")
44
+ assert resp.status_code == 200
45
+ body = resp.json()
46
+ assert body["total"] == 2
47
+ assert all(e["type"] == "REGRESSION" for e in body["items"])
48
+
49
+
50
+ def test_list_events_filter_by_service(client):
51
+ client.post("/events", json={"type": "PASS", "service": "users"})
52
+ client.post("/events", json={"type": "PASS", "service": "billing"})
53
+ resp = client.get("/events?service=users")
54
+ body = resp.json()
55
+ assert body["total"] == 1
56
+ assert body["items"][0]["service"] == "users"
57
+
58
+
59
+ def test_list_events_limit(client):
60
+ for i in range(20):
61
+ client.post("/events", json={"type": "PASS", "service": f"s{i}"})
62
+ resp = client.get("/events?limit=5")
63
+ body = resp.json()
64
+ assert body["total"] == 5
65
+
66
+
67
+ def test_event_stats(client):
68
+ client.post("/events", json={"type": "REGRESSION", "service": "x"})
69
+ client.post("/events", json={"type": "PASS", "service": "x"})
70
+ client.post("/events", json={"type": "PASS", "service": "y"})
71
+ resp = client.get("/events/stats")
72
+ assert resp.status_code == 200
73
+ stats = resp.json()
74
+ assert stats["total"] == 3
75
+ assert stats["by_type"]["REGRESSION"] == 1
76
+ assert stats["by_type"]["PASS"] == 2
77
+
78
+
79
+ def test_clear_events(client):
80
+ client.post("/events", json={"type": "PASS", "service": "x"})
81
+ resp = client.delete("/events")
82
+ assert resp.status_code == 204
83
+ body = client.get("/events").json()
84
+ assert body["total"] == 0
85
+
86
+
87
+ def test_event_extra_fields_preserved(client):
88
+ client.post("/events", json={
89
+ "type": "VISUAL_DIFF",
90
+ "service": "users",
91
+ "url": "http://localhost/users",
92
+ "diff": {"status": "changed", "counts": {"added": 5}},
93
+ })
94
+ body = client.get("/events").json()
95
+ assert body["items"][0]["diff"]["counts"]["added"] == 5
@@ -0,0 +1,7 @@
1
+ """WUP Browser Dashboard — FastAPI backend for WUP regression watcher."""
2
+
3
+ __version__ = "0.1.2"
4
+
5
+ from .main import create_app, app
6
+
7
+ __all__ = ["create_app", "app", "__version__"]
@@ -0,0 +1,21 @@
1
+ """CLI entry-point: `python -m wupbro` or `wupbro`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+
8
+
9
+ def main() -> None:
10
+ parser = argparse.ArgumentParser(prog="wupbro", description="WUP Browser Dashboard")
11
+ parser.add_argument("--host", default=os.environ.get("WUPBRO_HOST", "0.0.0.0"))
12
+ parser.add_argument("--port", type=int, default=int(os.environ.get("WUPBRO_PORT", "8000")))
13
+ parser.add_argument("--reload", action="store_true", help="auto-reload (dev)")
14
+ args = parser.parse_args()
15
+
16
+ import uvicorn
17
+ uvicorn.run("wupbro.main:app", host=args.host, port=args.port, reload=args.reload)
18
+
19
+
20
+ if __name__ == "__main__":
21
+ main()
@@ -0,0 +1,44 @@
1
+ """FastAPI entry-point for wupbro."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import FastAPI
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+
8
+ from . import __version__
9
+ from .routers import dashboard, drivers, events
10
+
11
+
12
+ def create_app() -> FastAPI:
13
+ app = FastAPI(
14
+ title="WUP Browser Dashboard",
15
+ version=__version__,
16
+ description=(
17
+ "Backend for the WUP regression watcher. Receives events from "
18
+ "WUP agents (REGRESSION, PASS, ANOMALY, VISUAL_DIFF, HEALTH_TRANSITION), "
19
+ "exposes drivers (DOM diff, browserless, anomaly), and serves a dashboard."
20
+ ),
21
+ )
22
+
23
+ # Permissive CORS — dashboard usually served from same origin, but agents
24
+ # may live elsewhere. Tighten in production.
25
+ app.add_middleware(
26
+ CORSMiddleware,
27
+ allow_origins=["*"],
28
+ allow_methods=["*"],
29
+ allow_headers=["*"],
30
+ )
31
+
32
+ app.include_router(events.router)
33
+ app.include_router(drivers.router)
34
+ app.include_router(dashboard.router)
35
+
36
+ @app.get("/healthz", tags=["meta"])
37
+ async def healthz():
38
+ return {"ok": True, "version": __version__}
39
+
40
+ return app
41
+
42
+
43
+ # Module-level instance for `uvicorn wupbro.main:app`
44
+ app = create_app()
@@ -0,0 +1,59 @@
1
+ """Pydantic models for wupbro events and driver requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any, Dict, List, Literal, Optional
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ EventType = Literal[
12
+ "REGRESSION",
13
+ "PASS",
14
+ "ANOMALY",
15
+ "VISUAL_DIFF",
16
+ "HEALTH_TRANSITION",
17
+ ]
18
+
19
+
20
+ class Event(BaseModel):
21
+ """Generic WUP event posted by an agent."""
22
+
23
+ type: EventType
24
+ service: Optional[str] = None
25
+ file: Optional[str] = None
26
+ endpoint: Optional[str] = None
27
+ url: Optional[str] = None
28
+ status: Optional[str] = None
29
+ stage: Optional[str] = None
30
+ reason: Optional[str] = None
31
+ diff: Optional[Dict[str, Any]] = None
32
+ timestamp: int = Field(default_factory=lambda: int(time.time()))
33
+
34
+ # Forward-compat: allow extra keys to land in `extra`
35
+ model_config = {"extra": "allow"}
36
+
37
+
38
+ class EventList(BaseModel):
39
+ items: List[Event]
40
+ total: int
41
+
42
+
43
+ class DomDiffRequest(BaseModel):
44
+ url: str
45
+ service: str = "default"
46
+ max_depth: int = 10
47
+
48
+
49
+ class ScreenshotRequest(BaseModel):
50
+ url: str
51
+ full_page: bool = True
52
+
53
+
54
+ class AnomalyReport(BaseModel):
55
+ service: str
56
+ metric: str
57
+ value: float
58
+ threshold: float
59
+ timestamp: int = Field(default_factory=lambda: int(time.time()))
@@ -0,0 +1 @@
1
+ """Routers for wupbro."""