smello-server 0.1.1__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.
- smello_server-0.1.1/.bumpversion.toml +16 -0
- smello_server-0.1.1/.gitignore +23 -0
- smello_server-0.1.1/PKG-INFO +89 -0
- smello_server-0.1.1/README.md +64 -0
- smello_server-0.1.1/pyproject.toml +40 -0
- smello_server-0.1.1/src/smello_server/__init__.py +3 -0
- smello_server-0.1.1/src/smello_server/__main__.py +50 -0
- smello_server-0.1.1/src/smello_server/app.py +47 -0
- smello_server-0.1.1/src/smello_server/models.py +31 -0
- smello_server-0.1.1/src/smello_server/routes/__init__.py +0 -0
- smello_server-0.1.1/src/smello_server/routes/api.py +160 -0
- smello_server-0.1.1/src/smello_server/routes/web.py +82 -0
- smello_server-0.1.1/src/smello_server/static/json-viewer.js +87 -0
- smello_server-0.1.1/src/smello_server/static/style.css +251 -0
- smello_server-0.1.1/src/smello_server/templates/base.html +28 -0
- smello_server-0.1.1/src/smello_server/templates/partials/request_detail_partial.html +73 -0
- smello_server-0.1.1/src/smello_server/templates/partials/request_list_items.html +23 -0
- smello_server-0.1.1/src/smello_server/templates/request_detail.html +22 -0
- smello_server-0.1.1/src/smello_server/templates/request_list.html +91 -0
- smello_server-0.1.1/tests/conftest.py +85 -0
- smello_server-0.1.1/tests/test_api.py +122 -0
- smello_server-0.1.1/tests/test_web.py +79 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[tool.bumpversion]
|
|
2
|
+
current_version = "0.1.1"
|
|
3
|
+
commit = true
|
|
4
|
+
tag = true
|
|
5
|
+
tag_name = "smello-server/v{new_version}"
|
|
6
|
+
tag_message = "Release smello-server v{new_version}"
|
|
7
|
+
message = "Bump smello-server version: {current_version} → {new_version}"
|
|
8
|
+
|
|
9
|
+
[[tool.bumpversion.files]]
|
|
10
|
+
filename = "server/pyproject.toml"
|
|
11
|
+
key_path = "project.version"
|
|
12
|
+
|
|
13
|
+
[[tool.bumpversion.files]]
|
|
14
|
+
filename = "server/src/smello_server/__init__.py"
|
|
15
|
+
search = '__version__ = "{current_version}"'
|
|
16
|
+
replace = '__version__ = "{new_version}"'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg
|
|
8
|
+
.eggs/
|
|
9
|
+
*.so
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
.env
|
|
14
|
+
*.db
|
|
15
|
+
*.sqlite3
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
.coverage
|
|
20
|
+
htmlcov/
|
|
21
|
+
*.log
|
|
22
|
+
.DS_Store
|
|
23
|
+
Thumbs.db
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smello-server
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A local web dashboard for inspecting outgoing HTTP requests from your code
|
|
5
|
+
Project-URL: Homepage, https://github.com/smelloscope/smello
|
|
6
|
+
Project-URL: Repository, https://github.com/smelloscope/smello
|
|
7
|
+
Project-URL: Issues, https://github.com/smelloscope/smello/issues
|
|
8
|
+
Author-email: Roman Imankulov <roman.imankulov@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: FastAPI
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
17
|
+
Classifier: Topic :: System :: Networking :: Monitoring
|
|
18
|
+
Requires-Python: >=3.14
|
|
19
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
20
|
+
Requires-Dist: fastapi>=0.115.0
|
|
21
|
+
Requires-Dist: jinja2>=3.1.0
|
|
22
|
+
Requires-Dist: tortoise-orm>=0.22.0
|
|
23
|
+
Requires-Dist: uvicorn[standard]>=0.34.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Smello Server
|
|
27
|
+
|
|
28
|
+
A local web dashboard for inspecting outgoing HTTP requests captured by the [smello](https://pypi.org/project/smello/) client SDK.
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install smello-server
|
|
34
|
+
smello-server run
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The dashboard opens at `http://localhost:5110`.
|
|
38
|
+
|
|
39
|
+
Or with Docker:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
docker run -p 5110:5110 ghcr.io/smelloscope/smello
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then add the client SDK to your Python code:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install smello
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import smello
|
|
53
|
+
smello.init()
|
|
54
|
+
|
|
55
|
+
# All outgoing requests via requests/httpx are now captured
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
Smello Server provides a JSON API for exploring captured requests from the command line.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# List all captured requests
|
|
64
|
+
curl -s http://localhost:5110/api/requests | python -m json.tool
|
|
65
|
+
|
|
66
|
+
# Filter by method, host, status, or URL substring
|
|
67
|
+
curl -s 'http://localhost:5110/api/requests?method=POST&host=api.stripe.com'
|
|
68
|
+
|
|
69
|
+
# Get full request/response details
|
|
70
|
+
curl -s http://localhost:5110/api/requests/{id} | python -m json.tool
|
|
71
|
+
|
|
72
|
+
# Clear all requests
|
|
73
|
+
curl -X DELETE http://localhost:5110/api/requests
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## CLI Options
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
smello-server run --host 0.0.0.0 --port 5110 --db-path /tmp/smello.db
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Requires
|
|
83
|
+
|
|
84
|
+
- Python >= 3.14
|
|
85
|
+
|
|
86
|
+
## Links
|
|
87
|
+
|
|
88
|
+
- [Documentation & Source](https://github.com/smelloscope/smello)
|
|
89
|
+
- [smello client SDK on PyPI](https://pypi.org/project/smello/)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Smello Server
|
|
2
|
+
|
|
3
|
+
A local web dashboard for inspecting outgoing HTTP requests captured by the [smello](https://pypi.org/project/smello/) client SDK.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install smello-server
|
|
9
|
+
smello-server run
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The dashboard opens at `http://localhost:5110`.
|
|
13
|
+
|
|
14
|
+
Or with Docker:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
docker run -p 5110:5110 ghcr.io/smelloscope/smello
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then add the client SDK to your Python code:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install smello
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import smello
|
|
28
|
+
smello.init()
|
|
29
|
+
|
|
30
|
+
# All outgoing requests via requests/httpx are now captured
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
Smello Server provides a JSON API for exploring captured requests from the command line.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# List all captured requests
|
|
39
|
+
curl -s http://localhost:5110/api/requests | python -m json.tool
|
|
40
|
+
|
|
41
|
+
# Filter by method, host, status, or URL substring
|
|
42
|
+
curl -s 'http://localhost:5110/api/requests?method=POST&host=api.stripe.com'
|
|
43
|
+
|
|
44
|
+
# Get full request/response details
|
|
45
|
+
curl -s http://localhost:5110/api/requests/{id} | python -m json.tool
|
|
46
|
+
|
|
47
|
+
# Clear all requests
|
|
48
|
+
curl -X DELETE http://localhost:5110/api/requests
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## CLI Options
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
smello-server run --host 0.0.0.0 --port 5110 --db-path /tmp/smello.db
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Requires
|
|
58
|
+
|
|
59
|
+
- Python >= 3.14
|
|
60
|
+
|
|
61
|
+
## Links
|
|
62
|
+
|
|
63
|
+
- [Documentation & Source](https://github.com/smelloscope/smello)
|
|
64
|
+
- [smello client SDK on PyPI](https://pypi.org/project/smello/)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "smello-server"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "A local web dashboard for inspecting outgoing HTTP requests from your code"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
authors = [{ name = "Roman Imankulov", email = "roman.imankulov@gmail.com" }]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.14",
|
|
15
|
+
"Framework :: FastAPI",
|
|
16
|
+
"Topic :: Software Development :: Testing",
|
|
17
|
+
"Topic :: System :: Networking :: Monitoring",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"fastapi>=0.115.0",
|
|
21
|
+
"uvicorn[standard]>=0.34.0",
|
|
22
|
+
"jinja2>=3.1.0",
|
|
23
|
+
"tortoise-orm>=0.22.0",
|
|
24
|
+
"aiosqlite>=0.20.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/smelloscope/smello"
|
|
29
|
+
Repository = "https://github.com/smelloscope/smello"
|
|
30
|
+
Issues = "https://github.com/smelloscope/smello/issues"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
smello-server = "smello_server.__main__:main"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/smello_server"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""CLI entry point: `smello-server run` or `python -m smello_server`."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import uvicorn
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
prog="smello-server", description="Smello HTTP request inspector"
|
|
12
|
+
)
|
|
13
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
14
|
+
|
|
15
|
+
run_parser = subparsers.add_parser("run", help="Start the Smello server")
|
|
16
|
+
run_parser.add_argument(
|
|
17
|
+
"--host", default="0.0.0.0", help="Host to bind to (default: 0.0.0.0)"
|
|
18
|
+
)
|
|
19
|
+
run_parser.add_argument(
|
|
20
|
+
"--port", type=int, default=5110, help="Port to bind to (default: 5110)"
|
|
21
|
+
)
|
|
22
|
+
run_parser.add_argument(
|
|
23
|
+
"--db-path", default=None, help="Path to SQLite database file"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
args = parser.parse_args()
|
|
27
|
+
|
|
28
|
+
if args.command is None:
|
|
29
|
+
args.command = "run"
|
|
30
|
+
args.host = "0.0.0.0"
|
|
31
|
+
args.port = 5110
|
|
32
|
+
args.db_path = None
|
|
33
|
+
|
|
34
|
+
if args.command == "run":
|
|
35
|
+
if args.db_path:
|
|
36
|
+
os.environ["SMELLO_DB_PATH"] = args.db_path
|
|
37
|
+
|
|
38
|
+
from smello_server.app import create_app
|
|
39
|
+
|
|
40
|
+
app = create_app()
|
|
41
|
+
uvicorn.run(
|
|
42
|
+
app,
|
|
43
|
+
host=args.host,
|
|
44
|
+
port=args.port,
|
|
45
|
+
log_level="info",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
main()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""FastAPI application setup with Tortoise ORM."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
from fastapi.staticfiles import StaticFiles
|
|
8
|
+
from fastapi.templating import Jinja2Templates
|
|
9
|
+
from tortoise.contrib.fastapi import register_tortoise
|
|
10
|
+
|
|
11
|
+
from smello_server.routes.api import router as api_router
|
|
12
|
+
from smello_server.routes.web import router as web_router
|
|
13
|
+
|
|
14
|
+
PACKAGE_DIR = Path(__file__).parent
|
|
15
|
+
TEMPLATES_DIR = PACKAGE_DIR / "templates"
|
|
16
|
+
STATIC_DIR = PACKAGE_DIR / "static"
|
|
17
|
+
|
|
18
|
+
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_db_url() -> str:
|
|
22
|
+
db_path = os.environ.get("SMELLO_DB_PATH")
|
|
23
|
+
if db_path:
|
|
24
|
+
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
return f"sqlite://{db_path}"
|
|
26
|
+
default_dir = Path.home() / ".smello"
|
|
27
|
+
default_dir.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
return f"sqlite://{default_dir / 'smello.db'}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def create_app(db_url: str | None = None) -> FastAPI:
|
|
32
|
+
"""Create and configure the FastAPI application."""
|
|
33
|
+
application = FastAPI(title="Smello")
|
|
34
|
+
|
|
35
|
+
application.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|
36
|
+
application.include_router(api_router)
|
|
37
|
+
application.include_router(web_router)
|
|
38
|
+
|
|
39
|
+
register_tortoise(
|
|
40
|
+
application,
|
|
41
|
+
db_url=db_url or _get_db_url(),
|
|
42
|
+
modules={"models": ["smello_server.models"]},
|
|
43
|
+
generate_schemas=True,
|
|
44
|
+
add_exception_handlers=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return application
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Tortoise ORM models for captured HTTP requests."""
|
|
2
|
+
|
|
3
|
+
from tortoise import fields
|
|
4
|
+
from tortoise.models import Model
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CapturedRequest(Model):
|
|
8
|
+
id = fields.UUIDField(pk=True)
|
|
9
|
+
timestamp = fields.DatetimeField(auto_now_add=True)
|
|
10
|
+
duration_ms = fields.IntField()
|
|
11
|
+
|
|
12
|
+
# Request
|
|
13
|
+
method = fields.CharField(max_length=10)
|
|
14
|
+
url = fields.TextField()
|
|
15
|
+
request_headers: dict = fields.JSONField()
|
|
16
|
+
request_body = fields.TextField(null=True)
|
|
17
|
+
request_body_size = fields.IntField(default=0)
|
|
18
|
+
|
|
19
|
+
# Response
|
|
20
|
+
status_code = fields.IntField()
|
|
21
|
+
response_headers: dict = fields.JSONField()
|
|
22
|
+
response_body = fields.TextField(null=True)
|
|
23
|
+
response_body_size = fields.IntField(default=0)
|
|
24
|
+
|
|
25
|
+
# Meta
|
|
26
|
+
host = fields.CharField(max_length=255, index=True)
|
|
27
|
+
library = fields.CharField(max_length=50)
|
|
28
|
+
|
|
29
|
+
class Meta:
|
|
30
|
+
table = "captured_requests"
|
|
31
|
+
ordering = ["-timestamp"]
|
|
File without changes
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""API routes: ingestion endpoint and JSON API."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from smello_server.models import CapturedRequest
|
|
11
|
+
|
|
12
|
+
router = APIRouter(prefix="/api")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# --- Input models ---
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RequestData(BaseModel):
|
|
19
|
+
method: str
|
|
20
|
+
url: str
|
|
21
|
+
headers: dict[str, str]
|
|
22
|
+
body: str | None = None
|
|
23
|
+
body_size: int = 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ResponseData(BaseModel):
|
|
27
|
+
status_code: int
|
|
28
|
+
headers: dict[str, str]
|
|
29
|
+
body: str | None = None
|
|
30
|
+
body_size: int = 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MetaData(BaseModel):
|
|
34
|
+
library: str = "unknown"
|
|
35
|
+
python_version: str = ""
|
|
36
|
+
smello_version: str = ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CapturePayload(BaseModel):
|
|
40
|
+
id: str | None = None
|
|
41
|
+
timestamp: str | None = None
|
|
42
|
+
duration_ms: int = 0
|
|
43
|
+
request: RequestData
|
|
44
|
+
response: ResponseData
|
|
45
|
+
meta: MetaData = MetaData()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --- Output models ---
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CaptureResponse(BaseModel):
|
|
52
|
+
status: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RequestSummary(BaseModel):
|
|
56
|
+
id: str
|
|
57
|
+
timestamp: datetime
|
|
58
|
+
method: str
|
|
59
|
+
url: str
|
|
60
|
+
host: str
|
|
61
|
+
status_code: int
|
|
62
|
+
duration_ms: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class RequestDetail(RequestSummary):
|
|
66
|
+
library: str
|
|
67
|
+
request_headers: dict[str, str]
|
|
68
|
+
request_body: str | None
|
|
69
|
+
request_body_size: int
|
|
70
|
+
response_headers: dict[str, str]
|
|
71
|
+
response_body: str | None
|
|
72
|
+
response_body_size: int
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --- Routes ---
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@router.post("/capture", status_code=201, response_model=CaptureResponse)
|
|
79
|
+
async def capture(payload: CapturePayload) -> CaptureResponse:
|
|
80
|
+
host = urlparse(payload.request.url).hostname or "unknown"
|
|
81
|
+
|
|
82
|
+
await CapturedRequest.create(
|
|
83
|
+
id=payload.id or uuid.uuid4(),
|
|
84
|
+
duration_ms=payload.duration_ms,
|
|
85
|
+
method=payload.request.method.upper(),
|
|
86
|
+
url=payload.request.url,
|
|
87
|
+
request_headers=payload.request.headers,
|
|
88
|
+
request_body=payload.request.body,
|
|
89
|
+
request_body_size=payload.request.body_size,
|
|
90
|
+
status_code=payload.response.status_code,
|
|
91
|
+
response_headers=payload.response.headers,
|
|
92
|
+
response_body=payload.response.body,
|
|
93
|
+
response_body_size=payload.response.body_size,
|
|
94
|
+
host=host,
|
|
95
|
+
library=payload.meta.library,
|
|
96
|
+
)
|
|
97
|
+
return CaptureResponse(status="ok")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@router.get("/requests", response_model=list[RequestSummary])
|
|
101
|
+
async def list_requests(
|
|
102
|
+
host: str | None = Query(None),
|
|
103
|
+
method: str | None = Query(None),
|
|
104
|
+
status: int | None = Query(None),
|
|
105
|
+
search: str | None = Query(None),
|
|
106
|
+
limit: int = Query(50, le=200),
|
|
107
|
+
) -> list[RequestSummary]:
|
|
108
|
+
qs = CapturedRequest.all()
|
|
109
|
+
if host:
|
|
110
|
+
qs = qs.filter(host=host)
|
|
111
|
+
if method:
|
|
112
|
+
qs = qs.filter(method=method.upper())
|
|
113
|
+
if status:
|
|
114
|
+
qs = qs.filter(status_code=status)
|
|
115
|
+
if search:
|
|
116
|
+
qs = qs.filter(url__icontains=search)
|
|
117
|
+
|
|
118
|
+
requests = await qs.limit(limit)
|
|
119
|
+
return [
|
|
120
|
+
RequestSummary(
|
|
121
|
+
id=str(r.id),
|
|
122
|
+
timestamp=r.timestamp,
|
|
123
|
+
method=r.method,
|
|
124
|
+
url=r.url,
|
|
125
|
+
host=r.host,
|
|
126
|
+
status_code=r.status_code,
|
|
127
|
+
duration_ms=r.duration_ms,
|
|
128
|
+
)
|
|
129
|
+
for r in requests
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@router.get("/requests/{request_id}", response_model=RequestDetail)
|
|
134
|
+
async def get_request(request_id: str) -> RequestDetail:
|
|
135
|
+
try:
|
|
136
|
+
r = await CapturedRequest.get(id=request_id)
|
|
137
|
+
except Exception:
|
|
138
|
+
raise HTTPException(status_code=404, detail="Request not found")
|
|
139
|
+
|
|
140
|
+
return RequestDetail(
|
|
141
|
+
id=str(r.id),
|
|
142
|
+
timestamp=r.timestamp,
|
|
143
|
+
method=r.method,
|
|
144
|
+
url=r.url,
|
|
145
|
+
host=r.host,
|
|
146
|
+
status_code=r.status_code,
|
|
147
|
+
duration_ms=r.duration_ms,
|
|
148
|
+
library=r.library,
|
|
149
|
+
request_headers=r.request_headers,
|
|
150
|
+
request_body=r.request_body,
|
|
151
|
+
request_body_size=r.request_body_size,
|
|
152
|
+
response_headers=r.response_headers,
|
|
153
|
+
response_body=r.response_body,
|
|
154
|
+
response_body_size=r.response_body_size,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@router.delete("/requests", status_code=204)
|
|
159
|
+
async def clear_requests() -> None:
|
|
160
|
+
await CapturedRequest.all().delete()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Web UI routes: request list and detail pages."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Query, Request
|
|
4
|
+
from fastapi.responses import HTMLResponse
|
|
5
|
+
|
|
6
|
+
from smello_server.models import CapturedRequest
|
|
7
|
+
|
|
8
|
+
router = APIRouter(include_in_schema=False)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@router.get("/", response_class=HTMLResponse)
|
|
12
|
+
async def request_list(
|
|
13
|
+
request: Request,
|
|
14
|
+
host: str | None = Query(None),
|
|
15
|
+
method: str | None = Query(None),
|
|
16
|
+
status: int | None = Query(None),
|
|
17
|
+
search: str | None = Query(None),
|
|
18
|
+
_partial: str | None = Query(None),
|
|
19
|
+
):
|
|
20
|
+
from smello_server.app import templates
|
|
21
|
+
|
|
22
|
+
qs = CapturedRequest.all()
|
|
23
|
+
if host:
|
|
24
|
+
qs = qs.filter(host=host)
|
|
25
|
+
if method:
|
|
26
|
+
qs = qs.filter(method=method.upper())
|
|
27
|
+
if status:
|
|
28
|
+
qs = qs.filter(status_code=status)
|
|
29
|
+
if search:
|
|
30
|
+
qs = qs.filter(url__icontains=search)
|
|
31
|
+
|
|
32
|
+
requests_list = await qs.limit(100)
|
|
33
|
+
|
|
34
|
+
hosts = await CapturedRequest.all().distinct().values_list("host", flat=True)
|
|
35
|
+
methods = await CapturedRequest.all().distinct().values_list("method", flat=True)
|
|
36
|
+
|
|
37
|
+
context = {
|
|
38
|
+
"request": request,
|
|
39
|
+
"requests": requests_list,
|
|
40
|
+
"hosts": sorted(set(hosts)),
|
|
41
|
+
"methods": sorted(set(methods)),
|
|
42
|
+
"filter_host": host or "",
|
|
43
|
+
"filter_method": method or "",
|
|
44
|
+
"filter_status": status or "",
|
|
45
|
+
"filter_search": search or "",
|
|
46
|
+
"selected_id": "",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if _partial == "list":
|
|
50
|
+
return templates.TemplateResponse("partials/request_list_items.html", context)
|
|
51
|
+
|
|
52
|
+
return templates.TemplateResponse("request_list.html", context)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.get("/requests/{request_id}", response_class=HTMLResponse)
|
|
56
|
+
async def request_detail(request: Request, request_id: str):
|
|
57
|
+
from smello_server.app import templates
|
|
58
|
+
|
|
59
|
+
captured = await CapturedRequest.get(id=request_id)
|
|
60
|
+
|
|
61
|
+
return templates.TemplateResponse(
|
|
62
|
+
"request_detail.html",
|
|
63
|
+
{
|
|
64
|
+
"request": request,
|
|
65
|
+
"captured": captured,
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.get("/requests/{request_id}/partial", response_class=HTMLResponse)
|
|
71
|
+
async def request_detail_partial(request: Request, request_id: str):
|
|
72
|
+
from smello_server.app import templates
|
|
73
|
+
|
|
74
|
+
captured = await CapturedRequest.get(id=request_id)
|
|
75
|
+
|
|
76
|
+
return templates.TemplateResponse(
|
|
77
|
+
"partials/request_detail_partial.html",
|
|
78
|
+
{
|
|
79
|
+
"request": request,
|
|
80
|
+
"captured": captured,
|
|
81
|
+
},
|
|
82
|
+
)
|