Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions granian/_granian.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import threading
from typing import Any

from ._types import WebsocketMessage
from .files import StaticFilesSettings
from .http import HTTP1Settings, HTTP2Settings

__version__: str
Expand Down Expand Up @@ -71,7 +72,7 @@ class ASGIWorker:
http1_opts: HTTP1Settings | None,
http2_opts: HTTP2Settings | None,
websockets_enabled: bool,
static_files: tuple[str, str, str | None, str | None] | None,
static_files: StaticFilesSettings | None,
ssl_enabled: bool,
ssl_cert: str | None,
ssl_key: str | None,
Expand All @@ -95,7 +96,7 @@ class WSGIWorker:
http_mode: str,
http1_opts: HTTP1Settings | None,
http2_opts: HTTP2Settings | None,
static_files: tuple[str, str, str | None, str | None] | None,
static_files: StaticFilesSettings | None,
ssl_enabled: bool,
ssl_cert: str | None,
ssl_key: str | None,
Expand All @@ -120,7 +121,7 @@ class RSGIWorker:
http1_opts: HTTP1Settings | None,
http2_opts: HTTP2Settings | None,
websockets_enabled: bool,
static_files: tuple[str, str, str | None, str | None] | None,
static_files: StaticFilesSettings | None,
ssl_enabled: bool,
ssl_cert: str | None,
ssl_key: str | None,
Expand Down
7 changes: 7 additions & 0 deletions granian/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,11 @@ def option(*param_decls: str, cls: type[click.Option] | None = None, **attrs: An
default=86400,
help='Cache headers expiration (in seconds or a human-readable duration) for static file serving. 0 to disable.',
)
@option(
'--static-path-precompressed/--no-static-path-precompressed',
default=False,
help='Enable serving precompressed static files with .br, .gz, or .zst extensions',
)
@option('--metrics/--no-metrics', 'metrics_enabled', default=False, help='Enable the prometheus metrics exporter.')
@option(
'--metrics-scrape-interval', default=15, type=Duration(1, 60), help='Configure the interval for metrics collection.'
Expand Down Expand Up @@ -491,6 +496,7 @@ def cli(
static_path_mount: list[pathlib.Path],
static_path_dir_to_file: str | None,
static_path_expires: int,
static_path_precompressed: bool,
metrics_enabled: bool,
metrics_scrape_interval: int,
metrics_address: str,
Expand Down Expand Up @@ -581,6 +587,7 @@ def cli(
static_path_mount=static_path_mount,
static_path_dir_to_file=static_path_dir_to_file,
static_path_expires=static_path_expires,
static_path_precompressed=static_path_precompressed,
metrics_enabled=metrics_enabled,
metrics_scrape_interval=metrics_scrape_interval,
metrics_address=metrics_address,
Expand Down
18 changes: 18 additions & 0 deletions granian/files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from dataclasses import dataclass


@dataclass
class StaticFilesSettings:
"""Configuration for static file serving.
Attributes:
mounts: List of (route_prefix, filesystem_path) tuples for serving static files.
dir_to_file: Optional filename to serve when a directory is requested (e.g., 'index.html').
expires: Cache-Control max-age value in seconds (as string), or None to disable.
precompressed: Whether to serve pre-compressed sidecar files (.br, .gz, .zst).
"""

mounts: list[tuple[str, str]]
dir_to_file: str | None = None
expires: str | None = None
precompressed: bool = False
23 changes: 14 additions & 9 deletions granian/server/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .._signals import set_main_signals
from ..constants import HTTPModes, Interfaces, Loops, RuntimeModes, SSLProtocols, TaskImpl
from ..errors import ConfigurationError, PidFileError
from ..files import StaticFilesSettings
from ..http import HTTP1Settings, HTTP2Settings
from ..log import DEFAULT_ACCESSLOG_FMT, LogLevels, configure_logging, logger
from ..net import SocketSpec, UnixSocketSpec
Expand Down Expand Up @@ -129,6 +130,7 @@ def __init__(
static_path_mount: Sequence[Path] | None = None,
static_path_dir_to_file: str | None = None,
static_path_expires: int = 86400,
static_path_precompressed: bool = False,
metrics_enabled: bool = False,
metrics_scrape_interval: int = 15,
metrics_address: str = '127.0.0.1',
Expand Down Expand Up @@ -186,7 +188,8 @@ def __init__(
self.factory = factory
self.working_dir = working_dir
self.env_files = env_files or ()
self.static_path = None
self.static_files = None
self.static_path_precompressed = static_path_precompressed
self.metrics_enabled = metrics_enabled
self.metrics_scrape_interval = metrics_scrape_interval
self.metrics_address = metrics_address
Expand Down Expand Up @@ -243,19 +246,21 @@ def _init_static_mounts(
if not paths:
return
if len(paths) == 1 and not routes:
self.static_path = (
[('/static', str(paths[0].resolve()))],
dir_to_file,
expires,
self.static_files = StaticFilesSettings(
mounts=[('/static', str(paths[0].resolve()))],
dir_to_file=dir_to_file,
expires=expires,
precompressed=self.static_path_precompressed,
)
return
if len(paths) != len(routes):
logger.error('Static path routes and mounts should have the same length')
raise ConfigurationError('static_path')
self.static_path = (
[(routes[idx], str(path.resolve())) for idx, path in enumerate(paths)],
dir_to_file,
expires,
self.static_files = StaticFilesSettings(
mounts=[(routes[idx], str(path.resolve())) for idx, path in enumerate(paths)],
dir_to_file=dir_to_file,
expires=expires,
precompressed=self.static_path_precompressed,
)

def build_ssl_context(
Expand Down
17 changes: 10 additions & 7 deletions granian/server/embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .._types import SSLCtx
from ..asgi import LifespanProtocol, _callback_wrapper as _asgi_call_wrap
from ..errors import ConfigurationError, FatalError
from ..files import StaticFilesSettings
from ..rsgi import _callback_wrapper as _rsgi_call_wrap, _callbacks_from_target as _rsgi_cbs_from_target
from .common import (
_PY_312,
Expand Down Expand Up @@ -128,6 +129,7 @@ def __init__(
static_path_mount: Sequence[Path] | None = None,
static_path_dir_to_file: str | None = None,
static_path_expires: int = 86400,
static_path_precompressed: bool = False,
):
super().__init__(
target=target,
Expand Down Expand Up @@ -164,6 +166,7 @@ def __init__(
static_path_mount=static_path_mount,
static_path_dir_to_file=static_path_dir_to_file,
static_path_expires=static_path_expires,
static_path_precompressed=static_path_precompressed,
)
self.main_loop_interrupt = asyncio.Event()

Expand All @@ -189,7 +192,7 @@ def _spawn_worker(self, idx, target, callback_loader) -> AsyncWorker:
self.http1_settings,
self.http2_settings,
self.websockets,
self.static_path,
self.static_files,
self.log_access_format if self.log_access else None,
self.ssl_ctx,
{'url_path_prefix': self.url_path_prefix},
Expand All @@ -215,7 +218,7 @@ async def _spawn_asgi_worker(
http1_settings: HTTP1Settings | None,
http2_settings: HTTP2Settings | None,
websockets: bool,
static_path: tuple[str, str, str | None, str | None] | None,
static_files: StaticFilesSettings | None,
log_access_fmt: str | None,
ssl_ctx: SSLCtx,
scope_opts: dict[str, Any],
Expand All @@ -235,7 +238,7 @@ async def _spawn_asgi_worker(
http1_settings,
http2_settings,
websockets,
static_path,
static_files,
*ssl_ctx,
(None, None),
)
Expand All @@ -261,7 +264,7 @@ async def _spawn_asgi_lifespan_worker(
http1_settings: HTTP1Settings | None,
http2_settings: HTTP2Settings | None,
websockets: bool,
static_path: tuple[str, str, str | None, str | None] | None,
static_files: StaticFilesSettings | None,
log_access_fmt: str | None,
ssl_ctx: SSLCtx,
scope_opts: dict[str, Any],
Expand Down Expand Up @@ -289,7 +292,7 @@ async def _spawn_asgi_lifespan_worker(
http1_settings,
http2_settings,
websockets,
static_path,
static_files,
*ssl_ctx,
(None, None),
)
Expand All @@ -316,7 +319,7 @@ async def _spawn_rsgi_worker(
http1_settings: HTTP1Settings | None,
http2_settings: HTTP2Settings | None,
websockets: bool,
static_path: tuple[str, str, str | None, str | None] | None,
static_files: StaticFilesSettings | None,
log_access_fmt: str | None,
ssl_ctx: SSLCtx,
scope_opts: dict[str, Any],
Expand All @@ -338,7 +341,7 @@ async def _spawn_rsgi_worker(
http1_settings,
http2_settings,
websockets,
static_path,
static_files,
*ssl_ctx,
(None, None),
)
Expand Down
19 changes: 10 additions & 9 deletions granian/server/mp.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .._internal import load_env
from .._types import SSLCtx
from ..asgi import LifespanProtocol, _callback_wrapper as _asgi_call_wrap
from ..files import StaticFilesSettings
from ..rsgi import _callback_wrapper as _rsgi_call_wrap, _callbacks_from_target as _rsgi_cbs_from_target
from ..wsgi import _callback_wrapper as _wsgi_call_wrap
from .common import (
Expand Down Expand Up @@ -129,7 +130,7 @@ def _spawn_asgi_worker(
http1_settings: HTTP1Settings | None,
http2_settings: HTTP2Settings | None,
websockets: bool,
static_path: tuple[str, str, str | None, str | None] | None,
static_files: StaticFilesSettings | None,
log_access_fmt: str | None,
ssl_ctx: SSLCtx,
scope_opts: dict[str, Any],
Expand All @@ -153,7 +154,7 @@ def _spawn_asgi_worker(
http1_settings,
http2_settings,
websockets,
static_path,
static_files,
*ssl_ctx,
metrics,
)
Expand All @@ -180,7 +181,7 @@ def _spawn_asgi_lifespan_worker(
http1_settings: HTTP1Settings | None,
http2_settings: HTTP2Settings | None,
websockets: bool,
static_path: tuple[str, str, str | None] | None,
static_files: StaticFilesSettings | None,
log_access_fmt: str | None,
ssl_ctx: SSLCtx,
scope_opts: dict[str, Any],
Expand Down Expand Up @@ -212,7 +213,7 @@ def _spawn_asgi_lifespan_worker(
http1_settings,
http2_settings,
websockets,
static_path,
static_files,
*ssl_ctx,
metrics,
)
Expand Down Expand Up @@ -240,7 +241,7 @@ def _spawn_rsgi_worker(
http1_settings: HTTP1Settings | None,
http2_settings: HTTP2Settings | None,
websockets: bool,
static_path: tuple[str, str, str | None] | None,
static_files: StaticFilesSettings | None,
log_access_fmt: str | None,
ssl_ctx: SSLCtx,
scope_opts: dict[str, Any],
Expand All @@ -266,7 +267,7 @@ def _spawn_rsgi_worker(
http1_settings,
http2_settings,
websockets,
static_path,
static_files,
*ssl_ctx,
metrics,
)
Expand Down Expand Up @@ -294,7 +295,7 @@ def _spawn_wsgi_worker(
http1_settings: HTTP1Settings | None,
http2_settings: HTTP2Settings | None,
websockets: bool,
static_path: tuple[str, str, str | None] | None,
static_files: StaticFilesSettings | None,
log_access_fmt: str | None,
ssl_ctx: SSLCtx,
scope_opts: dict[str, Any],
Expand All @@ -317,7 +318,7 @@ def _spawn_wsgi_worker(
http_mode,
http1_settings,
http2_settings,
static_path,
static_files,
*ssl_ctx,
metrics,
)
Expand Down Expand Up @@ -427,7 +428,7 @@ def _spawn_worker(self, idx, target, callback_loader) -> WorkerProcess:
self.http1_settings,
self.http2_settings,
self.websockets,
self.static_path,
self.static_files,
self.log_access_format if self.log_access else None,
self.ssl_ctx,
{'url_path_prefix': self.url_path_prefix},
Expand Down
Loading