mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Add local preview fixture manifests
This commit is contained in:
@@ -103,6 +103,8 @@ python3 -m ampg --config examples/wownero.gateway.toml plan
|
|||||||
python3 -m ampg --config examples/wownero.gateway.toml plan --write-artifacts
|
python3 -m ampg --config examples/wownero.gateway.toml plan --write-artifacts
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml build
|
python3 -m ampg --config examples/wownero.gateway.toml build
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml manifest
|
python3 -m ampg --config examples/wownero.gateway.toml manifest
|
||||||
|
python3 -m ampg --config examples/wownero.gateway.toml preview endpoints
|
||||||
|
python3 -m ampg --config examples/wownero.gateway.toml preview manifest
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml routes explain
|
python3 -m ampg --config examples/wownero.gateway.toml routes explain
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml routes validate
|
python3 -m ampg --config examples/wownero.gateway.toml routes validate
|
||||||
python3 tools/generate_route_manifest.py examples/route-catalog.json examples/route-manifest.json
|
python3 tools/generate_route_manifest.py examples/route-catalog.json examples/route-manifest.json
|
||||||
|
|||||||
+74
@@ -10,6 +10,7 @@ from .config import load_config
|
|||||||
from .docsgen import generate_docs
|
from .docsgen import generate_docs
|
||||||
from .manifest import write_fixture_manifests
|
from .manifest import write_fixture_manifests
|
||||||
from .plan import plan_gateway, write_plan_artifacts
|
from .plan import plan_gateway, write_plan_artifacts
|
||||||
|
from .preview import PreviewServers, preview_endpoints, write_preview_fixture_manifests
|
||||||
from .route_manifest import (
|
from .route_manifest import (
|
||||||
load_route_manifest,
|
load_route_manifest,
|
||||||
route_manifest_schema_json,
|
route_manifest_schema_json,
|
||||||
@@ -35,6 +36,24 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
)
|
)
|
||||||
subcommands.add_parser("build", help="Build enabled protocol outputs.")
|
subcommands.add_parser("build", help="Build enabled protocol outputs.")
|
||||||
subcommands.add_parser("manifest", help="Write AMPB fixture manifests.")
|
subcommands.add_parser("manifest", help="Write AMPB fixture manifests.")
|
||||||
|
preview_parser = subcommands.add_parser("preview", help="Preview generated outputs locally.")
|
||||||
|
preview_subcommands = preview_parser.add_subparsers(dest="preview_command", required=True)
|
||||||
|
for preview_command in ("endpoints", "manifest", "serve"):
|
||||||
|
command_parser = preview_subcommands.add_parser(
|
||||||
|
preview_command,
|
||||||
|
help=f"Preview {preview_command} for generated outputs.",
|
||||||
|
)
|
||||||
|
command_parser.add_argument(
|
||||||
|
"--base-port",
|
||||||
|
type=int,
|
||||||
|
default=19080,
|
||||||
|
help="First loopback port assigned to enabled protocol outputs.",
|
||||||
|
)
|
||||||
|
command_parser.add_argument(
|
||||||
|
"--host",
|
||||||
|
default="127.0.0.1",
|
||||||
|
help="Loopback host for preview URLs.",
|
||||||
|
)
|
||||||
routes_parser = subcommands.add_parser("routes", help="Explain route exposure policy.")
|
routes_parser = subcommands.add_parser("routes", help="Explain route exposure policy.")
|
||||||
routes_subcommands = routes_parser.add_subparsers(dest="routes_command", required=True)
|
routes_subcommands = routes_parser.add_subparsers(dest="routes_command", required=True)
|
||||||
routes_subcommands.add_parser("explain", help="Print per-protocol route decisions.")
|
routes_subcommands.add_parser("explain", help="Print per-protocol route decisions.")
|
||||||
@@ -93,6 +112,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
return _cmd_build(config)
|
return _cmd_build(config)
|
||||||
if args.command == "manifest":
|
if args.command == "manifest":
|
||||||
return _cmd_manifest(config)
|
return _cmd_manifest(config)
|
||||||
|
if args.command == "preview":
|
||||||
|
return _cmd_preview(config, args)
|
||||||
if args.command == "routes":
|
if args.command == "routes":
|
||||||
return _cmd_routes(config, args)
|
return _cmd_routes(config, args)
|
||||||
if args.command == "audit":
|
if args.command == "audit":
|
||||||
@@ -166,6 +187,59 @@ def _cmd_manifest(config) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_preview(config, args) -> int:
|
||||||
|
endpoints = preview_endpoints(config, base_port=args.base_port, host=args.host)
|
||||||
|
if args.preview_command == "endpoints":
|
||||||
|
for endpoint in endpoints:
|
||||||
|
_print_preview_endpoint(endpoint)
|
||||||
|
print(
|
||||||
|
"AMPG_PREVIEW_SUMMARY "
|
||||||
|
f"sites={len(config.sites)} endpoints={len(endpoints)}"
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.preview_command == "manifest":
|
||||||
|
for endpoint in endpoints:
|
||||||
|
_print_preview_endpoint(endpoint)
|
||||||
|
for result in write_preview_fixture_manifests(
|
||||||
|
config,
|
||||||
|
base_port=args.base_port,
|
||||||
|
host=args.host,
|
||||||
|
):
|
||||||
|
print(
|
||||||
|
"AMPG_PREVIEW_MANIFEST "
|
||||||
|
f"site={result.site_id} "
|
||||||
|
f"path={result.path} "
|
||||||
|
f"fixtures={result.fixture_count}"
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.preview_command == "serve":
|
||||||
|
for endpoint in endpoints:
|
||||||
|
_print_preview_endpoint(endpoint)
|
||||||
|
with PreviewServers(endpoints) as servers:
|
||||||
|
print(
|
||||||
|
"AMPG_PREVIEW_SERVE "
|
||||||
|
f"status=running endpoints={len(servers.endpoints)}"
|
||||||
|
)
|
||||||
|
servers.wait_forever()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def _print_preview_endpoint(endpoint) -> None:
|
||||||
|
print(
|
||||||
|
"AMPG_PREVIEW_ENDPOINT "
|
||||||
|
f"site={endpoint.site_id} "
|
||||||
|
f"protocol={endpoint.protocol} "
|
||||||
|
f"renderer={endpoint.renderer} "
|
||||||
|
f"url={endpoint.url} "
|
||||||
|
f"root={endpoint.root} "
|
||||||
|
f"status={endpoint.status}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _cmd_routes(config, args) -> int:
|
def _cmd_routes(config, args) -> int:
|
||||||
exposures = route_exposures(config)
|
exposures = route_exposures(config)
|
||||||
issues = route_issues(config)
|
issues = route_issues(config)
|
||||||
|
|||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import AbstractContextManager
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from functools import partial
|
||||||
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import threading
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .config import GatewayConfig, ProtocolConfig, SiteConfig
|
||||||
|
from .manifest import fixture_manifest
|
||||||
|
|
||||||
|
|
||||||
|
PREVIEW_MANIFEST_NAME = "ampg-preview-fixture-manifest.json"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PreviewEndpoint:
|
||||||
|
site_id: str
|
||||||
|
protocol: str
|
||||||
|
renderer: str
|
||||||
|
root: Path
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
url: str
|
||||||
|
status: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PreviewManifestWriteResult:
|
||||||
|
site_id: str
|
||||||
|
path: Path
|
||||||
|
fixture_count: int
|
||||||
|
|
||||||
|
|
||||||
|
def preview_endpoints(
|
||||||
|
config: GatewayConfig,
|
||||||
|
*,
|
||||||
|
base_port: int = 19080,
|
||||||
|
host: str = "127.0.0.1",
|
||||||
|
) -> list[PreviewEndpoint]:
|
||||||
|
endpoints: list[PreviewEndpoint] = []
|
||||||
|
offset = 0
|
||||||
|
for site in config.sites:
|
||||||
|
for protocol in site.protocols.values():
|
||||||
|
if not protocol.enabled:
|
||||||
|
continue
|
||||||
|
port = int(protocol.options.get("preview_port", base_port + offset))
|
||||||
|
root = site.outputs.root / protocol.name
|
||||||
|
status = "ready" if (root / ".ampg-output").exists() else "missing-output"
|
||||||
|
endpoints.append(
|
||||||
|
PreviewEndpoint(
|
||||||
|
site_id=site.id,
|
||||||
|
protocol=protocol.name,
|
||||||
|
renderer=protocol.renderer,
|
||||||
|
root=root,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
url=f"http://{host}:{port}/",
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
offset += 1
|
||||||
|
return endpoints
|
||||||
|
|
||||||
|
|
||||||
|
def preview_fixture_manifest(
|
||||||
|
site: SiteConfig,
|
||||||
|
endpoints: list[PreviewEndpoint],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
by_protocol = {
|
||||||
|
endpoint.protocol: endpoint
|
||||||
|
for endpoint in endpoints
|
||||||
|
if endpoint.site_id == site.id
|
||||||
|
}
|
||||||
|
manifest = fixture_manifest(site)
|
||||||
|
manifest["mode"] = "preview"
|
||||||
|
for fixture in manifest["fixtures"]:
|
||||||
|
endpoint = by_protocol[fixture["protocol"]]
|
||||||
|
published_url = fixture["url"]
|
||||||
|
published_checks = dict(fixture["checks"])
|
||||||
|
fixture_path = fixture.get("route", {}).get("fixture_path", "/")
|
||||||
|
fixture["published"] = {
|
||||||
|
"url": published_url,
|
||||||
|
"checks": published_checks,
|
||||||
|
"address_status": fixture["address_status"],
|
||||||
|
}
|
||||||
|
fixture["preview"] = {
|
||||||
|
"mode": "loopback-http",
|
||||||
|
"root": str(endpoint.root),
|
||||||
|
"status": endpoint.status,
|
||||||
|
}
|
||||||
|
fixture["url"] = _preview_url(endpoint.url, fixture_path)
|
||||||
|
fixture["address_status"] = "preview"
|
||||||
|
fixture["checks"] = {
|
||||||
|
"transport": "clearnet",
|
||||||
|
"profile": "clearnet",
|
||||||
|
}
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
|
||||||
|
def write_preview_fixture_manifests(
|
||||||
|
config: GatewayConfig,
|
||||||
|
*,
|
||||||
|
base_port: int = 19080,
|
||||||
|
host: str = "127.0.0.1",
|
||||||
|
) -> list[PreviewManifestWriteResult]:
|
||||||
|
endpoints = preview_endpoints(config, base_port=base_port, host=host)
|
||||||
|
results: list[PreviewManifestWriteResult] = []
|
||||||
|
for site in config.sites:
|
||||||
|
manifest = preview_fixture_manifest(site, endpoints)
|
||||||
|
path = preview_manifest_path(site)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
results.append(
|
||||||
|
PreviewManifestWriteResult(
|
||||||
|
site_id=site.id,
|
||||||
|
path=path,
|
||||||
|
fixture_count=len(manifest["fixtures"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def preview_manifest_path(site: SiteConfig) -> Path:
|
||||||
|
return site.outputs.root / PREVIEW_MANIFEST_NAME
|
||||||
|
|
||||||
|
|
||||||
|
class PreviewServers(AbstractContextManager):
|
||||||
|
def __init__(self, endpoints: list[PreviewEndpoint]) -> None:
|
||||||
|
self.endpoints = endpoints
|
||||||
|
self._servers: list[ThreadingHTTPServer] = []
|
||||||
|
self._threads: list[threading.Thread] = []
|
||||||
|
|
||||||
|
def __enter__(self) -> "PreviewServers":
|
||||||
|
for endpoint in self.endpoints:
|
||||||
|
if endpoint.status != "ready":
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"{endpoint.site_id}/{endpoint.protocol}: output root is not built: {endpoint.root}"
|
||||||
|
)
|
||||||
|
handler = partial(SimpleHTTPRequestHandler, directory=str(endpoint.root))
|
||||||
|
server = ThreadingHTTPServer((endpoint.host, endpoint.port), handler)
|
||||||
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
self._servers.append(server)
|
||||||
|
self._threads.append(thread)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||||||
|
for server in self._servers:
|
||||||
|
server.shutdown()
|
||||||
|
server.server_close()
|
||||||
|
for thread in self._threads:
|
||||||
|
thread.join(timeout=2)
|
||||||
|
|
||||||
|
def wait_forever(self) -> None:
|
||||||
|
try:
|
||||||
|
threading.Event().wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def _preview_url(base_url: str, fixture_path: str) -> str:
|
||||||
|
if not fixture_path or fixture_path == "/":
|
||||||
|
return base_url
|
||||||
|
return base_url.rstrip("/") + "/" + fixture_path.lstrip("/")
|
||||||
@@ -32,11 +32,23 @@ or deployment notes.
|
|||||||
```sh
|
```sh
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml build
|
python3 -m ampg --config examples/wownero.gateway.toml build
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml manifest
|
python3 -m ampg --config examples/wownero.gateway.toml manifest
|
||||||
|
python3 -m ampg --config examples/wownero.gateway.toml preview manifest
|
||||||
```
|
```
|
||||||
|
|
||||||
`build` emits the manifest after writing protocol outputs. `manifest` rewrites only the
|
`build` emits the manifest after writing protocol outputs. `manifest` rewrites only the
|
||||||
manifest and does not rebuild protocol outputs.
|
manifest and does not rebuild protocol outputs.
|
||||||
|
|
||||||
|
`preview manifest` writes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<site.outputs.root>/ampg-preview-fixture-manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Preview manifests point fixture URLs at loopback HTTP servers for generated output roots.
|
||||||
|
They preserve the intended published URL and transport checks under each fixture's
|
||||||
|
`published` metadata. Use preview manifests to verify generated output with AMPB before
|
||||||
|
real Tor, I2P, Gemini, or other transport daemons are installed or adopted.
|
||||||
|
|
||||||
## Address Status
|
## Address Status
|
||||||
|
|
||||||
Some transports may not have final public addresses when a fixture is generated. In those
|
Some transports may not have final public addresses when a fixture is generated. In those
|
||||||
|
|||||||
@@ -139,6 +139,8 @@ daemon_policy = "auto"
|
|||||||
- `ampg plan` identifies the Wownero source tree and selected protocols.
|
- `ampg plan` identifies the Wownero source tree and selected protocols.
|
||||||
- `ampg build` creates clearnet, Tor, I2P, and Gemini output without requiring Markdown.
|
- `ampg build` creates clearnet, Tor, I2P, and Gemini output without requiring Markdown.
|
||||||
- `ampg build` writes a fixture manifest that AMPB can check.
|
- `ampg build` writes a fixture manifest that AMPB can check.
|
||||||
|
- `ampg preview manifest` writes loopback fixture URLs that AMPB can check before live
|
||||||
|
transport daemons are installed or adopted.
|
||||||
- Tor output contains no `<script>` tags or inline event handlers.
|
- Tor output contains no `<script>` tags or inline event handlers.
|
||||||
- Generated Gemini output has readable headings, paragraphs, and links.
|
- Generated Gemini output has readable headings, paragraphs, and links.
|
||||||
- Generated Micron output fits terminal-first browsing.
|
- Generated Micron output fits terminal-first browsing.
|
||||||
@@ -150,6 +152,8 @@ daemon_policy = "auto"
|
|||||||
python3 -m ampg --config examples/wownero.gateway.toml plan
|
python3 -m ampg --config examples/wownero.gateway.toml plan
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml build
|
python3 -m ampg --config examples/wownero.gateway.toml build
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml manifest
|
python3 -m ampg --config examples/wownero.gateway.toml manifest
|
||||||
|
python3 -m ampg --config examples/wownero.gateway.toml preview endpoints
|
||||||
|
python3 -m ampg --config examples/wownero.gateway.toml preview manifest
|
||||||
python3 -m ampg --config examples/i2p-only.gateway.toml plan
|
python3 -m ampg --config examples/i2p-only.gateway.toml plan
|
||||||
python3 -m ampg --config examples/i2p-only.gateway.toml build
|
python3 -m ampg --config examples/i2p-only.gateway.toml build
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ampg.build import build_gateway
|
||||||
|
from ampg.cli import main
|
||||||
|
from ampg.config import load_config
|
||||||
|
from ampg.preview import preview_endpoints, write_preview_fixture_manifests
|
||||||
|
|
||||||
|
|
||||||
|
class PreviewTest(unittest.TestCase):
|
||||||
|
def test_preview_manifest_rewrites_urls_to_loopback_and_preserves_published_metadata(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
config_path = _write_config(Path(tmp))
|
||||||
|
config = load_config(config_path)
|
||||||
|
build_gateway(config)
|
||||||
|
|
||||||
|
endpoints = preview_endpoints(config, base_port=19100)
|
||||||
|
self.assertEqual(["clearnet", "tor"], [endpoint.protocol for endpoint in endpoints])
|
||||||
|
self.assertEqual([19100, 19101], [endpoint.port for endpoint in endpoints])
|
||||||
|
self.assertEqual(["ready", "ready"], [endpoint.status for endpoint in endpoints])
|
||||||
|
|
||||||
|
results = write_preview_fixture_manifests(config, base_port=19100)
|
||||||
|
self.assertEqual(1, len(results))
|
||||||
|
self.assertEqual(4, results[0].fixture_count)
|
||||||
|
|
||||||
|
data = json.loads(results[0].path.read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual("preview", data["mode"])
|
||||||
|
by_protocol_path = {
|
||||||
|
(fixture["protocol"], fixture.get("route", {}).get("fixture_path", "/")): fixture
|
||||||
|
for fixture in data["fixtures"]
|
||||||
|
}
|
||||||
|
|
||||||
|
tor_root = by_protocol_path[("tor", "/")]
|
||||||
|
self.assertEqual("http://127.0.0.1:19101/", tor_root["url"])
|
||||||
|
self.assertEqual({"transport": "clearnet", "profile": "clearnet"}, tor_root["checks"])
|
||||||
|
self.assertEqual("http://preview.onion/", tor_root["published"]["url"])
|
||||||
|
self.assertEqual({"transport": "tor", "profile": "tor"}, tor_root["published"]["checks"])
|
||||||
|
self.assertEqual("preview", tor_root["address_status"])
|
||||||
|
self.assertEqual("ready", tor_root["preview"]["status"])
|
||||||
|
|
||||||
|
tor_play = by_protocol_path[("tor", "/play/")]
|
||||||
|
self.assertEqual("http://127.0.0.1:19101/play/", tor_play["url"])
|
||||||
|
|
||||||
|
def test_preview_manifest_cli(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
config_path = _write_config(Path(tmp))
|
||||||
|
config = load_config(config_path)
|
||||||
|
build_gateway(config)
|
||||||
|
stdout = io.StringIO()
|
||||||
|
|
||||||
|
with contextlib.redirect_stdout(stdout):
|
||||||
|
status = main(["--config", str(config_path), "preview", "manifest", "--base-port", "19200"])
|
||||||
|
self.assertTrue((config.sites[0].outputs.root / "ampg-preview-fixture-manifest.json").exists())
|
||||||
|
|
||||||
|
output = stdout.getvalue()
|
||||||
|
self.assertEqual(0, status)
|
||||||
|
self.assertIn("AMPG_PREVIEW_ENDPOINT site=preview protocol=clearnet", output)
|
||||||
|
self.assertIn("AMPG_PREVIEW_ENDPOINT site=preview protocol=tor", output)
|
||||||
|
self.assertIn("AMPG_PREVIEW_MANIFEST site=preview", output)
|
||||||
|
|
||||||
|
def test_preview_endpoints_report_missing_outputs(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
config = load_config(_write_config(Path(tmp)))
|
||||||
|
|
||||||
|
endpoints = preview_endpoints(config)
|
||||||
|
|
||||||
|
self.assertEqual(["missing-output", "missing-output"], [endpoint.status for endpoint in endpoints])
|
||||||
|
|
||||||
|
|
||||||
|
def _write_config(root: Path) -> Path:
|
||||||
|
source = root / "site"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "index.html").write_text("<h1>Preview</h1>", encoding="utf-8")
|
||||||
|
config_path = root / "gateway.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
"""
|
||||||
|
[[site]]
|
||||||
|
id = "preview"
|
||||||
|
domain = "preview.test"
|
||||||
|
|
||||||
|
[site.source]
|
||||||
|
kind = "static-html"
|
||||||
|
path = "./site"
|
||||||
|
canonical_url = "https://preview.test"
|
||||||
|
|
||||||
|
[site.outputs]
|
||||||
|
root = "./out"
|
||||||
|
plan_root = "./plan"
|
||||||
|
|
||||||
|
[[site.interactions.route]]
|
||||||
|
match = "/play/*"
|
||||||
|
tier = "interactive-lite"
|
||||||
|
|
||||||
|
[site.protocols.clearnet]
|
||||||
|
enabled = true
|
||||||
|
renderer = "clearnet"
|
||||||
|
|
||||||
|
[site.protocols.tor]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return config_path
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user