httk-web 0.1.0__py3-none-any.whl

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.
httk/web/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .api import create_asgi_app, publish, serve
2
+
3
+ __all__ = ["create_asgi_app", "serve", "publish"]
httk/web/api.py ADDED
@@ -0,0 +1,71 @@
1
+ from pathlib import Path
2
+
3
+ from starlette.applications import Starlette
4
+
5
+ from .engine.site_engine import SiteEngine
6
+ from .model.config import SiteConfig
7
+ from .model.page import PublishReport
8
+ from .publishing.static import publish_site
9
+ from .runtime.asgi import create_app
10
+ from .runtime.devserver import run_dev_server
11
+
12
+
13
+ def create_asgi_app(
14
+ srcdir: str | Path,
15
+ *,
16
+ baseurl: str | None = None,
17
+ compatibility_mode: bool = False,
18
+ config_name: str = "config",
19
+ debug: bool = False,
20
+ ) -> Starlette:
21
+ config = SiteConfig.from_srcdir(
22
+ srcdir=srcdir,
23
+ baseurl=baseurl,
24
+ compatibility_mode=compatibility_mode,
25
+ config_name=config_name,
26
+ )
27
+ engine = SiteEngine(config)
28
+ return create_app(engine=engine, debug=debug)
29
+
30
+
31
+ def serve(
32
+ srcdir: str | Path,
33
+ *,
34
+ host: str = "127.0.0.1",
35
+ port: int = 8080,
36
+ baseurl: str | None = None,
37
+ compatibility_mode: bool = False,
38
+ config_name: str = "config",
39
+ debug: bool = False,
40
+ ) -> None:
41
+ app = create_asgi_app(
42
+ srcdir=srcdir,
43
+ baseurl=baseurl,
44
+ compatibility_mode=compatibility_mode,
45
+ config_name=config_name,
46
+ debug=debug,
47
+ )
48
+ run_dev_server(app=app, host=host, port=port)
49
+
50
+
51
+ def publish(
52
+ srcdir: str | Path,
53
+ outdir: str | Path,
54
+ baseurl: str,
55
+ *,
56
+ host_static: str | None = None,
57
+ compatibility_mode: bool = False,
58
+ config_name: str = "config",
59
+ use_urls_without_ext: bool | None = None,
60
+ ) -> PublishReport:
61
+ publish_use_urls_without_ext = use_urls_without_ext if use_urls_without_ext is not None else not compatibility_mode
62
+ config = SiteConfig.from_srcdir(
63
+ srcdir=srcdir,
64
+ baseurl=baseurl,
65
+ host_static=host_static,
66
+ compatibility_mode=compatibility_mode,
67
+ config_name=config_name,
68
+ publish_use_urls_without_ext=publish_use_urls_without_ext,
69
+ )
70
+ engine = SiteEngine(config)
71
+ return publish_site(engine=engine, outdir=outdir)
@@ -0,0 +1 @@
1
+ # Placeholder package for compatibility helpers.
@@ -0,0 +1,3 @@
1
+ from .site_engine import SiteEngine
2
+
3
+ __all__ = ["SiteEngine"]
@@ -0,0 +1,51 @@
1
+ from pathlib import Path
2
+
3
+ from httk.web.model.config import SiteConfig
4
+ from httk.web.model.page import ResolvedRoute
5
+
6
+ DEFAULT_CONTENT_EXTENSIONS: tuple[str, ...] = (".md", ".rst", ".html", ".httkweb")
7
+
8
+
9
+ def normalize_route(route: str) -> str:
10
+ normalized = route.lstrip("/").strip()
11
+ if normalized in {"", "."}:
12
+ return "index"
13
+ return normalized
14
+
15
+
16
+ def _is_within(path: Path, root: Path) -> bool:
17
+ try:
18
+ path.resolve().relative_to(root.resolve())
19
+ return True
20
+ except ValueError:
21
+ return False
22
+
23
+
24
+ def resolve_route(config: SiteConfig, route: str) -> ResolvedRoute:
25
+ normalized = normalize_route(route)
26
+
27
+ static_candidate = (config.static_dir / normalized).resolve()
28
+ if static_candidate.exists() and static_candidate.is_file() and _is_within(static_candidate, config.static_dir):
29
+ return ResolvedRoute(kind="static", route=normalized, source_path=static_candidate)
30
+
31
+ content = _resolve_content_route(content_dir=config.content_dir, route=normalized)
32
+ if content is not None:
33
+ return ResolvedRoute(kind="content", route=normalized, source_path=content)
34
+
35
+ return ResolvedRoute(kind="missing", route=normalized)
36
+
37
+
38
+ def _resolve_content_route(content_dir: Path, route: str) -> Path | None:
39
+ route_path = Path(route)
40
+
41
+ if route_path.suffix:
42
+ candidate = (content_dir / route_path).resolve()
43
+ if candidate.exists() and candidate.is_file() and _is_within(candidate, content_dir):
44
+ return candidate
45
+
46
+ for ext in DEFAULT_CONTENT_EXTENSIONS:
47
+ candidate = (content_dir / route_path).with_suffix(ext).resolve()
48
+ if candidate.exists() and candidate.is_file() and _is_within(candidate, content_dir):
49
+ return candidate
50
+
51
+ return None